domma-cms 0.37.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
- import{SHORTCODES as T}from"./shortcode-registry.js";import{EFFECTS as g}from"./effect-defs.js";import{findEnclosingShortcode as j,setAttr as S,setAttrs as D,effectAttrKeys as W}from"./shortcode-tags.js";import{insertAtCursor as _,replaceRange as Y}from"./markdown-toolbar.js";import{api as h}from"../api.js";const k=new Map,F={forms:async()=>(await h.forms.list()).map(e=>[e.slug,e.title||e.slug]),collections:async()=>(await h.collections.list()).map(e=>[e.slug,e.title||e.slug]),views:async()=>(await h.views.list()).map(e=>[e.slug,e.title||e.slug]),blocks:async()=>(await h.blocks.list()).map(e=>{const t=e?.name??e;return[t,t]}),actions:async()=>(await h.actions.list()).map(e=>[e.slug,e.name||e.title||e.slug]),menus:async()=>(await h.menus.list()).map(e=>[e.slug,e.name||e.title||e.slug]),"menu-locations":async()=>Object.keys(await h.menuLocations.get()||{}).map(e=>[e,e]),"collection-fields":async()=>{const e=i?.span?.attrs?.slug;return!e||e===!0?[]:((await h.collections.get(e)).fields||[]).map(n=>[n.name,n.label||n.name])}};function R(e){const t=e==="collection-fields"?`${e}:${i?.span?.attrs?.slug}`:e;return k.has(t)||k.set(t,F[e]().catch(()=>[])),k.get(t)}const B=T.map(e=>({name:e.name,selfClosing:!!e.selfClosing})),U=new Map(T.map(e=>[e.name,e])),K=["Layout","Content","Components","Data","Media","Effects"],V=new Map(g.map(e=>[e.name,e])),G=g.map(e=>({name:e.name,selfClosing:!!e.selfClosing})),X=[...B,...G],A=new Set(["card","hero","banner","col","button"]);let i=null;const M="position:fixed;z-index:10000;min-width:210px;max-height:70vh;overflow-y:auto;background:var(--dm-surface,#1c1c30);border:1px solid var(--dm-border,#2a2a40);border-radius:6px;box-shadow:var(--dm-shadow-lg);padding:.3rem;font-size:.83rem;color:var(--dm-text,#e5e5e5);",u="display:flex;align-items:center;gap:.5rem;width:100%;text-align:left;padding:.35rem .55rem;border:none;background:transparent;color:inherit;border-radius:4px;cursor:pointer;font-size:.83rem;",L="padding:.3rem .55rem;color:var(--dm-text-muted,#8a8a9a);font-size:.68rem;text-transform:uppercase;letter-spacing:.06em;",q="flex:1;min-width:0;padding:.2rem .4rem;background:var(--dm-input-bg,#12121e);border:1px solid var(--dm-border,#333);border-radius:3px;color:var(--dm-text,#eee);font-size:.8rem;",J="height:1px;background:var(--dm-border,#2a2a40);margin:.3rem .2rem;";function m(e){return e.addEventListener("mouseenter",()=>{e.style.background="var(--dm-primary,#4338ca)",e.style.color="var(--dm-primary-text,#fff)"}),e.addEventListener("mouseleave",()=>{e.style.background="transparent",e.style.color="inherit"}),e}export function attachContextMenu(e){e.addEventListener("contextmenu",t=>{t.preventDefault(),p();const n=j(e,X);Q(e,n,t.clientX,t.clientY)})}function p(){i&&($(),i.root.remove(),document.removeEventListener("mousedown",N,!0),document.removeEventListener("keydown",O,!0),window.removeEventListener("scroll",p,!0),i=null)}const N=e=>{i&&!i.root.contains(e.target)&&!(i.submenu&&i.submenu.contains(e.target))&&p()},O=e=>{e.key==="Escape"&&p()};function Q(e,t,n,o){k.clear();const s=document.createElement("div");s.className="dm-sc-menu",s.style.cssText=M,i={root:s,textarea:e,span:t,submenu:null};const a=t&&(U.get(t.name)||V.get(t.name)),l=t&&A.has(t.name)?g.filter(r=>r.name in t.attrs):[];a&&ee(s,a);for(const r of l)ce(s,r);(a||l.length)&&s.appendChild(Z()),s.appendChild(C("Add effect\u2026",()=>re())),s.appendChild(C("Insert\u2026",()=>pe()));const c=e.selectionStart!==e.selectionEnd;(t||c)&&s.appendChild(C(c?"Wrap selection in\u2026":"Wrap in\u2026",()=>me())),document.body.appendChild(s),z(s,n,o),document.addEventListener("mousedown",N,!0),document.addEventListener("keydown",O,!0),window.addEventListener("scroll",p,!0),window.I&&I.scan(s)}function Z(){const e=document.createElement("div");return e.style.cssText=J,e}function x(e){const t=document.createElement("div");return t.style.cssText=L,t.textContent=e,t}function ee(e,t){e.appendChild(x(`${t.label} options`));for(const n of t.attrs||[])e.appendChild(P(n))}function P(e,t=e.name){const n=i.span.attrs[t];switch(e.type){case"checkbox":return oe(e,t,te(n,e.default));case"select":return ie(e,t,n);case"lookup":return le(e,t,n);case"colour":return ae(e,t,n);default:return se(e,t,n)}}function v(e,t){i.span=S(i.textarea,i.span,e,t)}function te(e,t){return e==null?!!t:!(e===!1||e==="false"||e==="0")}function ne(e,t){return t===!!e.default?null:t?"true":"false"}function oe(e,t,n){const o=m(document.createElement("label"));o.style.cssText=u;const s=document.createElement("input");return s.type="checkbox",s.checked=n,s.addEventListener("change",()=>v(t,ne(e,s.checked))),o.appendChild(s),o.appendChild(document.createTextNode(e.label)),o}function se(e,t,n){const o=document.createElement("div");o.style.cssText=u+"cursor:default;";const s=document.createElement("span");s.textContent=e.label,s.style.cssText="white-space:nowrap;color:var(--dm-text-muted,#9a9aac);";const a=document.createElement("input");a.type=e.type==="number"?"number":"text",a.value=n===!0||n==null?"":n,e.default!=null&&e.default!==""&&(a.placeholder=String(e.default)),a.style.cssText=q;let l=null;return a.addEventListener("input",()=>{clearTimeout(l),l=setTimeout(()=>v(t,a.value.trim()),180)}),a.addEventListener("mousedown",c=>c.stopPropagation()),o.appendChild(s),o.appendChild(a),o}function ae(e,t,n){const o=document.createElement("div");o.style.cssText=u+"cursor:default;";const s=document.createElement("span");s.textContent=e.label,s.style.flex="1";const a=document.createElement("input");return a.type="color",typeof n=="string"&&/^#/.test(n)?a.value=n:typeof e.default=="string"&&/^#/.test(e.default)&&(a.value=e.default),a.addEventListener("input",()=>v(t,a.value)),o.appendChild(s),o.appendChild(a),o}function ie(e,t,n){const o=n==null||n===!0?e.default??"":n;return C(`${e.label}${o!==""?`: ${o}`:""}`,()=>{const s=w();for(const[a,l]of e.options||[]){const c=m(document.createElement("button"));c.type="button",c.style.cssText=u,c.textContent=l,a===o&&(c.style.fontWeight="700"),c.addEventListener("click",()=>{v(t,a),$()}),s.appendChild(c)}return s})}function le(e,t,n){const o=n==null||n===!0?e.default??"":n;return C(`${e.label}${o!==""?`: ${o}`:""}`,()=>{const s=w();s.style.minHeight="48px";const a=typeof E<"u"&&E.loader?E.loader(s,{type:"dots"}):null;return R(e.source).then(l=>{if(a&&a.destroy(),!i||i.submenu!==s)return;if(s.style.minHeight="",!l.length){const d=document.createElement("div");d.style.cssText=L,d.textContent="Nothing found",s.appendChild(d)}const c=l.some(([d])=>d===o),r=[["","\u2014 none \u2014"],...l];o!==""&&!c&&r.push([o,`${o} (missing)`]);for(const[d,b]of r){const f=m(document.createElement("button"));f.type="button",f.style.cssText=u,f.textContent=b,d===o&&(f.style.fontWeight="700"),f.addEventListener("click",()=>{v(t,d===""?null:d),$()}),s.appendChild(f)}}),s})}function ce(e,t){e.appendChild(x(`Effect: ${t.label}`));for(const o of t.attrs||[])e.appendChild(P(o,`${t.name}-${o.name}`));const n=m(document.createElement("button"));n.type="button",n.style.cssText=u,n.innerHTML='<span data-icon="trash" data-icon-size="13" style="display:inline-flex;align-items:center;"></span> Remove effect',n.addEventListener("click",()=>{const o={};for(const s of W(i.span.attrs,t.name))o[s]=null;D(i.textarea,i.span,o),p()}),e.appendChild(n)}function re(){const e=w(),t=i.span,n=t&&A.has(t.name)?t.name:null;for(const o of[...new Set(g.map(s=>s.category))]){const s=g.filter(a=>a.category===o);if(s.length){e.appendChild(x(o));for(const a of s){const l=n&&(a.hosts||[]).includes(n)&&!(a.name in t.attrs),c=m(document.createElement("button"));if(c.type="button",c.style.cssText=u+"justify-content:space-between;",c.appendChild(document.createTextNode(a.label)),l){const r=document.createElement("span");r.textContent=`inject \u2192 [${n}]`,r.style.cssText="opacity:.6;font-size:.7rem;",c.appendChild(r)}c.addEventListener("click",()=>{if(l)S(i.textarea,i.span,a.name,!0);else{const r=a.selfClosing?`[${a.name} /]`:`[${a.name}]
1
+ import{SHORTCODES as A}from"./shortcode-registry.js";import{EFFECTS as _}from"./effect-defs.js";import{findEnclosingShortcode as K,setAttr as N,setAttrs as V,effectAttrKeys as G}from"./shortcode-tags.js";import{insertAtCursor as R,replaceRange as X}from"./markdown-toolbar.js";import{api as g}from"../api.js";const M=new Map,q={forms:async()=>(await g.forms.list()).map(e=>[e.slug,e.title||e.slug]),collections:async()=>(await g.collections.list()).map(e=>[e.slug,e.title||e.slug]),views:async()=>(await g.views.list()).map(e=>[e.slug,e.title||e.slug]),blocks:async()=>(await g.blocks.list()).map(e=>{const t=e?.name??e;return[t,t]}),actions:async()=>(await g.actions.list()).map(e=>[e.slug,e.name||e.title||e.slug]),menus:async()=>(await g.menus.list()).map(e=>[e.slug,e.name||e.title||e.slug]),"menu-locations":async()=>Object.keys(await g.menuLocations.get()||{}).map(e=>[e,e]),"collection-fields":async()=>{const e=a?.span?.attrs?.slug;return!e||e===!0?[]:((await g.collections.get(e)).fields||[]).map(n=>[n.name,n.label||n.name])}};function O(e){const t=e==="collection-fields"?`${e}:${a?.span?.attrs?.slug}`:e;return M.has(t)||M.set(t,q[e]().catch(()=>[])),M.get(t)}const J=A.map(e=>({name:e.name,selfClosing:!!e.selfClosing})),Q=new Map(A.map(e=>[e.name,e])),Z=["Layout","Content","Components","Data","Media","Effects"],ee=new Map(_.map(e=>[e.name,e])),te=_.map(e=>({name:e.name,selfClosing:!!e.selfClosing})),ne=[...J,...te],z=new Set(["card","hero","banner","col","button"]);let a=null;const H="position:fixed;z-index:10000;min-width:210px;max-height:70vh;overflow-y:auto;background:var(--dm-surface,#1c1c30);border:1px solid var(--dm-border,#2a2a40);border-radius:6px;box-shadow:var(--dm-shadow-lg);padding:.3rem;font-size:.83rem;color:var(--dm-text,#e5e5e5);",p="display:flex;align-items:center;gap:.5rem;width:100%;text-align:left;padding:.35rem .55rem;border:none;background:transparent;color:inherit;border-radius:4px;cursor:pointer;font-size:.83rem;",L="padding:.3rem .55rem;color:var(--dm-text-muted,#8a8a9a);font-size:.68rem;text-transform:uppercase;letter-spacing:.06em;",j="flex:1;min-width:0;padding:.2rem .4rem;background:var(--dm-input-bg,#12121e);border:1px solid var(--dm-border,#333);border-radius:3px;color:var(--dm-text,#eee);font-size:.8rem;",oe="height:1px;background:var(--dm-border,#2a2a40);margin:.3rem .2rem;";function m(e){return e.addEventListener("mouseenter",()=>{e.style.background="var(--dm-primary,#4338ca)",e.style.color="var(--dm-primary-text,#fff)"}),e.addEventListener("mouseleave",()=>{e.style.background="transparent",e.style.color="inherit"}),e}export function attachContextMenu(e){e.addEventListener("contextmenu",t=>{t.preventDefault(),b();const n=K(e,ne);se(e,n,t.clientX,t.clientY)})}function b(){a&&(T(),a.root.remove(),document.removeEventListener("mousedown",W,!0),document.removeEventListener("keydown",Y,!0),window.removeEventListener("scroll",D,!0),a=null)}const W=e=>{a&&!a.root.contains(e.target)&&!(a.submenu&&a.submenu.contains(e.target))&&b()},D=e=>{a&&!a.root.contains(e.target)&&!(a.submenu&&a.submenu.contains(e.target))&&b()},Y=e=>{e.key==="Escape"&&b()};function se(e,t,n,o){M.clear();const s=document.createElement("div");s.className="dm-sc-menu",s.style.cssText=H,a={root:s,textarea:e,span:t,submenu:null};const i=t&&(Q.get(t.name)||ee.get(t.name)),l=t&&z.has(t.name)?_.filter(r=>r.name in t.attrs):[];i&&ae(s,i);for(const r of l)he(s,r);(i||l.length)&&s.appendChild(ie()),s.appendChild(v("Add effect\u2026",()=>ye())),s.appendChild(v("Insert\u2026",()=>xe()));const c=e.selectionStart!==e.selectionEnd;(t||c)&&s.appendChild(v(c?"Wrap selection in\u2026":"Wrap in\u2026",()=>Ee())),document.body.appendChild(s),U(s,n,o),document.addEventListener("mousedown",W,!0),document.addEventListener("keydown",Y,!0),window.addEventListener("scroll",D,!0),window.I&&I.scan(s)}function ie(){const e=document.createElement("div");return e.style.cssText=oe,e}function k(e){const t=document.createElement("div");return t.style.cssText=L,t.textContent=e,t}function ae(e,t){e.appendChild(k(`${t.label} options`));for(const n of t.attrs||[])e.appendChild(F(n))}function F(e,t=e.name){const n=a.span.attrs[t];switch(e.type){case"checkbox":return re(e,t,le(n,e.default));case"select":return pe(e,t,n);case"lookup":return fe(e,t,n);case"colour":return ue(e,t,n);case"icon":return me(e,t,n);default:return de(e,t,n)}}function C(e,t){a.span=N(a.textarea,a.span,e,t)}function le(e,t){return e==null?!!t:!(e===!1||e==="false"||e==="0")}function ce(e,t){return t===!!e.default?null:t?"true":"false"}function re(e,t,n){const o=m(document.createElement("label"));o.style.cssText=p;const s=document.createElement("input");return s.type="checkbox",s.checked=n,s.addEventListener("change",()=>C(t,ce(e,s.checked))),o.appendChild(s),o.appendChild(document.createTextNode(e.label)),o}function de(e,t,n){const o=document.createElement("div");o.style.cssText=p+"cursor:default;";const s=document.createElement("span");s.textContent=e.label,s.style.cssText="white-space:nowrap;color:var(--dm-text-muted,#9a9aac);";const i=document.createElement("input");i.type=e.type==="number"?"number":"text",i.value=n===!0||n==null?"":n,e.default!=null&&e.default!==""&&(i.placeholder=String(e.default)),i.style.cssText=j;let l=null;return i.addEventListener("input",()=>{clearTimeout(l),l=setTimeout(()=>C(t,i.value.trim()),180)}),i.addEventListener("mousedown",c=>c.stopPropagation()),o.appendChild(s),o.appendChild(i),o}function ue(e,t,n){const o=document.createElement("div");o.style.cssText=p+"cursor:default;";const s=document.createElement("span");s.textContent=e.label,s.style.flex="1";const i=document.createElement("input");return i.type="color",typeof n=="string"&&/^#/.test(n)?i.value=n:typeof e.default=="string"&&/^#/.test(e.default)&&(i.value=e.default),i.addEventListener("input",()=>C(t,i.value)),o.appendChild(s),o.appendChild(i),o}function pe(e,t,n){const o=n==null||n===!0?e.default??"":n;return v(`${e.label}${o!==""?`: ${o}`:""}`,()=>{const s=w();for(const[i,l]of e.options||[]){const c=m(document.createElement("button"));c.type="button",c.style.cssText=p,c.textContent=l,i===o&&(c.style.fontWeight="700"),c.addEventListener("click",()=>{C(t,i),T()}),s.appendChild(c)}return s})}function me(e,t,n){const o=n==null||n===!0?"":String(n),s=o?`${e.label}: ${o}`:e.label;return v(s,()=>{const i=w();i.style.width="232px";const l=document.createElement("input");l.type="text",l.placeholder="Search icons\u2026",l.style.cssText=j+"margin:6px;width:calc(100% - 12px);",l.addEventListener("mousedown",y=>y.stopPropagation()),i.appendChild(l);const c=m(document.createElement("button"));c.type="button",c.style.cssText=p,c.textContent="\u2014 none \u2014",o||(c.style.fontWeight="700"),c.addEventListener("click",()=>{C(t,null),T()}),i.appendChild(c);const r=document.createElement("div");r.style.cssText="display:flex;flex-wrap:wrap;gap:2px;padding:4px 6px;max-height:220px;overflow-y:auto;",i.appendChild(r);const d=96,h=()=>{const y=typeof I<"u"&&typeof I.list=="function"?I.list():[],P=l.value.trim().toLowerCase(),$=P?y.filter(u=>u.toLowerCase().includes(P)):y;r.replaceChildren();for(const u of $.slice(0,d)){const x=m(document.createElement("button"));x.type="button",x.title=u,x.style.cssText="width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid transparent;border-radius:4px;cursor:pointer;color:inherit;",u===o&&(x.style.borderColor="var(--dm-primary,#5b8cff)");const S=document.createElement("span");S.setAttribute("data-icon",u),S.setAttribute("data-icon-size","15"),S.style.cssText="display:inline-flex;align-items:center;",x.appendChild(S),x.addEventListener("click",()=>{C(t,u),T()}),r.appendChild(x)}if($.length>d){const u=document.createElement("div");u.style.cssText=L,u.textContent=`${$.length-d} more \u2014 refine search`,r.appendChild(u)}if(!$.length){const u=document.createElement("div");u.style.cssText=L,u.textContent="No icons match",r.appendChild(u)}window.I&&I.scan(r)};let f=null;return l.addEventListener("input",()=>{clearTimeout(f),f=setTimeout(h,120)}),h(),setTimeout(()=>l.focus(),0),i})}function fe(e,t,n){const o=n==null||n===!0?e.default??"":n;return v(`${e.label}${o!==""?`: ${o}`:""}`,()=>{const s=w();s.style.minHeight="48px";const i=typeof E<"u"&&E.loader?E.loader(s,{type:"dots"}):null;return O(e.source).then(l=>{if(i&&i.destroy(),!a||a.submenu!==s)return;if(s.style.minHeight="",!l.length){const d=document.createElement("div");d.style.cssText=L,d.textContent="Nothing found",s.appendChild(d)}const c=l.some(([d])=>d===o),r=[["","\u2014 none \u2014"],...l];o!==""&&!c&&r.push([o,`${o} (missing)`]);for(const[d,h]of r){const f=m(document.createElement("button"));f.type="button",f.style.cssText=p,f.textContent=h,d===o&&(f.style.fontWeight="700"),f.addEventListener("click",()=>{C(t,d===""?null:d),T()}),s.appendChild(f)}}),s})}function he(e,t){e.appendChild(k(`Effect: ${t.label}`));for(const o of t.attrs||[])e.appendChild(F(o,`${t.name}-${o.name}`));const n=m(document.createElement("button"));n.type="button",n.style.cssText=p,n.innerHTML='<span data-icon="trash" data-icon-size="13" style="display:inline-flex;align-items:center;"></span> Remove effect',n.addEventListener("click",()=>{const o={};for(const s of G(a.span.attrs,t.name))o[s]=null;V(a.textarea,a.span,o),b()}),e.appendChild(n)}function ye(){const e=w(),t=a.span,n=t&&z.has(t.name)?t.name:null;for(const o of[...new Set(_.map(s=>s.category))]){const s=_.filter(i=>i.category===o);if(s.length){e.appendChild(k(o));for(const i of s){const l=n&&(i.hosts||[]).includes(n)&&!(i.name in t.attrs),c=m(document.createElement("button"));if(c.type="button",c.style.cssText=p+"justify-content:space-between;",c.appendChild(document.createTextNode(i.label)),l){const r=document.createElement("span");r.textContent=`inject \u2192 [${n}]`,r.style.cssText="opacity:.6;font-size:.7rem;",c.appendChild(r)}c.addEventListener("click",()=>{if(l)N(a.textarea,a.span,i.name,!0);else{const r=i.selfClosing?`[${i.name} /]`:`[${i.name}]
2
2
  Content
3
- [/${a.name}]`;_(i.textarea,r)}p()}),e.appendChild(c)}}}return e}function C(e,t){const n=m(document.createElement("button"));n.type="button",n.style.cssText=u+"justify-content:space-between;",n.appendChild(document.createTextNode(e));const o=document.createElement("span");return o.setAttribute("data-icon","chevron-right"),o.setAttribute("data-icon-size","12"),o.style.cssText="opacity:.6;display:inline-flex;align-items:center;flex-shrink:0;",n.appendChild(o),n.addEventListener("mouseenter",()=>H(n,t)),n.addEventListener("click",s=>{s.stopPropagation(),H(n,t)}),n}function w(){const e=document.createElement("div");return e.className="dm-sc-submenu",e.style.cssText=M,e}function H(e,t){$();const n=t();i.submenu=n,document.body.appendChild(n);const o=e.getBoundingClientRect();z(n,o.right+2,o.top,o.left-2),window.I&&I.scan(n)}function $(){i&&i.submenu&&(i.submenu.remove(),i.submenu=null)}function de(e){return(e.attrs||[]).find(t=>t.type==="lookup"&&t.source!=="collection-fields")||null}function ue(e,t,n){const o=e.template||`[${e.name} /]`,s=new RegExp(`\\b${t}="[^"]*"`);return s.test(o)?o.replace(s,`${t}="${n}"`):o.replace(`[${e.name}`,`[${e.name} ${t}="${n}"`)}function pe(){const e=w();for(const t of K){const n=T.filter(o=>o.category===t&&!o.child);if(n.length){e.appendChild(x(t));for(const o of n){const s=de(o),a=m(document.createElement("button"));if(a.type="button",a.style.cssText=u+(s?"justify-content:space-between;":""),a.appendChild(document.createTextNode(o.label)),s){const l=document.createElement("span");l.setAttribute("data-icon","chevron-right"),l.setAttribute("data-icon-size","12"),l.style.cssText="opacity:.6;display:inline-flex;align-items:center;flex-shrink:0;",a.appendChild(l)}a.addEventListener("click",async l=>{if(!s){_(i.textarea,o.template||`[${o.name} /]`),p();return}l.stopPropagation(),e.textContent="",e.style.minHeight="48px";const c=typeof E<"u"&&E.loader?E.loader(e,{type:"dots"}):null,r=await R(s.source);if(c&&c.destroy(),!i||i.submenu!==e)return;if(e.style.minHeight="",e.appendChild(x(`Insert ${o.label}`)),!r.length){const b=document.createElement("div");b.style.cssText=L,b.textContent="Nothing found \u2014 inserting the blank template instead.",e.appendChild(b)}for(const[b,f]of r){const y=m(document.createElement("button"));y.type="button",y.style.cssText=u,y.textContent=f,y.addEventListener("click",()=>{_(i.textarea,ue(o,s.name,b)),p()}),e.appendChild(y)}const d=m(document.createElement("button"));d.type="button",d.style.cssText=u+"opacity:.75;",d.textContent=r.length?"Blank template\u2026":"Insert blank template",d.addEventListener("click",()=>{_(i.textarea,o.template||`[${o.name} /]`),p()}),e.appendChild(d)}),e.appendChild(a)}}}return e}function me(){const e=w(),t=T.filter(n=>n.wrappable||n.container);for(const n of t){const o=m(document.createElement("button"));o.type="button",o.style.cssText=u,o.textContent=n.label,o.addEventListener("click",()=>{fe(n),p()}),e.appendChild(o)}return e}function fe(e){const t=i.textarea,n=t.selectionStart,o=t.selectionEnd,s=t.value.slice(n,o)||"Content";let a;e.name==="grid"?a=`[grid cols="2"]
3
+ [/${i.name}]`;R(a.textarea,r)}b()}),e.appendChild(c)}}}return e}function v(e,t){const n=m(document.createElement("button"));n.type="button",n.style.cssText=p+"justify-content:space-between;",n.appendChild(document.createTextNode(e));const o=document.createElement("span");return o.setAttribute("data-icon","chevron-right"),o.setAttribute("data-icon-size","12"),o.style.cssText="opacity:.6;display:inline-flex;align-items:center;flex-shrink:0;",n.appendChild(o),n.addEventListener("mouseenter",()=>B(n,t)),n.addEventListener("click",s=>{s.stopPropagation(),B(n,t)}),n}function w(){const e=document.createElement("div");return e.className="dm-sc-submenu",e.style.cssText=H,e}function B(e,t){T();const n=t();a.submenu=n,document.body.appendChild(n);const o=e.getBoundingClientRect();U(n,o.right+2,o.top,o.left-2),window.I&&I.scan(n)}function T(){a&&a.submenu&&(a.submenu.remove(),a.submenu=null)}function be(e){return(e.attrs||[]).find(t=>t.type==="lookup"&&t.source!=="collection-fields")||null}function ge(e,t,n){const o=e.template||`[${e.name} /]`,s=new RegExp(`\\b${t}="[^"]*"`);return s.test(o)?o.replace(s,`${t}="${n}"`):o.replace(`[${e.name}`,`[${e.name} ${t}="${n}"`)}function xe(){const e=w();for(const t of Z){const n=A.filter(o=>o.category===t&&!o.child);if(n.length){e.appendChild(k(t));for(const o of n){const s=be(o),i=m(document.createElement("button"));if(i.type="button",i.style.cssText=p+(s?"justify-content:space-between;":""),i.appendChild(document.createTextNode(o.label)),s){const l=document.createElement("span");l.setAttribute("data-icon","chevron-right"),l.setAttribute("data-icon-size","12"),l.style.cssText="opacity:.6;display:inline-flex;align-items:center;flex-shrink:0;",i.appendChild(l)}i.addEventListener("click",async l=>{if(!s){R(a.textarea,o.template||`[${o.name} /]`),b();return}l.stopPropagation(),e.textContent="",e.style.minHeight="48px";const c=typeof E<"u"&&E.loader?E.loader(e,{type:"dots"}):null,r=await O(s.source);if(c&&c.destroy(),!a||a.submenu!==e)return;if(e.style.minHeight="",e.appendChild(k(`Insert ${o.label}`)),!r.length){const h=document.createElement("div");h.style.cssText=L,h.textContent="Nothing found \u2014 inserting the blank template instead.",e.appendChild(h)}for(const[h,f]of r){const y=m(document.createElement("button"));y.type="button",y.style.cssText=p,y.textContent=f,y.addEventListener("click",()=>{R(a.textarea,ge(o,s.name,h)),b()}),e.appendChild(y)}const d=m(document.createElement("button"));d.type="button",d.style.cssText=p+"opacity:.75;",d.textContent=r.length?"Blank template\u2026":"Insert blank template",d.addEventListener("click",()=>{R(a.textarea,o.template||`[${o.name} /]`),b()}),e.appendChild(d)}),e.appendChild(i)}}}return e}function Ee(){const e=w(),t=A.filter(n=>n.wrappable||n.container);for(const n of t){const o=m(document.createElement("button"));o.type="button",o.style.cssText=p,o.textContent=n.label,o.addEventListener("click",()=>{Ce(n),b()}),e.appendChild(o)}return e}function Ce(e){const t=a.textarea,n=t.selectionStart,o=t.selectionEnd,s=t.value.slice(n,o)||"Content";let i;e.name==="grid"?i=`[grid cols="2"]
4
4
  [col]
5
5
  ${s}
6
6
  [/col]
7
- [/grid]`:a=`[${e.name}]
7
+ [/grid]`:i=`[${e.name}]
8
8
  ${s}
9
- [/${e.name}]`,Y(t,n,o,a)}function z(e,t,n,o){e.style.left="0px",e.style.top="0px";const s=e.getBoundingClientRect();let a=t;t+s.width>window.innerWidth-8&&(a=o!=null?o-s.width:t-s.width);let l=n;n+s.height>window.innerHeight-8&&(l=Math.max(8,window.innerHeight-s.height-8)),e.style.left=`${Math.max(8,a)}px`,e.style.top=`${Math.max(8,l)}px`}
9
+ [/${e.name}]`,X(t,n,o,i)}function U(e,t,n,o){e.style.left="0px",e.style.top="0px";const s=e.getBoundingClientRect();let i=t;t+s.width>window.innerWidth-8&&(i=o!=null?o-s.width:t-s.width);let l=n;n+s.height>window.innerHeight-8&&(l=Math.max(8,window.innerHeight-s.height-8)),e.style.left=`${Math.max(8,i)}px`,e.style.top=`${Math.max(8,l)}px`}
@@ -29,6 +29,7 @@
29
29
  <div class="card-body" style="display:flex;gap:.75rem;align-items:center;flex-wrap:wrap;">
30
30
  <input id="entry-search" type="text" class="form-input" placeholder="Search entries…" style="flex:1;min-width:200px;">
31
31
  <span id="entry-count" class="text-muted" style="white-space:nowrap;font-size:.85rem;"></span>
32
+ <div id="entry-filter-chips" style="display:none;flex-basis:100%;gap:.4rem;flex-wrap:wrap;align-items:center;"></div>
32
33
  </div>
33
34
  </div>
34
35
 
@@ -1 +1 @@
1
- import{api as h}from"../api.js";import{keepTableIconsAlive as M}from"../lib/table-icons.js";function L(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}let u=null,g=null,d=[],N=1,w=[],x={};const O=50;export const collectionEntriesView={templateUrl:"/admin/js/templates/collection-entries.html",async onMount(t){d=[],N=1,g=null,w=[],x={};const i=window.location.hash.match(/\/collections\/([^/?#]+)\/entries/);if(u=i?i[1]:null,!u){E.toast("No collection selected.",{type:"error"});return}const s=E.loader(t.get(0),{type:"dots"});try{if(g=await h.collections.get(u),!g){s.destroy(),E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const o=t.find("#entries-title").get(0);o&&(o.textContent=g.title+" \u2014 Entries");const e=t.find("#edit-schema-btn").get(0);e&&e.setAttribute("href",`#/collections/edit/${u}`);const r=t.find("#form-btn").get(0);r&&(new Set(["roles","user-profiles"]).has(u)?r.style.display="none":r.setAttribute("href",`#/forms/edit/${u}`))}catch{s.destroy(),E.toast("Failed to load collection schema.",{type:"error"});return}try{w=await h.actions.forCollection(u)}catch{w=[]}try{await v(t)}finally{s.destroy()}await k(t);const l=t.find("#entry-search").get(0);l&&l.addEventListener("input",()=>{const o=l.value.toLowerCase().trim(),e=o?d.filter(r=>Object.values(r.data||{}).some(a=>String(a).toLowerCase().includes(o))):d;C(e,t)}),t.find("#add-entry-btn").off("click").on("click",()=>{A(null,t)}),t.find("#export-btn").off("click").on("click",()=>{const o=E.slideover({title:"Export Entries",size:"sm",position:"right"}),e=document.createElement("div");e.style.cssText="padding:1.25rem;display:flex;flex-direction:column;gap:1rem;",["json","csv"].forEach(r=>{const a=document.createElement("button");a.className="btn btn-ghost",a.style.cssText="justify-content:flex-start;gap:.5rem;",a.textContent=r==="json"?"Export as JSON":"Export as CSV",a.addEventListener("click",()=>{fetch(`/api/collections/${u}/export?format=${r}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}}).then(c=>c.blob()).then(c=>{const n=document.createElement("a");n.href=URL.createObjectURL(c),n.download=`${u}-entries.${r}`,document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(n.href),o.close()}).catch(()=>E.toast("Export failed.",{type:"error"}))}),e.appendChild(a)}),o.setContent(e),o.open()}),t.find("#import-btn").off("click").on("click",()=>{I(t)}),t.find("#clear-all-btn").off("click").on("click",async()=>{if(await E.confirm("Delete ALL entries? This cannot be undone."))try{await h.collections.clearEntries(u),d=[],E.toast("All entries cleared.",{type:"success"}),C([],t)}catch{E.toast("Failed to clear entries.",{type:"error"})}}),t.off("click",".js-run-action").on("click",".js-run-action",async function(o){o.stopPropagation();const e=this,{actionSlug:r,entryId:a,confirmMessage:c}=e.dataset;if(!(c&&!await E.confirm(c))){e.disabled=!0;try{const n=await h.actions.execute(r,a);if(n.success)E.toast(`Action completed (${n.stepsCompleted} step${n.stepsCompleted!==1?"s":""}).`,{type:"success"}),await v(t);else{const p=n.results?.find(f=>!f.success);E.toast(p?.error||"Action failed.",{type:"error"})}}catch(n){E.toast(n.message||"Action failed.",{type:"error"})}finally{e.disabled=!1}}}),t.off("click",".js-edit-entry").on("click",".js-edit-entry",function(o){o.stopPropagation();const e=d.find(r=>r.id===this.dataset.id);e&&A(e,t)}),t.off("click",".js-delete-entry").on("click",".js-delete-entry",async function(o){o.stopPropagation();const e=this.dataset.id;if(await E.confirm("Delete this entry?"))try{await h.collections.deleteEntry(u,e),d=d.filter(a=>a.id!==e),E.toast("Entry deleted.",{type:"success"}),C(d,t)}catch{E.toast("Failed to delete entry.",{type:"error"})}}),t.off("click","#entries-table tbody tr").on("click","#entries-table tbody tr",function(o){if(o.target.closest(".js-run-action, .js-edit-entry, .js-delete-entry"))return;const e=this.querySelector(".js-edit-entry")?.dataset.id,r=d.find(a=>a.id===e);r&&B(r)}),Domma.icons.scan(),M(t.get(0))}};async function k(t){const m=w.filter(s=>s.access?.rowLevel);if(m.length===0)return;const i=d.map(s=>s.id);i.length!==0&&(await Promise.all(m.map(async s=>{try{const{allowed:l}=await h.actions.checkAccess(s.slug,i);x[s.slug]=new Set(l)}catch{x[s.slug]=null}})),C(d,t))}async function v(t){try{const m=await h.collections.listEntries(u,{limit:500});d=m.entries||m||[]}catch{d=[],E.toast("Could not load entries.",{type:"error"})}C(d,t),await k(t)}function C(t,m){const i=m.find("#entry-count").get(0);i&&(i.textContent=t.length===d.length?`${d.length} entr${d.length!==1?"ies":"y"}`:`Showing ${t.length} of ${d.length}`);const l=[...(g?.fields||[]).slice(0,5).map(e=>({key:`data.${e.name}`,title:e.label||e.name,render:(r,a)=>{const c=a.data?.[e.name]??"",n=String(c),p=document.createElement("span");return p.title=n,p.textContent=n.length>60?n.slice(0,60)+"\u2026":n,p.outerHTML}})),{key:"meta",title:"Created",render:e=>D(e?.createdAt).format("DD MMM YYYY HH:mm")},{key:"id",title:"",render:e=>{const r=document.createElement("div");r.style.cssText="display:flex;gap:.3rem;justify-content:flex-end;flex-wrap:wrap;",w.forEach(n=>{if(x[n.slug]!==void 0&&x[n.slug]!==null&&!x[n.slug].has(e))return;const p=document.createElement("button");p.className="btn btn-sm btn-ghost js-run-action",p.dataset.actionSlug=n.slug,p.dataset.entryId=e,p.dataset.confirmMessage=n.trigger?.confirmMessage||"",p.title=n.title||n.slug;const f=document.createElement("span");f.setAttribute("data-icon",L(n.trigger?.icon||"zap")),f.setAttribute("data-icon-size","14"),p.appendChild(f),p.appendChild(document.createTextNode(" "+(n.trigger?.label||n.title))),r.appendChild(p)});const a=document.createElement("button");a.className="btn btn-sm btn-ghost js-edit-entry",a.dataset.id=e,a.setAttribute("data-tooltip","Edit"),a.innerHTML='<span data-icon="edit" data-icon-size="14"></span>';const c=document.createElement("button");return c.className="btn btn-sm btn-ghost js-delete-entry",c.dataset.id=e,c.setAttribute("data-tooltip","Delete"),c.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',r.appendChild(a),r.appendChild(c),r.outerHTML}}];T.create("#entries-table",{data:t,columns:l,emptyMessage:'No entries yet. Click "Add Entry" to get started.'});const o=document.querySelector("#entries-table");o&&(o.querySelectorAll("tbody tr").forEach(e=>{e.style.cursor="pointer"}),Domma.icons.scan(o),o.querySelectorAll("[data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})}))}function z(t,m){const i={};return t.forEach(s=>{const l={};s.fullWidth?l.span=m:s.span>1&&(l.span=s.span),s.placeholder&&(l.placeholder=s.placeholder),s.helper&&(l.hint=s.helper);const o=s.type==="checkbox"?"boolean":s.type==="date"?"string":s.type;i[s.name]={type:o,label:s.label,required:s.required,options:s.options,...Object.keys(l).length?{formConfig:l}:{}}}),i}function A(t,m){const i=!!t,s=E.slideover({title:i?"Edit Entry":"Add Entry",size:"md",position:"right"}),l=document.createElement("div");l.style.cssText="padding:.25rem 0 .5rem;";const o=g?.fields||[],e=g?.columns||2,r=g?.layout||"stacked",a=document.createElement("div");F.create(z(o,e),t?.data||{},{layout:r,columns:e,showSubmitButton:!1}).renderTo(a),(o||[]).forEach(f=>{if(f.type!=="date"||!f.name)return;const y=a.querySelector(`[name="${f.name}"]`);y&&y.type!=="date"&&(y.type="date")}),l.appendChild(a);const c=document.createElement("div");c.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.5rem;";const n=document.createElement("button");n.className="btn btn-ghost",n.textContent="Cancel";const p=document.createElement("button");p.className="btn btn-primary",p.textContent=i?"Save Changes":"Add Entry",c.appendChild(n),c.appendChild(p),l.appendChild(c),s.element.appendChild(l),s.open(),n.addEventListener("click",()=>s.close()),p.addEventListener("click",async()=>{const f={};o.forEach(y=>{const b=a.querySelector(`[name="${y.name}"]`);b&&(f[y.name]=y.type==="checkbox"?b.checked:b.value)}),p.disabled=!0;try{if(i){const y=await h.collections.updateEntry(u,t.id,f),b=d.findIndex(j=>j.id===t.id);b!==-1&&(d[b]=y),E.toast("Entry updated.",{type:"success"})}else{const y=await h.collections.createEntry(u,f);d.unshift(y),E.toast("Entry added.",{type:"success"})}s.close(),C(d,m||{find:()=>({get:()=>null})})}catch(y){E.toast(y.message||"Failed to save entry.",{type:"error"}),p.disabled=!1}})}function B(t){const m=document.createElement("div"),i=g?.fields||[],s=document.createElement("div");s.style.cssText="display:flex;flex-direction:column;gap:.75rem;margin-bottom:1.25rem;",i.forEach(e=>{const r=t.data?.[e.name];if(r==null||r==="")return;const a=document.createElement("div");a.style.cssText="border-bottom:1px solid var(--dm-border,#333);padding-bottom:.6rem;";const c=document.createElement("strong");c.textContent=e.label||e.name,c.style.cssText="display:block;font-size:.8rem;color:var(--dm-text-muted,#888);margin-bottom:.2rem;";const n=document.createElement("p");n.textContent=String(r),n.style.cssText="margin:0;word-break:break-word;",a.appendChild(c),a.appendChild(n),s.appendChild(a)});const l=document.createElement("div");if(l.style.cssText="font-size:.8rem;color:var(--dm-text-muted,#888);display:flex;flex-direction:column;gap:.3rem;border-top:1px solid var(--dm-border,#333);padding-top:.75rem;",t.meta?.createdAt){const e=document.createElement("span");e.textContent=`Created: ${D(t.meta.createdAt).format("DD MMM YYYY HH:mm")}`,l.appendChild(e)}if(t.meta?.source){const e=document.createElement("span");e.textContent=`Source: ${t.meta.source}`,l.appendChild(e)}m.appendChild(s),m.appendChild(l);const o=E.slideover({title:"Entry Details",size:"md",position:"right"});o.element.appendChild(m),o.open()}function I(t){const m=E.slideover({title:"Import Entries",size:"md",position:"right"}),i=document.createElement("div");i.style.cssText="padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem;";const s=document.createElement("p");s.textContent='Paste a JSON array of entries. Each item should have a "data" object with field values.',s.style.cssText="font-size:.875rem;color:var(--dm-text-muted,#888);margin:0;",i.appendChild(s);const l=document.createElement("textarea");l.className="form-input",l.rows=10,l.placeholder='[{"data": {"name": "Example"}}, ...]',i.appendChild(l);const o=document.createElement("p");o.style.cssText="font-size:.875rem;margin:0;display:none;",i.appendChild(o);const e=document.createElement("div");e.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;";const r=document.createElement("button");r.className="btn btn-ghost",r.textContent="Cancel";const a=document.createElement("button");a.className="btn btn-primary",a.textContent="Import",e.appendChild(r),e.appendChild(a),i.appendChild(e),m.element.appendChild(i),m.open(),r.addEventListener("click",()=>m.close()),a.addEventListener("click",async()=>{let c;try{if(c=JSON.parse(l.value),!Array.isArray(c))throw new Error("Must be a JSON array")}catch(n){o.style.display="",o.style.color="var(--dm-danger,#f87171)",o.textContent=`Invalid JSON: ${n.message}`;return}a.disabled=!0;try{const n=await h.collections.import(u,c);o.style.display="",o.style.color="var(--dm-success,#4ade80)",o.textContent=`Imported ${n.imported} entries. Skipped: ${n.skipped}.`,await v(t||{find:()=>({get:()=>null,off:()=>({on:()=>{}})})}),setTimeout(()=>m.close(),1500)}catch(n){o.style.display="",o.style.color="var(--dm-danger,#f87171)",o.textContent=n.message||"Import failed."}finally{a.disabled=!1}})}
1
+ import{api as h}from"../api.js";import{keepTableIconsAlive as _}from"../lib/table-icons.js";function O(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}let y=null,g=null,u=[],H=1,L=[],k={},b=[];const te=50,P={eq:"=",ne:"\u2260",contains:"contains",gt:">",lt:"<"};function U(e,r){const a=e.data?.[r.field],o=String(a??"");switch(r.op){case"eq":return o===String(r.value);case"ne":return o!==String(r.value);case"contains":return o.toLowerCase().includes(String(r.value).toLowerCase());case"gt":case"lt":{const c=parseFloat(o),t=parseFloat(r.value);if(!Number.isNaN(c)&&!Number.isNaN(t))return r.op==="gt"?c>t:c<t;const n=Date.parse(o),i=Date.parse(r.value);return!Number.isNaN(n)&&!Number.isNaN(i)?r.op==="gt"?n>i:n<i:r.op==="gt"?o>String(r.value):o<String(r.value)}default:return!0}}function x(e){const r=(e.find("#entry-search").get(0)?.value||"").toLowerCase().trim();let a=u;for(const o of b)a=a.filter(c=>U(c,o));r&&(a=a.filter(o=>Object.values(o.data||{}).some(c=>String(c).toLowerCase().includes(r)))),W(e),B(a,e)}function W(e){const r=e.find("#entry-filter-chips").get(0);if(!r)return;if(r.replaceChildren(),!b.length){r.style.display="none";return}r.style.display="flex",b.forEach((o,c)=>{const t=document.createElement("span");t.className="badge badge-secondary",t.style.cssText="display:inline-flex;align-items:center;gap:.35rem;font-size:.78rem;";const n=String(o.value);t.textContent=`${o.label} ${P[o.op]||o.op} \u201C${n.length>24?n.slice(0,24)+"\u2026":n}\u201D`;const i=document.createElement("button");i.type="button",i.textContent="\xD7",i.title="Remove filter",i.style.cssText="background:none;border:none;cursor:pointer;color:inherit;font-size:.9rem;line-height:1;padding:0;",i.addEventListener("click",()=>{b.splice(c,1),x(e)}),t.appendChild(i),r.appendChild(t)});const a=document.createElement("button");a.type="button",a.className="btn btn-ghost btn-sm",a.textContent="Clear all",a.addEventListener("click",()=>{b=[],x(e)}),r.appendChild(a)}let v=null;const J="position:fixed;z-index:10000;min-width:200px;max-height:60vh;overflow-y:auto;background:var(--dm-surface,#1c1c30);border:1px solid var(--dm-border,#2a2a40);border-radius:6px;box-shadow:var(--dm-shadow-lg);padding:.3rem;font-size:.83rem;color:var(--dm-text,#e5e5e5);",G="display:flex;align-items:center;gap:.5rem;width:100%;text-align:left;background:none;border:none;color:inherit;font:inherit;padding:.35rem .6rem;border-radius:4px;cursor:pointer;";function C(){v&&(v.remove(),v=null,document.removeEventListener("mousedown",A,!0),document.removeEventListener("keydown",j,!0),window.removeEventListener("scroll",z,!0))}const A=e=>{v&&!v.contains(e.target)&&C()},j=e=>{e.key==="Escape"&&C()},z=e=>{v&&!v.contains(e.target)&&C()};function N(e,r){const a=document.createElement("button");return a.type="button",a.style.cssText=G,a.textContent=e,a.addEventListener("mouseenter",()=>{a.style.background="var(--dm-surface-2,rgba(255,255,255,.06))"}),a.addEventListener("mouseleave",()=>{a.style.background="none"}),a.addEventListener("click",r),a}function V(e){const r=document.createElement("div");return r.style.cssText="padding:.3rem .6rem;font-size:.72rem;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted,#9a9aac);",r.textContent=e,r}function K(e,r,a,o,c){C();const t=document.createElement("div");t.style.cssText=J,v=t;const n=a.label||a.name,i=String(o).length>28?String(o).slice(0,28)+"\u2026":String(o),l=(m,f)=>{b.push({field:a.name,label:n,op:m,value:f}),x(c),C()};t.appendChild(V(n)),o!==""&&(t.appendChild(N(`Filter: = \u201C${i}\u201D`,()=>l("eq",o))),t.appendChild(N(`Filter: \u2260 \u201C${i}\u201D`,()=>l("ne",o))),["number","date","time"].includes(a.type)&&(t.appendChild(N(`Filter: > \u201C${i}\u201D`,()=>l("gt",o))),t.appendChild(N(`Filter: < \u201C${i}\u201D`,()=>l("lt",o)))));const d=document.createElement("div");d.style.cssText="padding:.35rem .6rem;";const s=document.createElement("input");if(s.type="text",s.placeholder=`${n} contains\u2026`,s.className="form-input",s.style.cssText="width:100%;font-size:.8rem;padding:.3rem .5rem;",s.addEventListener("mousedown",m=>m.stopPropagation()),s.addEventListener("keydown",m=>{m.key==="Enter"&&s.value.trim()&&l("contains",s.value.trim())}),d.appendChild(s),t.appendChild(d),b.length){const m=document.createElement("div");m.style.cssText="border-top:1px solid var(--dm-border,#2a2a40);margin:.25rem 0;",t.appendChild(m),t.appendChild(N(`Clear all filters (${b.length})`,()=>{b=[],x(c),C()}))}document.body.appendChild(t);const p=t.getBoundingClientRect();t.style.left=Math.min(e,window.innerWidth-p.width-8)+"px",t.style.top=Math.min(r,window.innerHeight-p.height-8)+"px",document.addEventListener("mousedown",A,!0),document.addEventListener("keydown",j,!0),window.addEventListener("scroll",z,!0),setTimeout(()=>s.focus(),0)}export const collectionEntriesView={templateUrl:"/admin/js/templates/collection-entries.html",async onMount(e){u=[],H=1,g=null,L=[],k={},b=[],C();const a=window.location.hash.match(/\/collections\/([^/?#]+)\/entries/);if(y=a?a[1]:null,!y){E.toast("No collection selected.",{type:"error"});return}const o=E.loader(e.get(0),{type:"dots"});try{if(g=await h.collections.get(y),!g){o.destroy(),E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const t=e.find("#entries-title").get(0);t&&(t.textContent=g.title+" \u2014 Entries");const n=e.find("#edit-schema-btn").get(0);n&&n.setAttribute("href",`#/collections/edit/${y}`);const i=e.find("#form-btn").get(0);i&&(new Set(["roles","user-profiles"]).has(y)?i.style.display="none":i.setAttribute("href",`#/forms/edit/${y}`))}catch{o.destroy(),E.toast("Failed to load collection schema.",{type:"error"});return}try{L=await h.actions.forCollection(y)}catch{L=[]}try{await M(e)}finally{o.destroy()}await I(e);const c=e.find("#entry-search").get(0);c&&c.addEventListener("input",()=>x(e)),e.get(0).addEventListener("contextmenu",t=>{const n=t.target.closest("#entries-table tbody td");if(!n)return;const l=(g?.fields||[]).slice(0,5)[n.cellIndex];if(!l)return;t.preventDefault();const d=n.querySelector("span[title]")?.title??n.textContent.trim();K(t.clientX,t.clientY,l,d,e)}),e.find("#add-entry-btn").off("click").on("click",()=>{q(null,e)}),e.find("#export-btn").off("click").on("click",()=>{const t=E.slideover({title:"Export Entries",size:"sm",position:"right"}),n=document.createElement("div");n.style.cssText="padding:1.25rem;display:flex;flex-direction:column;gap:1rem;",["json","csv"].forEach(i=>{const l=document.createElement("button");l.className="btn btn-ghost",l.style.cssText="justify-content:flex-start;gap:.5rem;",l.textContent=i==="json"?"Export as JSON":"Export as CSV",l.addEventListener("click",()=>{fetch(`/api/collections/${y}/export?format=${i}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}}).then(d=>d.blob()).then(d=>{const s=document.createElement("a");s.href=URL.createObjectURL(d),s.download=`${y}-entries.${i}`,document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(s.href),t.close()}).catch(()=>E.toast("Export failed.",{type:"error"}))}),n.appendChild(l)}),t.setContent(n),t.open()}),e.find("#import-btn").off("click").on("click",()=>{Q(e)}),e.find("#clear-all-btn").off("click").on("click",async()=>{if(await E.confirm("Delete ALL entries? This cannot be undone."))try{await h.collections.clearEntries(y),u=[],E.toast("All entries cleared.",{type:"success"}),x(e)}catch{E.toast("Failed to clear entries.",{type:"error"})}}),e.off("click",".js-run-action").on("click",".js-run-action",async function(t){t.stopPropagation();const n=this,{actionSlug:i,entryId:l,confirmMessage:d}=n.dataset;if(!(d&&!await E.confirm(d))){n.disabled=!0;try{const s=await h.actions.execute(i,l);if(s.success)E.toast(`Action completed (${s.stepsCompleted} step${s.stepsCompleted!==1?"s":""}).`,{type:"success"}),await M(e);else{const p=s.results?.find(m=>!m.success);E.toast(p?.error||"Action failed.",{type:"error"})}}catch(s){E.toast(s.message||"Action failed.",{type:"error"})}finally{n.disabled=!1}}}),e.off("click",".js-edit-entry").on("click",".js-edit-entry",function(t){t.stopPropagation();const n=u.find(i=>i.id===this.dataset.id);n&&q(n,e)}),e.off("click",".js-delete-entry").on("click",".js-delete-entry",async function(t){t.stopPropagation();const n=this.dataset.id;if(await E.confirm("Delete this entry?"))try{await h.collections.deleteEntry(y,n),u=u.filter(l=>l.id!==n),E.toast("Entry deleted.",{type:"success"}),x(e)}catch{E.toast("Failed to delete entry.",{type:"error"})}}),e.off("click","#entries-table tbody tr").on("click","#entries-table tbody tr",function(t){if(t.target.closest(".js-run-action, .js-edit-entry, .js-delete-entry"))return;const n=this.querySelector(".js-edit-entry")?.dataset.id,i=u.find(l=>l.id===n);i&&Z(i)}),Domma.icons.scan(),_(e.get(0))}};async function I(e){const r=L.filter(o=>o.access?.rowLevel);if(r.length===0)return;const a=u.map(o=>o.id);a.length!==0&&(await Promise.all(r.map(async o=>{try{const{allowed:c}=await h.actions.checkAccess(o.slug,a);k[o.slug]=new Set(c)}catch{k[o.slug]=null}})),x(e))}async function M(e){try{const r=await h.collections.listEntries(y,{limit:500});u=r.entries||r||[]}catch{u=[],E.toast("Could not load entries.",{type:"error"})}x(e),await I(e)}function B(e,r){const a=r.find("#entry-count").get(0);a&&(a.textContent=e.length===u.length?`${u.length} entr${u.length!==1?"ies":"y"}`:`Showing ${e.length} of ${u.length}`);const c=[...(g?.fields||[]).slice(0,5).map(n=>({key:`data.${n.name}`,title:n.label||n.name,render:(i,l)=>{const d=l.data?.[n.name]??"",s=String(d),p=document.createElement("span");return p.title=s,p.textContent=s.length>60?s.slice(0,60)+"\u2026":s,p.outerHTML}})),{key:"meta",title:"Created",render:n=>D(n?.createdAt).format("DD MMM YYYY HH:mm")},{key:"id",title:"",render:n=>{const i=document.createElement("div");i.style.cssText="display:flex;gap:.3rem;justify-content:flex-end;flex-wrap:wrap;",L.forEach(s=>{if(k[s.slug]!==void 0&&k[s.slug]!==null&&!k[s.slug].has(n))return;const p=document.createElement("button");p.className="btn btn-sm btn-ghost js-run-action",p.dataset.actionSlug=s.slug,p.dataset.entryId=n,p.dataset.confirmMessage=s.trigger?.confirmMessage||"",p.title=s.title||s.slug;const m=document.createElement("span");m.setAttribute("data-icon",O(s.trigger?.icon||"zap")),m.setAttribute("data-icon-size","14"),p.appendChild(m),p.appendChild(document.createTextNode(" "+(s.trigger?.label||s.title))),i.appendChild(p)});const l=document.createElement("button");l.className="btn btn-sm btn-ghost js-edit-entry",l.dataset.id=n,l.setAttribute("data-tooltip","Edit"),l.innerHTML='<span data-icon="edit" data-icon-size="14"></span>';const d=document.createElement("button");return d.className="btn btn-sm btn-ghost js-delete-entry",d.dataset.id=n,d.setAttribute("data-tooltip","Delete"),d.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',i.appendChild(l),i.appendChild(d),i.outerHTML}}];T.create("#entries-table",{data:e,columns:c,emptyMessage:'No entries yet. Click "Add Entry" to get started.'});const t=document.querySelector("#entries-table");t&&(t.querySelectorAll("tbody tr").forEach(n=>{n.style.cursor="pointer"}),Domma.icons.scan(t),t.querySelectorAll("[data-tooltip]").forEach(n=>{E.tooltip(n,{content:n.getAttribute("data-tooltip"),position:"top"})}))}function X(e,r){const a={};return e.forEach(o=>{const c={};o.fullWidth?c.span=r:o.span>1&&(c.span=o.span),o.placeholder&&(c.placeholder=o.placeholder),o.helper&&(c.hint=o.helper);const t=o.type==="checkbox"?"boolean":o.type==="date"?"string":o.type;a[o.name]={type:t,label:o.label,required:o.required,options:o.options,...Object.keys(c).length?{formConfig:c}:{}}}),a}function q(e,r){const a=!!e,o=E.slideover({title:a?"Edit Entry":"Add Entry",size:"md",position:"right"}),c=document.createElement("div");c.style.cssText="padding:.25rem 0 .5rem;";const t=g?.fields||[],n=g?.columns||2,i=g?.layout||"stacked",l=document.createElement("div");F.create(X(t,n),e?.data||{},{layout:i,columns:n,showSubmitButton:!1}).renderTo(l),(t||[]).forEach(m=>{if(m.type!=="date"||!m.name)return;const f=l.querySelector(`[name="${m.name}"]`);f&&f.type!=="date"&&(f.type="date")}),c.appendChild(l);const d=document.createElement("div");d.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.5rem;";const s=document.createElement("button");s.className="btn btn-ghost",s.textContent="Cancel";const p=document.createElement("button");p.className="btn btn-primary",p.textContent=a?"Save Changes":"Add Entry",d.appendChild(s),d.appendChild(p),c.appendChild(d),o.element.appendChild(c),o.open(),s.addEventListener("click",()=>o.close()),p.addEventListener("click",async()=>{const m={};t.forEach(f=>{const w=l.querySelector(`[name="${f.name}"]`);w&&(m[f.name]=f.type==="checkbox"?w.checked:w.value)}),p.disabled=!0;try{if(a){const f=await h.collections.updateEntry(y,e.id,m),w=u.findIndex(Y=>Y.id===e.id);w!==-1&&(u[w]=f),E.toast("Entry updated.",{type:"success"})}else{const f=await h.collections.createEntry(y,m);u.unshift(f),E.toast("Entry added.",{type:"success"})}o.close(),r?x(r):B(u,{find:()=>({get:()=>null})})}catch(f){E.toast(f.message||"Failed to save entry.",{type:"error"}),p.disabled=!1}})}function Z(e){const r=document.createElement("div"),a=g?.fields||[],o=document.createElement("div");o.style.cssText="display:flex;flex-direction:column;gap:.75rem;margin-bottom:1.25rem;",a.forEach(n=>{const i=e.data?.[n.name];if(i==null||i==="")return;const l=document.createElement("div");l.style.cssText="border-bottom:1px solid var(--dm-border,#333);padding-bottom:.6rem;";const d=document.createElement("strong");d.textContent=n.label||n.name,d.style.cssText="display:block;font-size:.8rem;color:var(--dm-text-muted,#888);margin-bottom:.2rem;";const s=document.createElement("p");s.textContent=String(i),s.style.cssText="margin:0;word-break:break-word;",l.appendChild(d),l.appendChild(s),o.appendChild(l)});const c=document.createElement("div");if(c.style.cssText="font-size:.8rem;color:var(--dm-text-muted,#888);display:flex;flex-direction:column;gap:.3rem;border-top:1px solid var(--dm-border,#333);padding-top:.75rem;",e.meta?.createdAt){const n=document.createElement("span");n.textContent=`Created: ${D(e.meta.createdAt).format("DD MMM YYYY HH:mm")}`,c.appendChild(n)}if(e.meta?.source){const n=document.createElement("span");n.textContent=`Source: ${e.meta.source}`,c.appendChild(n)}r.appendChild(o),r.appendChild(c);const t=E.slideover({title:"Entry Details",size:"md",position:"right"});t.element.appendChild(r),t.open()}function Q(e){const r=E.slideover({title:"Import Entries",size:"md",position:"right"}),a=document.createElement("div");a.style.cssText="padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem;";const o=document.createElement("p");o.textContent='Paste a JSON array of entries. Each item should have a "data" object with field values.',o.style.cssText="font-size:.875rem;color:var(--dm-text-muted,#888);margin:0;",a.appendChild(o);const c=document.createElement("textarea");c.className="form-input",c.rows=10,c.placeholder='[{"data": {"name": "Example"}}, ...]',a.appendChild(c);const t=document.createElement("p");t.style.cssText="font-size:.875rem;margin:0;display:none;",a.appendChild(t);const n=document.createElement("div");n.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;";const i=document.createElement("button");i.className="btn btn-ghost",i.textContent="Cancel";const l=document.createElement("button");l.className="btn btn-primary",l.textContent="Import",n.appendChild(i),n.appendChild(l),a.appendChild(n),r.element.appendChild(a),r.open(),i.addEventListener("click",()=>r.close()),l.addEventListener("click",async()=>{let d;try{if(d=JSON.parse(c.value),!Array.isArray(d))throw new Error("Must be a JSON array")}catch(s){t.style.display="",t.style.color="var(--dm-danger,#f87171)",t.textContent=`Invalid JSON: ${s.message}`;return}l.disabled=!0;try{const s=await h.collections.import(y,d);t.style.display="",t.style.color="var(--dm-success,#4ade80)",t.textContent=`Imported ${s.imported} entries. Skipped: ${s.skipped}.`,await M(e||{find:()=>({get:()=>null,off:()=>({on:()=>{}})})}),setTimeout(()=>r.close(),1500)}catch(s){t.style.display="",t.style.color="var(--dm-danger,#f87171)",t.textContent=s.message||"Import failed."}finally{l.disabled=!1}})}
@@ -1,3 +1,3 @@
1
1
  import{api as C,apiRequest as v}from"../api.js";import{populateThemeSelect as w}from"../lib/themes.js";export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(e){E.tabs(e.find("#settings-tabs").get(0));const k=E.loader(e.get(0),{type:"dots"}),l=await C.settings.get().catch(()=>({}));if(k.destroy(),e.find("#field-site-title").val(l.title||""),e.find("#field-tagline").val(l.tagline||""),e.find("#field-font-family").val(l.fontFamily||"Roboto"),e.find("#field-font-size").val(l.fontSize||16),w(e.find("#field-theme").get(0)),w(e.find("#field-admin-theme").get(0),{includeAdmin:!0}),e.find("#field-theme").val(l.baseTheme||l.theme||"charcoal-dark"),l.baseTheme){e.find("#field-theme").prop("disabled",!0);const t=document.createElement("p");t.className="form-hint",t.style.cssText="margin-top:.4rem;font-size:.8rem;color:var(--dm-info);";const s=document.createElement("a");s.href="#/plugins/theme-roller",s.textContent="Theme Roller",t.appendChild(document.createTextNode(`Custom theme \u201C${l.theme}\u201D is active (based on ${l.baseTheme}). Manage via `)),t.appendChild(s),t.appendChild(document.createTextNode(".")),e.find("#field-theme").get(0).closest(".col-6").appendChild(t)}e.find("#field-admin-theme").val(l.adminTheme||"charcoal-dark");const p=l.autoTheme||{},h=!!p.enabled,b=e.find("#field-auto-theme-enabled"),y=e.find("#auto-theme-fields"),u=e.find("#field-theme"),f=u.get(0).innerHTML;e.find("#field-day-theme").html(f),e.find("#field-night-theme").html(f),l.baseTheme?(b.prop("disabled",!0),e.find("#auto-theme-roller-hint").show()):(b.prop("checked",h),h&&(y.show(),u.prop("disabled",!0)),b.on("change",function(){const t=this.checked;y.toggle(t),u.prop("disabled",t)})),e.find("#field-day-theme").val(p.dayTheme||"charcoal-light"),e.find("#field-night-theme").val(p.nightTheme||"charcoal-dark"),e.find("#field-day-start").val(p.dayStart||"07:00"),e.find("#field-night-start").val(p.nightStart||"19:00"),e.find("#field-spacer-size").val(l.layoutOptions?.spacerSize??40),e.find("#field-seo-title").val(l.seo?.defaultTitle||""),e.find("#field-seo-separator").val(l.seo?.titleSeparator||" | "),e.find("#field-seo-desc").val(l.seo?.defaultDescription||""),e.find("#field-footer-copy").val(l.footer?.copyright||""),e.find("#field-social-twitter").val(l.social?.twitter||""),e.find("#field-social-facebook").val(l.social?.facebook||""),e.find("#field-social-instagram").val(l.social?.instagram||""),e.find("#field-social-linkedin").val(l.social?.linkedin||""),e.find("#field-social-github").val(l.social?.github||""),e.find("#field-social-youtube").val(l.social?.youtube||""),e.find("#field-smtp-host").val(l.smtp?.host||""),e.find("#field-smtp-port").val(l.smtp?.port||587),e.find("#field-smtp-user").val(l.smtp?.user||""),e.find("#field-smtp-pass").val(l.smtp?.pass||""),e.find("#field-smtp-secure").prop("checked",l.smtp?.secure||!1),e.find("#field-smtp-from-address").val(l.smtp?.fromAddress||""),e.find("#field-smtp-from-name").val(l.smtp?.fromName||"");const i=l.backToTop||{};e.find("#field-btt-enabled").prop("checked",i.enabled!==!1),e.find("#field-btt-threshold").val(i.scrollThreshold??300),e.find("#field-btt-position").val(i.position||"bottom-right"),e.find("#field-btt-offset").val(i.offset??32),e.find("#field-btt-bottom-offset").val(i.bottomOffset??i.offset??32),e.find("#field-btt-label").val(i.label||""),e.find("#field-btt-smooth").prop("checked",i.smooth!==!1);const d=l.cookieConsent||{};e.find("#field-cc-enabled").prop("checked",d.enabled!==!1),e.find("#field-cc-message").val(d.message||""),e.find("#field-cc-accept-all").val(d.acceptAllText||"Accept All"),e.find("#field-cc-reject-all").val(d.rejectAllText||"Reject All"),e.find("#field-cc-customize").val(d.customizeText||"Customize"),e.find("#field-cc-save-prefs").val(d.savePreferencesText||"Save Preferences"),e.find("#field-cc-privacy-text").val(d.privacyPolicyText||"Privacy Policy"),e.find("#field-cc-privacy-url").val(d.privacyPolicyUrl||""),e.find("#field-cc-cookie-text").val(d.cookiePolicyText||"Cookie Policy"),e.find("#field-cc-cookie-url").val(d.cookiePolicyUrl||""),e.find("#field-cc-position").val(d.position||"bottom"),e.find("#field-cc-layout").val(d.layout||"bar"),e.find("#field-cc-theme").val(d.theme||"dark"),e.find("#field-cc-show-functional").prop("checked",d.showFunctional!==!1),e.find("#field-cc-show-analytics").prop("checked",d.showAnalytics!==!1),e.find("#field-cc-show-marketing").prop("checked",d.showMarketing!==!1),e.find("#field-cc-version").val(d.consentVersion||"1.0");const n=l.breadcrumbs||{};e.find("#field-breadcrumbs-enabled").prop("checked",n.enabled===!0),e.find("#field-breadcrumbs-home-label").val(n.homeLabel||"Home"),e.find("#field-breadcrumbs-position").val(n.position||"TL"),e.find("#field-bc-offset-x").val(n.offsetX??8),e.find("#field-bc-offset-y").val(n.offsetY??8);function c(t){e.find(".bc-pos-btn").each(function(){const s=this.dataset.pos===t;this.style.background=s?"var(--dm-primary, #5b8cff)":"",this.style.color=s?"var(--dm-primary-text)":""})}c(n.position||"TL"),e.find(".bc-pos-btn").on("click",function(){const t=this.dataset.pos;e.find("#field-breadcrumbs-position").val(t),c(t)});let a=null;try{const{css:t}=await v("/settings/custom-css");e.find("#field-custom-css").val(t||""),E.editor&&(a=E.editor(e.find("#field-custom-css").get(0),{mode:"code",language:"css",lineNumbers:!0,showToolbar:!1,minHeight:420,placeholder:"/* Add your custom CSS here */",characterCount:!0}))}catch{}a&&a._editorEl&&a._editorEl.addEventListener("keydown",t=>{if(!t.ctrlKey&&!t.metaKey)return;if(t.key==="s"){t.preventDefault(),e.find("#save-css-btn").get(0)?.click();return}const s=a._editorEl,m=s.selectionStart!==s.selectionEnd;if((t.key==="c"||t.key==="x")&&!m){t.preventDefault();const o=s.value,g=s.selectionStart,T=o.lastIndexOf(`
2
2
  `,g-1)+1,x=o.indexOf(`
3
- `,g),S=x===-1?o.slice(T):o.slice(T,x+1);if(navigator.clipboard.writeText(S),t.key==="x"){const z=o.slice(0,T),P=x===-1?"":o.slice(x+1);s.value=z+P,s.selectionStart=s.selectionEnd=T,s.dispatchEvent(new Event("input",{bubbles:!0})),E.toast("Line cut",{type:"info",duration:1500})}else E.toast("Line copied",{type:"info",duration:1500})}}),e.find("#save-settings-btn").on("click",async()=>{const t=e.find("#field-admin-theme").val(),s=!l.baseTheme&&e.find("#field-auto-theme-enabled").prop("checked"),m={enabled:s,dayTheme:e.find("#field-day-theme").val()||"charcoal-light",nightTheme:e.find("#field-night-theme").val()||"charcoal-dark",dayStart:e.find("#field-day-start").val()||"07:00",nightStart:e.find("#field-night-start").val()||"19:00"},o={title:e.find("#field-site-title").val().trim(),tagline:e.find("#field-tagline").val().trim(),fontFamily:e.find("#field-font-family").val()||"Roboto",fontSize:parseInt(e.find("#field-font-size").val(),10)||16,theme:l.baseTheme?l.theme:s?m.dayTheme:e.find("#field-theme").val(),...l.baseTheme?{baseTheme:l.baseTheme}:{},autoTheme:m,adminTheme:t,layoutOptions:{spacerSize:parseInt(e.find("#field-spacer-size").val(),10)||40},seo:{defaultTitle:e.find("#field-seo-title").val().trim(),titleSeparator:e.find("#field-seo-separator").val()||" | ",defaultDescription:e.find("#field-seo-desc").val().trim()},footer:{copyright:e.find("#field-footer-copy").val().trim(),links:l.footer?.links||[]},social:{twitter:e.find("#field-social-twitter").val().trim(),facebook:e.find("#field-social-facebook").val().trim(),instagram:e.find("#field-social-instagram").val().trim(),linkedin:e.find("#field-social-linkedin").val().trim(),github:e.find("#field-social-github").val().trim(),youtube:e.find("#field-social-youtube").val().trim()},smtp:{host:e.find("#field-smtp-host").val().trim(),port:parseInt(e.find("#field-smtp-port").val(),10)||587,user:e.find("#field-smtp-user").val().trim(),pass:e.find("#field-smtp-pass").val(),secure:e.find("#field-smtp-secure").prop("checked"),fromAddress:e.find("#field-smtp-from-address").val().trim(),fromName:e.find("#field-smtp-from-name").val().trim()},backToTop:{enabled:e.find("#field-btt-enabled").prop("checked"),scrollThreshold:parseInt(e.find("#field-btt-threshold").val(),10)||300,position:e.find("#field-btt-position").val()||"bottom-right",offset:parseInt(e.find("#field-btt-offset").val(),10)||32,bottomOffset:parseInt(e.find("#field-btt-bottom-offset").val(),10)||32,label:e.find("#field-btt-label").val().trim(),smooth:e.find("#field-btt-smooth").prop("checked")},cookieConsent:{enabled:e.find("#field-cc-enabled").prop("checked"),message:e.find("#field-cc-message").val().trim(),acceptAllText:e.find("#field-cc-accept-all").val().trim(),rejectAllText:e.find("#field-cc-reject-all").val().trim(),customizeText:e.find("#field-cc-customize").val().trim(),savePreferencesText:e.find("#field-cc-save-prefs").val().trim(),privacyPolicyText:e.find("#field-cc-privacy-text").val().trim(),privacyPolicyUrl:e.find("#field-cc-privacy-url").val().trim(),cookiePolicyText:e.find("#field-cc-cookie-text").val().trim(),cookiePolicyUrl:e.find("#field-cc-cookie-url").val().trim(),position:e.find("#field-cc-position").val(),layout:e.find("#field-cc-layout").val(),theme:e.find("#field-cc-theme").val(),showFunctional:e.find("#field-cc-show-functional").prop("checked"),showAnalytics:e.find("#field-cc-show-analytics").prop("checked"),showMarketing:e.find("#field-cc-show-marketing").prop("checked"),consentVersion:e.find("#field-cc-version").val().trim()||"1.0"}},g=e.find("#save-settings-btn");g.prop("disabled",!0);try{await C.settings.save(o),Domma.theme.set(t),E.toast("Settings saved.",{type:"success"})}catch{E.toast("Failed to save settings.",{type:"error"})}finally{g.prop("disabled",!1)}}),e.find("#send-test-email-btn").on("click",async()=>{const t=e.find("#field-test-email-to").val().trim(),s=e.find("#test-email-result").get(0),m=e.find("#send-test-email-btn");m.prop("disabled",!0),s&&(s.textContent="Sending\u2026",s.style.color="");try{const o=await v("/settings/test-email",{method:"POST",body:JSON.stringify({to:t||void 0})});s&&(s.textContent=o.message||"Test email sent.",s.style.color="var(--dm-success,#4ade80)")}catch(o){s&&(s.textContent=o.message||"Failed to send test email.",s.style.color="var(--dm-danger,#f87171)")}finally{m.prop("disabled",!1)}}),e.find("#save-breadcrumbs-btn").on("click",async()=>{const t=e.find("#save-breadcrumbs-btn");t.prop("disabled",!0);try{const s={enabled:e.find("#field-breadcrumbs-enabled").prop("checked"),homeLabel:e.find("#field-breadcrumbs-home-label").val().trim()||"Home",position:e.find("#field-breadcrumbs-position").val()||"TL",offsetX:parseInt(e.find("#field-bc-offset-x").val(),10)||8,offsetY:parseInt(e.find("#field-bc-offset-y").val(),10)||8};await C.settings.save({...l,breadcrumbs:s}),l.breadcrumbs=s,E.toast("Breadcrumbs settings saved.",{type:"success"})}catch{E.toast("Failed to save breadcrumbs settings.",{type:"error"})}finally{t.prop("disabled",!1)}}),e.find("#save-css-btn").on("click",async()=>{const t=a?a.getValue():e.find("#field-custom-css").val(),s=e.find("#save-css-btn");s.prop("disabled",!0);try{await v("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:t})}),E.toast("Custom CSS saved.",{type:"success"})}catch(m){E.toast(m.message||"Failed to save CSS.",{type:"error"})}finally{s.prop("disabled",!1)}});async function r(){try{const t=await v("/cache/status"),s=t.enabled?`enabled (${t.driver})`:"disabled",m=t.maxItems?` \xB7 ${t.size} / ${t.maxItems} entries`:"";e.find("#cache-status").text(s+m)}catch{e.find("#cache-status").text("unknown")}try{const{entries:t}=await v("/cache/keys");A(e.find("#cache-keys-table"),t),e.find("#cache-key-count").text(`(${t.length})`)}catch(t){e.find("#cache-keys-table").get(0).textContent="Failed to load keys: "+t.message}}await r(),e.find("#btn-clear-cache").on("click",async()=>{if(!await E.confirm("Clear the entire response cache?"))return;const t=e.find("#btn-clear-cache");t.prop("disabled",!0);try{await v("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"}),await r()}catch(s){E.toast(s.message||"Failed to clear cache.",{type:"error"})}finally{t.prop("disabled",!1)}}),e.find("#btn-refresh-cache-keys").on("click",r)}};function A(e,k){const l=e.get(0);if(!l)return;for(;l.firstChild;)l.removeChild(l.firstChild);if(!k.length){const f=document.createElement("p");f.className="text-muted",f.style.fontSize=".875rem",f.textContent="Cache is empty.",l.appendChild(f);return}const p=[...k].reverse(),h=document.createElement("table");h.className="table table-compact";const b=document.createElement("thead"),y=document.createElement("tr");for(const f of["Key","Tags","Expires"]){const i=document.createElement("th");i.textContent=f,y.appendChild(i)}b.appendChild(y),h.appendChild(b);const u=document.createElement("tbody");for(const f of p){const i=document.createElement("tr"),d=document.createElement("td");d.style.fontFamily="monospace",d.style.fontSize="12px",d.textContent=f.key,i.appendChild(d);const n=document.createElement("td");n.style.fontSize="12px";for(const a of f.tags){const r=document.createElement("span");r.className="badge",r.style.marginRight="4px",r.style.fontSize="11px",r.textContent=a,n.appendChild(r)}i.appendChild(n);const c=document.createElement("td");if(c.style.fontSize="12px",c.style.color="var(--dm-text-muted, #6b7280)",f.expiresAt===null)c.textContent="no expiry";else{const a=f.expiresAt-Date.now();a<=0?c.textContent="expired":a<6e4?c.textContent=`${Math.round(a/1e3)}s`:a<36e5?c.textContent=`${Math.round(a/6e4)}m`:c.textContent=`${Math.round(a/36e5)}h`}i.appendChild(c),u.appendChild(i)}h.appendChild(u),l.appendChild(h)}
3
+ `,g),S=x===-1?o.slice(T):o.slice(T,x+1);if(navigator.clipboard.writeText(S),t.key==="x"){const z=o.slice(0,T),P=x===-1?"":o.slice(x+1);s.value=z+P,s.selectionStart=s.selectionEnd=T,s.dispatchEvent(new Event("input",{bubbles:!0})),E.toast("Line cut",{type:"info",duration:1500})}else E.toast("Line copied",{type:"info",duration:1500})}}),e.find("#save-settings-btn").on("click",async()=>{const t=e.find("#field-admin-theme").val(),s=!l.baseTheme&&e.find("#field-auto-theme-enabled").prop("checked"),m={enabled:s,dayTheme:e.find("#field-day-theme").val()||"charcoal-light",nightTheme:e.find("#field-night-theme").val()||"charcoal-dark",dayStart:e.find("#field-day-start").val()||"07:00",nightStart:e.find("#field-night-start").val()||"19:00"},o={title:e.find("#field-site-title").val().trim(),tagline:e.find("#field-tagline").val().trim(),fontFamily:e.find("#field-font-family").val()||"Roboto",fontSize:parseInt(e.find("#field-font-size").val(),10)||16,theme:l.baseTheme?l.theme:s?m.dayTheme:e.find("#field-theme").val(),...l.baseTheme?{baseTheme:l.baseTheme}:{},autoTheme:m,adminTheme:t,layoutOptions:{spacerSize:parseInt(e.find("#field-spacer-size").val(),10)||40},seo:{defaultTitle:e.find("#field-seo-title").val().trim(),titleSeparator:e.find("#field-seo-separator").val()||" | ",defaultDescription:e.find("#field-seo-desc").val().trim()},footer:{copyright:e.find("#field-footer-copy").val().trim(),links:l.footer?.links||[]},social:{twitter:e.find("#field-social-twitter").val().trim(),facebook:e.find("#field-social-facebook").val().trim(),instagram:e.find("#field-social-instagram").val().trim(),linkedin:e.find("#field-social-linkedin").val().trim(),github:e.find("#field-social-github").val().trim(),youtube:e.find("#field-social-youtube").val().trim()},smtp:{host:e.find("#field-smtp-host").val().trim(),port:parseInt(e.find("#field-smtp-port").val(),10)||587,user:e.find("#field-smtp-user").val().trim(),pass:e.find("#field-smtp-pass").val(),secure:e.find("#field-smtp-secure").prop("checked"),fromAddress:e.find("#field-smtp-from-address").val().trim(),fromName:e.find("#field-smtp-from-name").val().trim()},backToTop:{enabled:e.find("#field-btt-enabled").prop("checked"),scrollThreshold:parseInt(e.find("#field-btt-threshold").val(),10)||300,position:e.find("#field-btt-position").val()||"bottom-right",offset:parseInt(e.find("#field-btt-offset").val(),10)||32,bottomOffset:parseInt(e.find("#field-btt-bottom-offset").val(),10)||32,label:e.find("#field-btt-label").val().trim(),smooth:e.find("#field-btt-smooth").prop("checked")},cookieConsent:{enabled:e.find("#field-cc-enabled").prop("checked"),message:e.find("#field-cc-message").val().trim(),acceptAllText:e.find("#field-cc-accept-all").val().trim(),rejectAllText:e.find("#field-cc-reject-all").val().trim(),customizeText:e.find("#field-cc-customize").val().trim(),savePreferencesText:e.find("#field-cc-save-prefs").val().trim(),privacyPolicyText:e.find("#field-cc-privacy-text").val().trim(),privacyPolicyUrl:e.find("#field-cc-privacy-url").val().trim(),cookiePolicyText:e.find("#field-cc-cookie-text").val().trim(),cookiePolicyUrl:e.find("#field-cc-cookie-url").val().trim(),position:e.find("#field-cc-position").val(),layout:e.find("#field-cc-layout").val(),theme:e.find("#field-cc-theme").val(),showFunctional:e.find("#field-cc-show-functional").prop("checked"),showAnalytics:e.find("#field-cc-show-analytics").prop("checked"),showMarketing:e.find("#field-cc-show-marketing").prop("checked"),consentVersion:e.find("#field-cc-version").val().trim()||"1.0"}},g=e.find("#save-settings-btn");g.prop("disabled",!0);try{await C.settings.save(o),Domma.theme.set(t),E.toast("Settings saved.",{type:"success"})}catch{E.toast("Failed to save settings.",{type:"error"})}finally{g.prop("disabled",!1)}}),e.find("#send-test-email-btn").on("click",async()=>{const t=e.find("#field-test-email-to").val().trim(),s=e.find("#test-email-result").get(0),m=e.find("#send-test-email-btn");m.prop("disabled",!0),s&&(s.textContent="Sending\u2026",s.style.color="");try{const o=await v("/settings/test-email",{method:"POST",body:JSON.stringify({to:t||void 0})});s&&(s.textContent=o.message||"Test email sent.",s.style.color="var(--dm-success,#4ade80)")}catch(o){s&&(s.textContent=o.message||"Failed to send test email.",s.style.color="var(--dm-danger,#f87171)")}finally{m.prop("disabled",!1)}}),e.find("#save-breadcrumbs-btn").on("click",async()=>{const t=e.find("#save-breadcrumbs-btn");t.prop("disabled",!0);try{const s={enabled:e.find("#field-breadcrumbs-enabled").prop("checked"),homeLabel:e.find("#field-breadcrumbs-home-label").val().trim()||"Home",position:e.find("#field-breadcrumbs-position").val()||"TL",offsetX:parseInt(e.find("#field-bc-offset-x").val(),10)||8,offsetY:parseInt(e.find("#field-bc-offset-y").val(),10)||8};await C.settings.save({breadcrumbs:s}),l.breadcrumbs=s,E.toast("Breadcrumbs settings saved.",{type:"success"})}catch{E.toast("Failed to save breadcrumbs settings.",{type:"error"})}finally{t.prop("disabled",!1)}}),e.find("#save-css-btn").on("click",async()=>{const t=a?a.getValue():e.find("#field-custom-css").val(),s=e.find("#save-css-btn");s.prop("disabled",!0);try{await v("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:t})}),E.toast("Custom CSS saved.",{type:"success"})}catch(m){E.toast(m.message||"Failed to save CSS.",{type:"error"})}finally{s.prop("disabled",!1)}});async function r(){try{const t=await v("/cache/status"),s=t.enabled?`enabled (${t.driver})`:"disabled",m=t.maxItems?` \xB7 ${t.size} / ${t.maxItems} entries`:"";e.find("#cache-status").text(s+m)}catch{e.find("#cache-status").text("unknown")}try{const{entries:t}=await v("/cache/keys");A(e.find("#cache-keys-table"),t),e.find("#cache-key-count").text(`(${t.length})`)}catch(t){e.find("#cache-keys-table").get(0).textContent="Failed to load keys: "+t.message}}await r(),e.find("#btn-clear-cache").on("click",async()=>{if(!await E.confirm("Clear the entire response cache?"))return;const t=e.find("#btn-clear-cache");t.prop("disabled",!0);try{await v("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"}),await r()}catch(s){E.toast(s.message||"Failed to clear cache.",{type:"error"})}finally{t.prop("disabled",!1)}}),e.find("#btn-refresh-cache-keys").on("click",r)}};function A(e,k){const l=e.get(0);if(!l)return;for(;l.firstChild;)l.removeChild(l.firstChild);if(!k.length){const f=document.createElement("p");f.className="text-muted",f.style.fontSize=".875rem",f.textContent="Cache is empty.",l.appendChild(f);return}const p=[...k].reverse(),h=document.createElement("table");h.className="table table-compact";const b=document.createElement("thead"),y=document.createElement("tr");for(const f of["Key","Tags","Expires"]){const i=document.createElement("th");i.textContent=f,y.appendChild(i)}b.appendChild(y),h.appendChild(b);const u=document.createElement("tbody");for(const f of p){const i=document.createElement("tr"),d=document.createElement("td");d.style.fontFamily="monospace",d.style.fontSize="12px",d.textContent=f.key,i.appendChild(d);const n=document.createElement("td");n.style.fontSize="12px";for(const a of f.tags){const r=document.createElement("span");r.className="badge",r.style.marginRight="4px",r.style.fontSize="11px",r.textContent=a,n.appendChild(r)}i.appendChild(n);const c=document.createElement("td");if(c.style.fontSize="12px",c.style.color="var(--dm-text-muted, #6b7280)",f.expiresAt===null)c.textContent="no expiry";else{const a=f.expiresAt-Date.now();a<=0?c.textContent="expired":a<6e4?c.textContent=`${Math.round(a/1e3)}s`:a<36e5?c.textContent=`${Math.round(a/6e4)}m`:c.textContent=`${Math.round(a/36e5)}h`}i.appendChild(c),u.appendChild(i)}h.appendChild(u),l.appendChild(h)}
@@ -1 +1 @@
1
- import{api as a}from"../api.js";function q(n){return{string:"string",email:"email",tel:"string",number:"number",textarea:"textarea",select:"select",radio:"select",checkbox:"boolean","checkbox-group":"select",date:"string",time:"string",url:"string",hidden:"string"}[n]||"string"}export const userEditorView={templateUrl:"/admin/js/templates/user-editor.html",async onMount(n){const g=window.location.hash.match(/#\/users\/edit\/([^/]+)/),u=g?g[1]:null,r=!!u,k=E.loader(n.get(0),{type:"dots"});let o,m,y,w;try{[o,m,y,w]=await Promise.all([r?a.users.get(u).catch(()=>null):Promise.resolve(null),a.collections.listEntries("roles",{limit:100}).catch(()=>null),a.collections.get("user-profiles").catch(()=>null),a.projects.list().catch(()=>[])])}finally{k.destroy()}if(r&&!o){E.toast("User not found.",{type:"error"}),R.navigate("/users");return}n.find("#editor-title").text(r?"Edit User":"New User"),n.find("#cancel-btn").on("click",()=>R.navigate("/users"));const v=(m?.entries||[]).map(e=>({value:e.data.name,label:e.data.label})).sort((e,f)=>{const t=c=>m.entries.find(l=>l.data.name===c)?.data?.level??99;return t(e.value)-t(f.value)}),C=[{value:"admin",label:"Admin"},{value:"manager",label:"Manager"},{value:"editor",label:"Editor"},{value:"subscriber",label:"Subscriber"}],j=v.length>0?v:C,A=(w||[]).map(e=>({value:e.slug,label:e.name||e.slug})),O=[{value:"",label:"(none)"},...A],S={name:{type:"string",required:!0,minLength:2,label:"Full Name",formConfig:{placeholder:"Jane Smith"}},email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"jane@example.com"}},role:{type:"select",required:!0,label:"Primary role",options:j,formConfig:{tooltip:"Shown as the user's badge. Used for hierarchy comparisons."}},additionalRoles:{type:"multiselect",label:"Additional roles",options:j,formConfig:{placeholder:"",tooltip:"Optional \u2014 extra roles whose permissions also apply. Useful for someone who is both, say, a recruiter AND a content editor."}},ownedByProject:{type:"select",label:"Owned by project",options:O,formConfig:{tooltip:"Administrative tag \u2014 controls where this user appears in the sidebar Projects section. Mapped to meta.project."}},projects:{type:"multiselect",label:"Project access",options:A,formConfig:{placeholder:"",tooltip:"Access scope \u2014 limits which artefacts the user can see and edit. Leave empty for no restriction."}},isActive:{type:"boolean",label:"Active account"},password:{type:"password",required:!r,minLength:r?0:8,label:r?"New Password":"Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password",tooltip:r?"Leave blank to keep current password":"Minimum 8 characters"}}},B=o?{name:o.name||"",email:o.email||"",role:o.role||"editor",additionalRoles:Array.isArray(o.additionalRoles)?o.additionalRoles:[],ownedByProject:o.meta?.project||"",projects:Array.isArray(o.projects)?o.projects:[],isActive:o.isActive!==!1}:{role:"editor",additionalRoles:[],ownedByProject:"",projects:[],isActive:!0};F.render("#user-form-container",S,B,{layout:"stacked",submitText:r?"Update User":"Create User",onSubmit:async e=>{if(r&&e.password&&e.password.length<8)return E.toast("Password must be at least 8 characters.",{type:"warning"}),!1;const t=(Array.isArray(e.additionalRoles)?e.additionalRoles:[]).filter(s=>s&&s!==e.role).filter((s,d,i)=>i.indexOf(s)===d),l=(Array.isArray(e.projects)?e.projects:[]).filter(Boolean).filter((s,d,i)=>i.indexOf(s)===d),p=e.ownedByProject||null,h={name:e.name,email:e.email,role:e.role,additionalRoles:t,projects:l,meta:{...o?.meta||{},project:p},isActive:!!e.isActive};e.password&&(h.password=e.password);try{let s;r?s=await a.users.update(u,h):s=await a.users.create(h);const d=y?.fields||[];if(d.length>0){const i={};for(const b of d)b.name in e&&(i[b.name]=e[b.name]);const U=r?u:s?.id;U&&Object.keys(i).length>0&&await a.users.update(U,{profile:i})}E.toast("User saved successfully.",{type:"success"}),R.navigate("/users")}catch(s){return E.toast(`Save failed: ${s.message||"Unknown error"}`,{type:"error"}),!1}}});const P=y?.fields||[];if(P.length>0){n.find("#profile-card").show();const e={},f={};for(const t of P){const l={type:q(t.type),label:t.label||t.name};t.required&&(l.required=!0),t.placeholder&&(l.formConfig={placeholder:t.placeholder}),t.options&&Array.isArray(t.options)&&(l.options=t.options.map(p=>typeof p=="string"?{value:p,label:p}:p)),e[t.name]=l,f[t.name]=o?.profile?.[t.name]??""}F.render("#profile-form-container",e,f,{layout:"stacked",submitText:r?"Update Profile":"Save Profile",onSubmit:async t=>{const c=u;if(!c)return E.toast("Save the user first, then update the profile.",{type:"info"}),!1;try{await a.users.update(c,{profile:t}),E.toast("Profile saved.",{type:"success"})}catch(l){return E.toast(`Profile save failed: ${l.message||"Unknown error"}`,{type:"error"}),!1}}})}Domma.icons.scan()}};
1
+ import{api as a,getUser as N}from"../api.js";function I(c){return{string:"string",email:"email",tel:"string",number:"number",textarea:"textarea",select:"select",radio:"select",checkbox:"boolean","checkbox-group":"select",date:"string",time:"string",url:"string",hidden:"string"}[c]||"string"}export const userEditorView={templateUrl:"/admin/js/templates/user-editor.html",async onMount(c){const b=window.location.hash.match(/#\/users\/edit\/([^/]+)/),u=b?b[1]:null,r=!!u,C=E.loader(c.get(0),{type:"dots"});let o,m,y,v;try{[o,m,y,v]=await Promise.all([r?a.users.get(u).catch(()=>null):Promise.resolve(null),a.collections.listEntries("roles",{limit:100}).catch(()=>null),a.collections.get("user-profiles").catch(()=>null),a.projects.list().catch(()=>[])])}finally{C.destroy()}if(r&&!o){E.toast("User not found.",{type:"error"}),R.navigate("/users");return}c.find("#editor-title").text(r?"Edit User":"New User"),c.find("#cancel-btn").on("click",()=>R.navigate("/users"));const O=N(),h=e=>(m?.entries||[]).find(i=>i.data.name===e)?.data?.level??99,B=h(O?.role),j=(m?.entries||[]).filter(e=>(e.data.level??99)>B||r&&e.data.name===o?.role).map(e=>({value:e.data.name,label:e.data.label})).sort((e,i)=>h(e.value)-h(i.value)),S=[{value:"admin",label:"Admin"},{value:"user",label:"User"}],A=j.length>0?j:S,P=(v||[]).map(e=>({value:e.slug,label:e.name||e.slug})),q=[{value:"",label:"(none)"},...P],L={name:{type:"string",required:!0,minLength:2,label:"Full Name",formConfig:{placeholder:"Jane Smith"}},email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"jane@example.com"}},role:{type:"select",required:!0,label:"Primary role",options:A,formConfig:{tooltip:"Shown as the user's badge. Used for hierarchy comparisons."}},additionalRoles:{type:"multiselect",label:"Additional roles",options:A,formConfig:{placeholder:"",tooltip:"Optional \u2014 extra roles whose permissions also apply. Useful for someone who is both, say, a recruiter AND a content editor."}},ownedByProject:{type:"select",label:"Owned by project",options:q,formConfig:{tooltip:"Administrative tag \u2014 controls where this user appears in the sidebar Projects section. Mapped to meta.project."}},projects:{type:"multiselect",label:"Project access",options:P,formConfig:{placeholder:"",tooltip:"Access scope \u2014 limits which artefacts the user can see and edit. Leave empty for no restriction."}},isActive:{type:"boolean",label:"Active account"},password:{type:"password",required:!r,minLength:r?0:8,label:r?"New Password":"Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password",tooltip:r?"Leave blank to keep current password":"Minimum 8 characters"}}},T=o?{name:o.name||"",email:o.email||"",role:o.role||"user",additionalRoles:Array.isArray(o.additionalRoles)?o.additionalRoles:[],ownedByProject:o.meta?.project||"",projects:Array.isArray(o.projects)?o.projects:[],isActive:o.isActive!==!1}:{role:"user",additionalRoles:[],ownedByProject:"",projects:[],isActive:!0};F.render("#user-form-container",L,T,{layout:"stacked",submitText:r?"Update User":"Create User",onSubmit:async e=>{if(r&&e.password&&e.password.length<8)return E.toast("Password must be at least 8 characters.",{type:"warning"}),!1;const t=(Array.isArray(e.additionalRoles)?e.additionalRoles:[]).filter(s=>s&&s!==e.role).filter((s,d,n)=>n.indexOf(s)===d),l=(Array.isArray(e.projects)?e.projects:[]).filter(Boolean).filter((s,d,n)=>n.indexOf(s)===d),p=e.ownedByProject||null,g={name:e.name,email:e.email,role:e.role,additionalRoles:t,projects:l,meta:{...o?.meta||{},project:p},isActive:!!e.isActive};e.password&&(g.password=e.password);try{let s;r?s=await a.users.update(u,g):s=await a.users.create(g);const d=y?.fields||[];if(d.length>0){const n={};for(const w of d)w.name in e&&(n[w.name]=e[w.name]);const x=r?u:s?.id;x&&Object.keys(n).length>0&&await a.users.update(x,{profile:n})}E.toast("User saved successfully.",{type:"success"}),R.navigate("/users")}catch(s){return E.toast(`Save failed: ${s.message||"Unknown error"}`,{type:"error"}),!1}}});const U=y?.fields||[];if(U.length>0){c.find("#profile-card").show();const e={},i={};for(const t of U){const l={type:I(t.type),label:t.label||t.name};t.required&&(l.required=!0),t.placeholder&&(l.formConfig={placeholder:t.placeholder}),t.options&&Array.isArray(t.options)&&(l.options=t.options.map(p=>typeof p=="string"?{value:p,label:p}:p)),e[t.name]=l,i[t.name]=o?.profile?.[t.name]??""}F.render("#profile-form-container",e,i,{layout:"stacked",submitText:r?"Update Profile":"Save Profile",onSubmit:async t=>{const f=u;if(!f)return E.toast("Save the user first, then update the profile.",{type:"info"}),!1;try{await a.users.update(f,{profile:t}),E.toast("Profile saved.",{type:"success"})}catch(l){return E.toast(`Profile save failed: ${l.message||"Unknown error"}`,{type:"error"}),!1}}})}Domma.icons.scan()}};
@@ -1,4 +1,4 @@
1
- import{api as d,getUser as A}from"../api.js";import{filterByProject as k,getProjectFromHash as M}from"../lib/project-context.js";import{keepTableIconsAlive as j}from"../lib/table-icons.js";export const usersView={templateUrl:"/admin/js/templates/users.html",async onMount(o){const m=A(),f=E.loader(o.get(0),{type:"dots"});let[n,y]=await Promise.all([d.users.list().catch(()=>[]),d.get("/collections/roles/entries?limit=100").catch(()=>({entries:[]}))]);f.destroy();const p=M();p&&(n=k(n,p));const c=y.entries||[],u=r=>{T.create("#users-table",{data:r,columns:[{key:"name",title:"Name"},{key:"email",title:"Email"},{key:"role",title:"Role",render:(e,t)=>{const a=c.find(s=>s.data?.name===e),i=a?.data?.badgeClass||"badge-secondary",b=a?.data?.label||e,g=`<span class="badge ${i}">${b}</span>`,l=Array.isArray(t.additionalRoles)?t.additionalRoles.filter(s=>s&&s!==e).map(s=>`<span class="badge badge-outline" title="Additional role" style="margin-left:.25rem;">${c.find(h=>h.data?.name===s)?.data?.label||s}</span>`).join(""):"";return g+l}},{key:"isActive",title:"Status",render:e=>e?'<span class="badge badge-success">Active</span>':'<span class="badge badge-secondary">Inactive</span>'},{key:"lastLogin",title:"Last login",render:e=>e?D(e).format("DD MMM YYYY HH:mm"):"Never"},{key:"actions",title:"Actions",render:(e,t)=>{const a=t.id===m?.id,i=Object.fromEntries(c.map(l=>[l.data?.name,l.data?.level??99]));return(i[m?.role]??99)<(i[t.role]??99)?`
1
+ import{api as d,getUser as A}from"../api.js";import{filterByProject as k,getProjectFromHash as M}from"../lib/project-context.js";import{keepTableIconsAlive as j}from"../lib/table-icons.js";export const usersView={templateUrl:"/admin/js/templates/users.html",async onMount(o){const m=A(),f=E.loader(o.get(0),{type:"dots"});let[n,y]=await Promise.all([d.users.list().catch(()=>[]),d.get("/collections/roles/entries?limit=100").catch(()=>({entries:[]}))]);f.destroy();const p=M();p&&(n=k(n,p));const c=y.entries||[],u=r=>{T.create("#users-table",{data:r,columns:[{key:"name",title:"Name"},{key:"email",title:"Email"},{key:"role",title:"Role",render:(e,t)=>{const a=c.find(s=>s.data?.name===e),i=a?.data?.badgeClass||"badge-secondary",b=a?.data?.label||e,g=`<span class="badge ${i}">${b}</span>`,l=Array.isArray(t.additionalRoles)?t.additionalRoles.filter(s=>s&&s!==e).map(s=>`<span class="badge badge-outline" title="Additional role" style="margin-left:.25rem;">${c.find(h=>h.data?.name===s)?.data?.label||s}</span>`).join(""):"";return g+l}},{key:"isActive",title:"Status",render:e=>e?'<span class="badge badge-success">Active</span>':'<span class="badge badge-secondary">Inactive</span>'},{key:"lastLogin",title:"Last login",render:e=>e?D(e).format("DD MMM YYYY HH:mm"):"Never"},{key:"actions",title:"Actions",render:(e,t)=>{const a=t.id===m?.id,i=Object.fromEntries(c.map(l=>[l.data?.name,l.data?.level??99]));return!((i[m?.role]??99)<(i[t.role]??99))&&!a?"":`
2
2
  <a href="#/users/edit/${t.id}" class="btn btn-sm btn-ghost" data-tooltip="Edit"><span data-icon="edit" data-icon-size="14"></span></a>
3
3
  ${a?"":`<button class="btn btn-sm btn-ghost btn-delete-user" data-id="${t.id}" data-name="${v(t.name)}" data-tooltip="Delete"><span data-icon="trash" data-icon-size="14"></span></button>`}
4
- `:""}}],emptyMessage:"No users found."}),Domma.icons.scan(),document.querySelectorAll("#users-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})};u(n),j(o.get(0)),o.off("click",".btn-delete-user").on("click",".btn-delete-user",async function(){const r=$(this).data("id"),e=$(this).data("name");if(await E.confirm(`Delete user <strong>${e}</strong>? This cannot be undone.`))try{await d.delete(`/users/${r}`),E.toast("User deleted.",{type:"success"}),n=n.filter(a=>a.id!==r),u(n)}catch(a){E.toast(`Failed to delete: ${a.message}`,{type:"error"})}})}};function v(o){return String(o).replace(/"/g,"&quot;")}
4
+ `}}],emptyMessage:"No users found."}),Domma.icons.scan(),document.querySelectorAll("#users-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})})};u(n),j(o.get(0)),o.off("click",".btn-delete-user").on("click",".btn-delete-user",async function(){const r=$(this).data("id"),e=$(this).data("name");if(await E.confirm(`Delete user <strong>${e}</strong>? This cannot be undone.`))try{await d.delete(`/users/${r}`),E.toast("User deleted.",{type:"success"}),n=n.filter(a=>a.id!==r),u(n)}catch(a){E.toast(`Failed to delete: ${a.message}`,{type:"error"})}})}};function v(o){return String(o).replace(/"/g,"&quot;")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -0,0 +1,2 @@
1
+ (function(){"use strict";const _=document.createElement("style");_.textContent=["[data-ctx], [data-collection-table] { cursor: context-menu; }","[data-ctx] a, [data-ctx] button, [data-collection-table] a, [data-collection-table] button,","[data-ctx] input, [data-collection-table] input { cursor: pointer; }",'[data-ctx] input[type="text"], [data-collection-table] input[type="text"] { cursor: text; }',".dm-ctx-hidden { display: none !important; }",".dm-ctx-menu { position: fixed; z-index: 10000; min-width: 200px; max-width: 300px; max-height: 60vh; overflow-y: auto;background: var(--dm-surface, #1c1c30); border: 1px solid var(--dm-border, #2a2a40); border-radius: 6px;box-shadow: var(--dm-shadow-lg, 0 8px 24px rgba(0,0,0,.4)); padding: .3rem; font-size: .83rem;color: var(--dm-text, #e5e5e5); }",".dm-ctx-menu button.dm-ctx-row { display: flex; align-items: center; gap: .5rem; width: 100%; text-align: left;background: none; border: none; color: inherit; font: inherit; padding: .35rem .6rem; border-radius: 4px; cursor: pointer; }",".dm-ctx-menu button.dm-ctx-row:hover { background: var(--dm-surface-2, rgba(255,255,255,.06)); }",".dm-ctx-heading { padding: .3rem .6rem; font-size: .72rem; text-transform: uppercase; letter-spacing: .04em;color: var(--dm-text-muted, #9a9aac); }",".dm-ctx-input { width: 100%; font-size: .8rem; padding: .3rem .5rem; border-radius: 4px;border: 1px solid var(--dm-border, #2a2a40); background: var(--dm-surface-2, rgba(255,255,255,.04)); color: inherit; cursor: text; }",".dm-ctx-chips { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; margin: 0 0 .6rem; }",".dm-ctx-chip { display: inline-flex; align-items: center; gap: .35rem; font-size: .76rem; padding: .2rem .6rem;border-radius: 999px; background: var(--dm-surface-2, rgba(255,255,255,.08)); border: 1px solid var(--dm-border, #2a2a40); }",".dm-ctx-chip button { background: none; border: none; cursor: pointer; color: inherit; font-size: .9rem; line-height: 1; padding: 0; }",".dm-ctx-empty-note { padding: .75rem; font-size: .85rem; color: var(--dm-text-muted, #9a9aac); }"].join(`
2
+ `),document.head.appendChild(_);function v(t){if(t._dmTable)return{fields:t._dmTable.columns.filter(n=>n.key!=="_cta").map(n=>({name:n.key,label:n.title||n.key})),entries:t._dmTable.rows.map((n,e)=>({id:String(e),data:n}))};if(t._dmCtxModel)return t._dmCtxModel;try{t._dmCtxModel=JSON.parse(atob(t.dataset.ctx))}catch{t._dmCtxModel=null}return t._dmCtxModel}function M(t,n){if(n.field==="*"){const d=String(n.value).toLowerCase();return Object.values(t.data||{}).some(o=>String(o??"").toLowerCase().includes(d))}const e=String(t.data?.[n.field]??"");switch(n.op){case"eq":return e===String(n.value);case"ne":return e!==String(n.value);case"contains":return e.toLowerCase().includes(String(n.value).toLowerCase());case"gt":case"lt":{const d=parseFloat(e),o=parseFloat(n.value);if(!Number.isNaN(d)&&!Number.isNaN(o))return n.op==="gt"?d>o:d<o;const l=Date.parse(e),a=Date.parse(n.value);return!Number.isNaN(l)&&!Number.isNaN(a)?n.op==="gt"?l>a:l<a:n.op==="gt"?e>String(n.value):e<String(n.value)}default:return!0}}function g(t){const n=t._dmCtxFilters||[],e=v(t);if(!e)return;const d=e.entries.filter(l=>n.every(a=>M(l,a))),o=new Set(d.map(l=>l.id));t._dmTable?t._dmTable.render(d.map(l=>l.data)):t._dmCtx&&t._dmCtx.instance&&t._dmCtx.instance.setItems?t._dmCtx.instance.setItems(t._dmCtx.items.filter(l=>o.has(String(l._entryId)))):t._dmCtxSlides?$(t,o):t.querySelectorAll("[data-entry-id]").forEach(l=>{l.classList.toggle("dm-ctx-hidden",!o.has(l.dataset.entryId))}),T(t,n),k(t,d.length,n.length)}function $(t,n){if(t._dmCarousel&&typeof t._dmCarousel.destroy=="function")try{t._dmCarousel.destroy()}catch{}const e=document.createElement("div");e.className="carousel-track",t._dmCtxSlides.forEach(d=>{n.has(d.dataset.entryId)&&e.appendChild(d.cloneNode(!0))}),t.replaceChildren(e),e.children.length&&typeof Domma<"u"&&Domma.elements?.carousel&&(t._dmCarousel=Domma.elements.carousel(t,t._dmCarouselOpts||{}))}const D={eq:"=",ne:"\u2260",contains:"contains",gt:">",lt:"<"};function T(t,n){let e=t._dmCtxBar;if(!n.length){e&&(e.remove(),t._dmCtxBar=null);return}e||(e=document.createElement("div"),e.className="dm-ctx-chips",t.parentNode.insertBefore(e,t),t._dmCtxBar=e),e.replaceChildren(),n.forEach((o,l)=>{const a=document.createElement("span");a.className="dm-ctx-chip";const s=String(o.value),m=s.length>24?s.slice(0,24)+"\u2026":s;a.appendChild(document.createTextNode(o.field==="*"?`search \u201C${m}\u201D`:`${o.label||o.field} ${D[o.op]||o.op} \u201C${m}\u201D`));const r=document.createElement("button");r.type="button",r.textContent="\xD7",r.title="Remove filter",r.addEventListener("click",()=>{t._dmCtxFilters.splice(l,1),g(t)}),a.appendChild(r),e.appendChild(a)});const d=document.createElement("button");d.type="button",d.className="dm-ctx-chip",d.style.cursor="pointer",d.textContent="Clear all",d.addEventListener("click",()=>{t._dmCtxFilters=[],g(t)}),e.appendChild(d)}function k(t,n,e){if(t._dmCtxEmptyNote&&(t._dmCtxEmptyNote.remove(),t._dmCtxEmptyNote=null),n||!e)return;const d=document.createElement("div");d.className="dm-ctx-empty-note",d.textContent="No entries match the current filters.",t.appendChild(d),t._dmCtxEmptyNote=d}let u=null;function x(){u&&(u.remove(),u=null,document.removeEventListener("mousedown",E,!0),document.removeEventListener("keydown",N,!0),window.removeEventListener("scroll",S,!0))}const E=t=>{u&&!u.contains(t.target)&&x()},N=t=>{t.key==="Escape"&&x()},S=t=>{u&&!u.contains(t.target)&&x()};function f(t,n){const e=document.createElement("button");return e.type="button",e.className="dm-ctx-row",e.textContent=t,e.addEventListener("click",n),e}function L(t){const n=document.createElement("div");return n.className="dm-ctx-heading",n.textContent=t,n}const h=(t,n=24)=>{const e=String(t);return e.length>n?e.slice(0,n)+"\u2026":e};function z(t,n,e,d,o){x();const l=v(e);if(!l)return;const a=document.createElement("div");a.className="dm-ctx-menu",u=a;const s=(i,c,y,p)=>{(e._dmCtxFilters=e._dmCtxFilters||[]).push({field:i,label:c,op:y,value:p}),g(e),x()};if(o&&d){const i=d.data?.[o.name]??"";a.appendChild(L(o.label||o.name)),a.appendChild(f(`Filter: = \u201C${h(i)}\u201D`,()=>s(o.name,o.label,"eq",i))),a.appendChild(f(`Filter: \u2260 \u201C${h(i)}\u201D`,()=>s(o.name,o.label,"ne",i)));const c=String(i);(c!==""&&!Number.isNaN(parseFloat(c))||!Number.isNaN(Date.parse(c)))&&(a.appendChild(f(`Filter: > \u201C${h(i)}\u201D`,()=>s(o.name,o.label,"gt",i))),a.appendChild(f(`Filter: < \u201C${h(i)}\u201D`,()=>s(o.name,o.label,"lt",i))))}else if(d){a.appendChild(L("Filter by"));for(const i of l.fields){const c=d.data?.[i.name];c==null||String(c)===""||a.appendChild(f(`${i.label||i.name}: \u201C${h(c,20)}\u201D`,()=>s(i.name,i.label,"eq",c)))}}const m=document.createElement("div");m.style.padding=".35rem .6rem";const r=document.createElement("input");r.type="text",r.className="dm-ctx-input",r.placeholder="Search\u2026",r.addEventListener("mousedown",i=>i.stopPropagation()),r.addEventListener("keydown",i=>{i.key==="Enter"&&r.value.trim()&&s("*","search","contains",r.value.trim())}),m.appendChild(r),a.appendChild(m);const b=e._dmCtxFilters||[];if(b.length){const i=document.createElement("div");i.style.cssText="border-top:1px solid var(--dm-border,#2a2a40);margin:.25rem 0;",a.appendChild(i),a.appendChild(f(`Clear all filters (${b.length})`,()=>{e._dmCtxFilters=[],g(e),x()}))}document.body.appendChild(a);const C=a.getBoundingClientRect();a.style.left=Math.min(t,window.innerWidth-C.width-8)+"px",a.style.top=Math.min(n,window.innerHeight-C.height-8)+"px",document.addEventListener("mousedown",E,!0),document.addEventListener("keydown",N,!0),window.addEventListener("scroll",S,!0),setTimeout(()=>r.focus(),0)}function I(t,n,e){if(t._dmTable){const s=n.closest("tbody tr");if(!s)return{entry:null,field:null};const m=n.closest("td"),r=s.querySelectorAll("td"),b=Array.from(r).map(c=>c.textContent.trim()),C=e.entries.find(c=>e.fields.every((y,p)=>String(c.data?.[y.name]??"").trim()===(b[p]??"").trim()))||null,i=m&&e.fields[m.cellIndex]||null;return{entry:C,field:i}}const d=n.closest("[data-entry-id]");if(!d)return{entry:null,field:null};const o=e.entries.find(s=>s.id===d.dataset.entryId)||null;if(!o)return{entry:null,field:null};const l=(n.textContent||"").trim().slice(0,80);let a=null;if(l){const s=e.fields.filter(m=>{const r=String(o.data?.[m.name]??"");return r&&(r.includes(l)||l.includes(r))});s.length===1&&(a=s[0])}return{entry:o,field:a}}document.addEventListener("contextmenu",t=>{const n=t.target.closest("[data-ctx], [data-collection-table]");if(!n)return;const e=v(n);if(!e||!e.fields?.length)return;t.preventDefault();const{entry:d,field:o}=I(n,t.target,e);z(t.clientX,t.clientY,n,d,o)})})();
package/public/js/site.js CHANGED
@@ -1 +1 @@
1
- import{makeBadgeEl as _,applyColour as A,applyPill as I}from"/public/js/menu-decor.mjs";import{enhancePickerFields as D}from"/public/js/forms.js?v=20260722-pickers";$(()=>{const C=window.__CMS_NAV__||{},y=window.__CMS_SITE__||{};if(y.autoTheme?.enabled){let o=function(e){const u=(e||"07:00").split(":");return+u[0]*60+(+u[1]||0)},a=function(){const e=new Date,u=e.getHours()*60+e.getMinutes();return u>=o(t.dayStart)&&u<o(t.nightStart)?t.dayTheme:t.nightTheme};const t=y.autoTheme;Domma.theme.set(a()),setInterval(()=>Domma.theme.set(a()),6e4)}const b=$("#site-navbar");if(b.length&&C.brand){let a=function(i,n=0){return Array.isArray(i)?i.filter(c=>!(c&&c.type==="separator"&&n===0)).map(c=>c&&c.type==="separator"?{divider:!0}:Array.isArray(c?.items)?{...c,items:a(c.items,n+1)}:c):i},e=function(i){return Array.isArray(i)?i.filter(n=>!(n&&n.type==="separator")):[]};const t={...C.brand},o=t.size&&t.size!=="md"?` navbar-brand-${t.size}`:"";if(t.logo||t.icon||t.tagline){let i="";t.logo?i+=`<img src="${t.logo}" class="navbar-brand-logo" alt="${t.text||""}">`:t.icon&&(i+=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),t.text&&(i+=`<span class="navbar-brand-text${o}">${t.text}</span>`),t.tagline&&(i+=`<small class="navbar-brand-tagline">${t.tagline}</small>`),t.html=i}else o&&t.text&&(t.html=`<span class="navbar-brand-text${o}">${t.text}</span>`);Domma.elements.navbar("#site-navbar",{brand:t,items:a(C.items||[]),variant:C.variant||"dark",position:C.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar");const u=b.get(0);if(C.appearOnHover){const i=window.matchMedia("(min-width: 993px)");u.querySelectorAll(".navbar-dropdown").forEach(n=>{n.addEventListener("mouseenter",()=>{i.matches&&n.classList.add("open")}),n.addEventListener("mouseleave",()=>{i.matches&&n.classList.remove("open")})})}const d=u.querySelector(".navbar-toggle"),h=u.querySelector(".navbar-collapse");if(d&&h){let i=null;const n=()=>{h.classList.contains("show")&&d.click()},c=()=>{i&&(i.remove(),i=null),document.body.classList.remove("dm-drawer-open")},s=()=>{i||(i=document.createElement("div"),i.className="dm-navbar-backdrop",i.addEventListener("click",n),document.body.appendChild(i),document.body.classList.add("dm-drawer-open"))};d.addEventListener("click",()=>{requestAnimationFrame(()=>{h.classList.contains("show")?s():c()})}),h.addEventListener("click",p=>{p.target.closest("a")&&requestAnimationFrame(c)}),document.addEventListener("keydown",p=>{p.key==="Escape"&&h.classList.contains("show")&&n()})}const m=e(C.items||[]);u.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(i=>{const n=m[parseInt(i.dataset.index,10)];if(!n)return;const c=i.dataset.subindex!=null,s=c&&Array.isArray(n.items)?n.items[parseInt(i.dataset.subindex,10)]:n;if(!s)return;s.colour&&A(i,s.colour),s.pill&&!c&&I(i,s.pill);const p=_(s.badge);p&&i.appendChild(p)})}const v=$("#site-footer");if(v.length){let u=function(r){return String(r??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},d=function(r){return u(r).replace(/'/g,"&#39;")},h=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';const l=d(r.url||"#"),g=u(r.text||""),k=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(h).join("")}</ul>`:"";return`<li><a href="${l}">${g}</a>${k}</li>`},m=function(r,l){return(r||[]).forEach(g=>{g&&g.type==="separator"||(l.push(g),Array.isArray(g.items)&&g.items.length&&m(g.items,l))}),l};const t=y.social||{},o={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let a='<div class="footer-inner container">';const e=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(e.copyright&&(a+=`<p>${u(e.copyright)}</p>`),e.primary.length&&(a+=`<nav class="footer-nav footer-nav--primary"><ul>${e.primary.map(h).join("")}</ul></nav>`),e.legal.length&&(a+=`<nav class="footer-nav footer-nav--legal"><ul>${e.legal.map(h).join("")}</ul></nav>`),y.footer){const r=Object.keys(o).filter(l=>t[l]);r.length&&(a+='<div class="footer-social">',r.forEach(l=>{const{label:g,svg:k}=o[l];a+=`<a href="${t[l]}" target="_blank" rel="noopener noreferrer" aria-label="${g}" class="footer-social-link">${k}</a>`}),a+="</div>")}a+="</div>",v.html(a);const i=[...m(e.primary,[]),...m(e.legal,[])];v.get(0).querySelectorAll(".footer-nav a").forEach((r,l)=>{const g=i[l];if(!g)return;g.colour&&A(r,g.colour);const k=_(g.badge);k&&r.appendChild(k)});const c=S.get("reduced_motion"),s=c!==null?!!c:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),p=v.get(0).querySelector(".footer-inner");if(p){const r=document.createElement("input");r.type="checkbox",r.className="form-switch-input",r.id="dm-motion-switch",r.checked=s,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const l=document.createElement("span");l.className="form-switch-label",l.textContent="Reduce motion";const g=document.createElement("label");g.className="form-switch footer-motion-switch",g.title="Reduce motion",g.appendChild(r),g.appendChild(l),p.appendChild(g)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const f=$(".page-body");if(f.length){f.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),f.find(".tabs").each(function(){Domma.elements.tabs(this)}),f.find(".carousel").each(function(){const t={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const o=parseInt(this.dataset.animationDuration,10);Number.isFinite(o)&&(t.animationDuration=o)}this.dataset.animationEasing&&(t.animationEasing=this.dataset.animationEasing),Domma.elements.carousel(this,t)}),f.find(".dm-countdown").each(function(){const t={autoStart:!0};this.dataset.to&&(t.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(t.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(t.format=this.dataset.format),Domma.elements.timer(this,t)}),f.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),f.find(".dm-progression").each(function(){Domma.elements.progression(this,{layout:this.dataset.layout||"vertical",theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(t=>{const o=t.dataset.revealMode||"stagger",a=t.dataset.revealAnimation||"slide-up",e=parseInt(t.dataset.revealDuration,10)||400,u=parseInt(t.dataset.revealStagger,10)||60,d=parseInt(t.dataset.revealDelay,10)||0,h=t.dataset.revealDirection==="rtl",m=Array.from(t.children),i={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};m.forEach((n,c)=>{n.style.opacity="0",n.style.transform=i[a]||"",n.style.transition=`opacity ${e}ms ease, transform ${e}ms ease`;const s=h?m.length-1-c:c;n.style.transitionDelay=o==="stagger"?`${d+s*u}ms`:`${d}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const n=new IntersectionObserver(c=>{c.forEach(s=>{s.isIntersecting&&(s.target.offsetWidth,s.target.style.opacity="1",s.target.style.transform="none",n.unobserve(s.target))})},{threshold:.1});m.forEach(c=>n.observe(c))}))}),f.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),f.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,o=document.getElementById(t);if(!o)return;const a=E.slideover({title:o.dataset.soTitle||"",size:o.dataset.soSize||"md",position:o.dataset.soPosition||"right"});o.style.display="",a.setContent(o),a.open()})})}if(typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const a=atob(o.dataset.config),e=JSON.parse(a);Object.assign(t,e)}catch{}}),Object.keys(t).length>0){const o={};for(const[a,e]of Object.entries(t)){const u=e?.events?.click,{confirm:d,toast:h,alert:m,prompt:i,...n}=u||{};d||h||m||i?($(a).on("click",async function(s){if(s.preventDefault(),d&&!await E.confirm(d))return;let p=null;if(!(i&&(p=await E.prompt(i,{inputPlaceholder:n.promptPlaceholder||"",inputValue:n.promptDefault||""}),p===null))){if(n.target){const r=$(n.target);n.toggleClass&&r.toggleClass(n.toggleClass),n.addClass&&r.addClass(n.addClass),n.removeClass&&r.removeClass(n.removeClass),p!==null&&(n.setText&&r.text(p),n.setVal&&r.val(p),n.setAttr&&r.attr(n.setAttr,p))}n.href&&(window.location.href=n.href),h&&E.toast(h,{type:n.toastType||"success"}),m&&E.alert(m)}}),Object.keys(n).length&&(o[a]={...e,events:{...e.events,click:n}})):o[a]=e}$.setup(o)}}f.length&&x(f.get(0))});function x(C){C.querySelectorAll(".dm-cta-trigger").forEach(y=>{y.__ctaWired||(y.__ctaWired=!0,y.addEventListener("click",async()=>{const b=y.dataset.action,v=y.dataset.entry,w=y.dataset.confirm;let f=S.get("auth_token");if(!f){E.toast("Please log in to perform this action.",{type:"warning"});return}if(w&&!await E.confirm(w))return;const t=Array.from(y.childNodes).map(a=>a.cloneNode(!0));y.disabled=!0,y.textContent="Running\u2026";const o=a=>fetch(`/api/actions/${b}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({entryId:v})});try{let a=await o(f);if(a.status===401){const u=S.get("auth_refresh_token");if(u){const d=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:u})});if(d.ok){const{token:h}=await d.json();S.set("auth_token",h),f=h,a=await o(f)}}}const e=await a.json().catch(()=>({}));if(!a.ok)throw new Error(e.error||e.message||`Error ${a.status}`);E.toast(e.message||"Action completed.",{type:"success"})}catch(a){E.toast(a.message||"Action failed.",{type:"error"})}finally{y.disabled=!1,y.textContent="",t.forEach(a=>y.appendChild(a)),Domma.icons.scan(y)}}))})}(function(){const y=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!y.length)return;const b=S.get("auth_token");function v(f,t){const o=document.createElement("div");o.className="dm-collection-display dm-collection-empty";const a=document.createElement("p");if(t){a.appendChild(document.createTextNode("Please "));const e=document.createElement("a");e.href="/login",e.textContent="sign in",a.appendChild(e),a.appendChild(document.createTextNode(" to view your entries."))}else a.textContent=f;return o.appendChild(a),o}function w(f,t){const o=document.createElement("template");o.innerHTML=t||"",f.replaceWith(o.content)}y.forEach(async f=>{const t=f.dataset.collectionAttrs;if(!t)return;if(!b){f.replaceChildren(v("",!0));return}const o=a=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({attrs:t})});try{let a=b,e=await o(a);if(e.status===401){const d=S.get("auth_refresh_token");if(d){const h=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:d})});if(h.ok){const{token:m}=await h.json();S.set("auth_token",m),a=m,e=await o(a)}}}if(!e.ok){const{error:d}=await e.json().catch(()=>({}));f.replaceChildren(v(d||"Unable to load entries.",!1));return}const{html:u}=await e.json();w(f,u)}catch{f.replaceChildren(v("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),x(document.body)},100)})(),(function(){const y=document.querySelectorAll("[data-collection-table]");!y.length||typeof T>"u"||!T.create||y.forEach(b=>{let v;try{v=JSON.parse(atob(b.dataset.payload))}catch{return}const{columns:w,rows:f,search:t,sortable:o,exportable:a,pageSize:e,empty:u,ctaConfig:d}=v;if(!w?.length)return;if(d){const i=w.findIndex(n=>n.key==="_cta");i!==-1&&(w[i]={key:"_cta",title:"",render:(n,c)=>{const s=document.createElement("button");if(s.className=`btn btn-${d.style||"primary"} dm-cta-trigger`,s.dataset.action=d.action||"",s.dataset.entry=c._entryId||"",d.confirm&&(s.dataset.confirm=d.confirm),d.icon){const p=document.createElement("span");p.dataset.icon=d.icon,s.appendChild(p),s.appendChild(document.createTextNode(" "))}return s.appendChild(document.createTextNode(d.label||"Run")),s}})}const h="col-table-"+Math.random().toString(36).slice(2,7),m=document.createElement("div");m.id=h,b.replaceChildren(m),T.create("#"+h,{data:f,columns:w,search:t,sortable:o,exportable:a,pageSize:e,emptyMessage:u}),d&&x(m)})})(),(function(){const y=document.querySelectorAll("[data-form-inline]");if(!y.length||typeof F>"u")return;function b(t,o){const a={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const u=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),d=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":u?"chooser":e.type,h={...e.formConfig||{}};h.span==="full"&&o&&(h.span=o);const m={type:d,label:e.label,required:e.required,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...h}};u&&(e.variant&&(m.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(m.multiple=!0),e.density&&(m.density=e.density),e.columns&&(m.columns=e.columns),e.accent&&(m.accent=e.accent),e.accentStyle&&(m.accentStyle=e.accentStyle),e.glow&&(m.glow=!0),e.glowColour&&(m.glowColour=e.glowColour),e.shadow&&(m.shadow=e.shadow),e.shadowColour&&(m.shadowColour=e.shadowColour)),a[e.name]=m}),a}function v(t){const o={};return(t||[]).forEach(a=>{if(!(!a.name||a.type==="page-break"||a.type==="spacer")&&(a.type==="select"||a.type==="multiselect")&&a.required){const e=(a.options||[])[0];e&&(o[a.name]=typeof e=="object"?e.value:e)}}),o}function w(t,o){(o||[]).forEach(a=>{if(a.type!=="date"||!a.name)return;const e=t.querySelector(`[name="${a.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function f(t,o,a){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=o,e.style.cssText=a?"color:var(--dm-danger,#f87171);margin-top:.75rem;":"color:var(--dm-success,#4ade80);margin-top:.75rem;"}y.forEach(t=>{let o;try{o=JSON.parse(atob(t.dataset.formInline))}catch{return}const a=o.fields||[],e=o.settings||{},u=e.columns||1,d=e.layout||"stacked",h=a.some(c=>c.type==="page-break"),m=async c=>{try{const s=t.querySelector('[name="website"]'),p=t.querySelector('[name="_t"]'),r=Object.assign({},c);s!==null&&(r._hp=s.value),p!==null&&(r._t=p.value);const l=await H.post(`/api/forms/submit/${o.slug}`,r);if(l&&l.error){f(t,l.error,!0);const g=new Error(l.error);throw g.formSubmitFailed=!0,g}if(l?.redirect){window.location.href=l.redirect;return}for(;t.firstChild;)t.removeChild(t.firstChild);f(t,l?.message||e.successMessage||"Thank you for your submission.",!1)}catch(s){throw s.formSubmitFailed||f(t,s.message||"Submission failed. Please try again.",!0),s}};function i(c){const s=c.querySelector("form");if(!s)return;const p=document.createElement("div");p.className="fb-form-honeypot",p.setAttribute("aria-hidden","true");const r=document.createElement("input");r.name="website",r.type="text",r.tabIndex=-1,r.autocomplete="url",r.placeholder="https://",p.appendChild(r);const l=document.createElement("input");l.name="_t",l.type="hidden",l.value=Date.now(),p.appendChild(l),s.appendChild(p)}function n(){if(!window.FormLogicEngine||!a.some(s=>s.logic))return;const c=new window.FormLogicEngine.FormLogicRuntime(o,t);if(c.init(),t._formLogicRuntime=c,t.parentNode&&typeof MutationObserver<"u"){const s=new MutationObserver(function(p){for(const r of p)for(const l of r.removedNodes)if(l===t||l.nodeType===1&&l.contains&&l.contains(t)){c.destroy(),s.disconnect();return}});s.observe(t.parentNode,{childList:!0,subtree:!1})}}if(h&&F.wizard){const c=[];let s=[],p=o.title||"Step 1",r="";a.forEach(g=>{g.type==="page-break"?(c.push({title:p,description:r,fields:b(s,u)}),s=[],p=g.label||`Step ${c.length+1}`,r=g.description||""):g.type!=="spacer"&&s.push(g)}),c.push({title:p,description:r,fields:b(s,u)});const l=F.wizard(t,{schema:{steps:c},onSubmit:m});Promise.resolve(l).then(function(){w(t,a),D(t,a),e.honeypot!==!1&&i(t),n()})}else if(F.render){const c=F.render(t,b(a,u),v(a),{submitText:e.submitText||"Submit",layout:d,columns:u,onSubmit:m});Promise.resolve(c).then(function(){if(d==="grid"&&e.submitSpan==="full"){const s=t.querySelector(".form-buttons");s&&s.classList.add("col-span-full")}w(t,a),D(t,a),e.honeypot!==!1&&i(t),n()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
1
+ import{makeBadgeEl as _,applyColour as A,applyPill as I}from"/public/js/menu-decor.mjs";import{enhancePickerFields as D}from"/public/js/forms.js?v=20260722-pickers";$(()=>{const C=window.__CMS_NAV__||{},y=window.__CMS_SITE__||{};if(y.autoTheme?.enabled){let o=function(e){const l=(e||"07:00").split(":");return+l[0]*60+(+l[1]||0)},a=function(){const e=new Date,l=e.getHours()*60+e.getMinutes();return l>=o(t.dayStart)&&l<o(t.nightStart)?t.dayTheme:t.nightTheme};const t=y.autoTheme;Domma.theme.set(a()),setInterval(()=>Domma.theme.set(a()),6e4)}const v=$("#site-navbar");if(v.length&&C.brand){let a=function(i,n=0){return Array.isArray(i)?i.filter(c=>!(c&&c.type==="separator"&&n===0)).map(c=>c&&c.type==="separator"?{divider:!0}:Array.isArray(c?.items)?{...c,items:a(c.items,n+1)}:c):i},e=function(i){return Array.isArray(i)?i.filter(n=>!(n&&n.type==="separator")):[]};const t={...C.brand},o=t.size&&t.size!=="md"?` navbar-brand-${t.size}`:"";if(t.logo||t.icon||t.tagline){let i="";t.logo?i+=`<img src="${t.logo}" class="navbar-brand-logo" alt="${t.text||""}">`:t.icon&&(i+=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),t.text&&(i+=`<span class="navbar-brand-text${o}">${t.text}</span>`),t.tagline&&(i+=`<small class="navbar-brand-tagline">${t.tagline}</small>`),t.html=i}else o&&t.text&&(t.html=`<span class="navbar-brand-text${o}">${t.text}</span>`);Domma.elements.navbar("#site-navbar",{brand:t,items:a(C.items||[]),variant:C.variant||"dark",position:C.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar");const l=v.get(0);if(C.appearOnHover){const i=window.matchMedia("(min-width: 993px)");l.querySelectorAll(".navbar-dropdown").forEach(n=>{n.addEventListener("mouseenter",()=>{i.matches&&n.classList.add("open")}),n.addEventListener("mouseleave",()=>{i.matches&&n.classList.remove("open")})})}const d=l.querySelector(".navbar-toggle"),h=l.querySelector(".navbar-collapse");if(d&&h){let i=null;const n=()=>{h.classList.contains("show")&&d.click()},c=()=>{i&&(i.remove(),i=null),document.body.classList.remove("dm-drawer-open")},s=()=>{i||(i=document.createElement("div"),i.className="dm-navbar-backdrop",i.addEventListener("click",n),document.body.appendChild(i),document.body.classList.add("dm-drawer-open"))};d.addEventListener("click",()=>{requestAnimationFrame(()=>{h.classList.contains("show")?s():c()})}),h.addEventListener("click",p=>{p.target.closest("a")&&requestAnimationFrame(c)}),document.addEventListener("keydown",p=>{p.key==="Escape"&&h.classList.contains("show")&&n()})}const m=e(C.items||[]);l.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(i=>{const n=m[parseInt(i.dataset.index,10)];if(!n)return;const c=i.dataset.subindex!=null,s=c&&Array.isArray(n.items)?n.items[parseInt(i.dataset.subindex,10)]:n;if(!s)return;s.colour&&A(i,s.colour),s.pill&&!c&&I(i,s.pill);const p=_(s.badge);p&&i.appendChild(p)})}const b=$("#site-footer");if(b.length){let l=function(r){return String(r??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},d=function(r){return l(r).replace(/'/g,"&#39;")},h=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';const u=d(r.url||"#"),g=l(r.text||""),k=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(h).join("")}</ul>`:"";return`<li><a href="${u}">${g}</a>${k}</li>`},m=function(r,u){return(r||[]).forEach(g=>{g&&g.type==="separator"||(u.push(g),Array.isArray(g.items)&&g.items.length&&m(g.items,u))}),u};const t=y.social||{},o={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let a='<div class="footer-inner container">';const e=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(e.copyright&&(a+=`<p>${l(e.copyright)}</p>`),e.primary.length&&(a+=`<nav class="footer-nav footer-nav--primary"><ul>${e.primary.map(h).join("")}</ul></nav>`),e.legal.length&&(a+=`<nav class="footer-nav footer-nav--legal"><ul>${e.legal.map(h).join("")}</ul></nav>`),y.footer){const r=Object.keys(o).filter(u=>t[u]);r.length&&(a+='<div class="footer-social">',r.forEach(u=>{const{label:g,svg:k}=o[u];a+=`<a href="${t[u]}" target="_blank" rel="noopener noreferrer" aria-label="${g}" class="footer-social-link">${k}</a>`}),a+="</div>")}a+="</div>",b.html(a);const i=[...m(e.primary,[]),...m(e.legal,[])];b.get(0).querySelectorAll(".footer-nav a").forEach((r,u)=>{const g=i[u];if(!g)return;g.colour&&A(r,g.colour);const k=_(g.badge);k&&r.appendChild(k)});const c=S.get("reduced_motion"),s=c!==null?!!c:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),p=b.get(0).querySelector(".footer-inner");if(p){const r=document.createElement("input");r.type="checkbox",r.className="form-switch-input",r.id="dm-motion-switch",r.checked=s,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const u=document.createElement("span");u.className="form-switch-label",u.textContent="Reduce motion";const g=document.createElement("label");g.className="form-switch footer-motion-switch",g.title="Reduce motion",g.appendChild(r),g.appendChild(u),p.appendChild(g)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const f=$(".page-body");if(f.length){f.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),f.find(".tabs").each(function(){Domma.elements.tabs(this)}),f.find(".carousel").each(function(){const t={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const o=parseInt(this.dataset.animationDuration,10);Number.isFinite(o)&&(t.animationDuration=o)}this.dataset.animationEasing&&(t.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(o=>o.cloneNode(!0)),this._dmCarouselOpts=t),this._dmCarousel=Domma.elements.carousel(this,t)}),f.find(".dm-countdown").each(function(){const t={autoStart:!0};this.dataset.to&&(t.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(t.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(t.format=this.dataset.format),Domma.elements.timer(this,t)}),f.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),f.find(".dm-progression").each(function(){const t=Array.from(this.querySelectorAll(".dm-progression-item")).map(e=>{const l={title:e.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:e.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return e.dataset.date&&(l.date=e.dataset.date),e.dataset.status&&(l.status=e.dataset.status),e.dataset.entryId&&(l._entryId=e.dataset.entryId),l}),o=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",a=Domma.elements.progression(this,{layout:o,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...t.length?{items:t}:{}});t.length&&(this._dmCtx={instance:a,items:t})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(t=>{const o=t.dataset.revealMode||"stagger",a=t.dataset.revealAnimation||"slide-up",e=parseInt(t.dataset.revealDuration,10)||400,l=parseInt(t.dataset.revealStagger,10)||60,d=parseInt(t.dataset.revealDelay,10)||0,h=t.dataset.revealDirection==="rtl",m=Array.from(t.children),i={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};m.forEach((n,c)=>{n.style.opacity="0",n.style.transform=i[a]||"",n.style.transition=`opacity ${e}ms ease, transform ${e}ms ease`;const s=h?m.length-1-c:c;n.style.transitionDelay=o==="stagger"?`${d+s*l}ms`:`${d}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const n=new IntersectionObserver(c=>{c.forEach(s=>{s.isIntersecting&&(s.target.offsetWidth,s.target.style.opacity="1",s.target.style.transform="none",n.unobserve(s.target))})},{threshold:.1});m.forEach(c=>n.observe(c))}))}),f.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),f.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,o=document.getElementById(t);if(!o)return;const a=E.slideover({title:o.dataset.soTitle||"",size:o.dataset.soSize||"md",position:o.dataset.soPosition||"right"});o.style.display="",a.setContent(o),a.open()})})}if(typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const a=atob(o.dataset.config),e=JSON.parse(a);Object.assign(t,e)}catch{}}),Object.keys(t).length>0){const o={};for(const[a,e]of Object.entries(t)){const l=e?.events?.click,{confirm:d,toast:h,alert:m,prompt:i,...n}=l||{};d||h||m||i?($(a).on("click",async function(s){if(s.preventDefault(),d&&!await E.confirm(d))return;let p=null;if(!(i&&(p=await E.prompt(i,{inputPlaceholder:n.promptPlaceholder||"",inputValue:n.promptDefault||""}),p===null))){if(n.target){const r=$(n.target);n.toggleClass&&r.toggleClass(n.toggleClass),n.addClass&&r.addClass(n.addClass),n.removeClass&&r.removeClass(n.removeClass),p!==null&&(n.setText&&r.text(p),n.setVal&&r.val(p),n.setAttr&&r.attr(n.setAttr,p))}n.href&&(window.location.href=n.href),h&&E.toast(h,{type:n.toastType||"success"}),m&&E.alert(m)}}),Object.keys(n).length&&(o[a]={...e,events:{...e.events,click:n}})):o[a]=e}$.setup(o)}}f.length&&x(f.get(0))});function x(C){C.querySelectorAll(".dm-cta-trigger").forEach(y=>{y.__ctaWired||(y.__ctaWired=!0,y.addEventListener("click",async()=>{const v=y.dataset.action,b=y.dataset.entry,w=y.dataset.confirm;let f=S.get("auth_token");if(!f){E.toast("Please log in to perform this action.",{type:"warning"});return}if(w&&!await E.confirm(w))return;const t=Array.from(y.childNodes).map(a=>a.cloneNode(!0));y.disabled=!0,y.textContent="Running\u2026";const o=a=>fetch(`/api/actions/${v}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({entryId:b})});try{let a=await o(f);if(a.status===401){const l=S.get("auth_refresh_token");if(l){const d=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:l})});if(d.ok){const{token:h}=await d.json();S.set("auth_token",h),f=h,a=await o(f)}}}const e=await a.json().catch(()=>({}));if(!a.ok)throw new Error(e.error||e.message||`Error ${a.status}`);E.toast(e.message||"Action completed.",{type:"success"})}catch(a){E.toast(a.message||"Action failed.",{type:"error"})}finally{y.disabled=!1,y.textContent="",t.forEach(a=>y.appendChild(a)),Domma.icons.scan(y)}}))})}(function(){const y=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!y.length)return;const v=S.get("auth_token");function b(f,t){const o=document.createElement("div");o.className="dm-collection-display dm-collection-empty";const a=document.createElement("p");if(t){a.appendChild(document.createTextNode("Please "));const e=document.createElement("a");e.href="/login",e.textContent="sign in",a.appendChild(e),a.appendChild(document.createTextNode(" to view your entries."))}else a.textContent=f;return o.appendChild(a),o}function w(f,t){const o=document.createElement("template");o.innerHTML=t||"",f.replaceWith(o.content)}y.forEach(async f=>{const t=f.dataset.collectionAttrs;if(!t)return;if(!v){f.replaceChildren(b("",!0));return}const o=a=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({attrs:t})});try{let a=v,e=await o(a);if(e.status===401){const d=S.get("auth_refresh_token");if(d){const h=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:d})});if(h.ok){const{token:m}=await h.json();S.set("auth_token",m),a=m,e=await o(a)}}}if(!e.ok){const{error:d}=await e.json().catch(()=>({}));f.replaceChildren(b(d||"Unable to load entries.",!1));return}const{html:l}=await e.json();w(f,l)}catch{f.replaceChildren(b("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),x(document.body)},100)})(),(function(){const y=document.querySelectorAll("[data-collection-table]");!y.length||typeof T>"u"||!T.create||y.forEach(v=>{let b;try{b=JSON.parse(atob(v.dataset.payload))}catch{return}const{columns:w,rows:f,search:t,sortable:o,exportable:a,pageSize:e,empty:l,ctaConfig:d}=b;if(!w?.length)return;if(d){const i=w.findIndex(n=>n.key==="_cta");i!==-1&&(w[i]={key:"_cta",title:"",render:(n,c)=>{const s=document.createElement("button");if(s.className=`btn btn-${d.style||"primary"} dm-cta-trigger`,s.dataset.action=d.action||"",s.dataset.entry=c._entryId||"",d.confirm&&(s.dataset.confirm=d.confirm),d.icon){const p=document.createElement("span");p.dataset.icon=d.icon,s.appendChild(p),s.appendChild(document.createTextNode(" "))}return s.appendChild(document.createTextNode(d.label||"Run")),s}})}const h="col-table-"+Math.random().toString(36).slice(2,7),m=document.createElement("div");m.id=h,v.replaceChildren(m),T.create("#"+h,{data:f,columns:w,search:t,sortable:o,exportable:a,pageSize:e,emptyMessage:l}),d&&x(m),v._dmTable={columns:w,rows:f,render(i){T.create("#"+h,{data:i,columns:w,search:t,sortable:o,exportable:a,pageSize:e,emptyMessage:l}),d&&x(m)}}})})(),(function(){const y=document.querySelectorAll("[data-form-inline]");if(!y.length||typeof F>"u")return;function v(t,o){const a={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const l=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),d=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":l?"chooser":e.type,h={...e.formConfig||{}};h.span==="full"&&o&&(h.span=o);const m={type:d,label:e.label,required:e.required,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...h}};l&&(e.variant&&(m.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(m.multiple=!0),e.density&&(m.density=e.density),e.columns&&(m.columns=e.columns),e.accent&&(m.accent=e.accent),e.accentStyle&&(m.accentStyle=e.accentStyle),e.glow&&(m.glow=!0),e.glowColour&&(m.glowColour=e.glowColour),e.shadow&&(m.shadow=e.shadow),e.shadowColour&&(m.shadowColour=e.shadowColour)),a[e.name]=m}),a}function b(t){const o={};return(t||[]).forEach(a=>{if(!(!a.name||a.type==="page-break"||a.type==="spacer")&&(a.type==="select"||a.type==="multiselect")&&a.required){const e=(a.options||[])[0];e&&(o[a.name]=typeof e=="object"?e.value:e)}}),o}function w(t,o){(o||[]).forEach(a=>{if(a.type!=="date"||!a.name)return;const e=t.querySelector(`[name="${a.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function f(t,o,a){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=o,e.style.cssText=a?"color:var(--dm-danger,#f87171);margin-top:.75rem;":"color:var(--dm-success,#4ade80);margin-top:.75rem;"}y.forEach(t=>{let o;try{o=JSON.parse(atob(t.dataset.formInline))}catch{return}const a=o.fields||[],e=o.settings||{},l=e.columns||1,d=e.layout||"stacked",h=a.some(c=>c.type==="page-break"),m=async c=>{try{const s=t.querySelector('[name="website"]'),p=t.querySelector('[name="_t"]'),r=Object.assign({},c);s!==null&&(r._hp=s.value),p!==null&&(r._t=p.value);const u=await H.post(`/api/forms/submit/${o.slug}`,r);if(u&&u.error){f(t,u.error,!0);const g=new Error(u.error);throw g.formSubmitFailed=!0,g}if(u?.redirect){window.location.href=u.redirect;return}for(;t.firstChild;)t.removeChild(t.firstChild);f(t,u?.message||e.successMessage||"Thank you for your submission.",!1)}catch(s){throw s.formSubmitFailed||f(t,s.message||"Submission failed. Please try again.",!0),s}};function i(c){const s=c.querySelector("form");if(!s)return;const p=document.createElement("div");p.className="fb-form-honeypot",p.setAttribute("aria-hidden","true");const r=document.createElement("input");r.name="website",r.type="text",r.tabIndex=-1,r.autocomplete="url",r.placeholder="https://",p.appendChild(r);const u=document.createElement("input");u.name="_t",u.type="hidden",u.value=Date.now(),p.appendChild(u),s.appendChild(p)}function n(){if(!window.FormLogicEngine||!a.some(s=>s.logic))return;const c=new window.FormLogicEngine.FormLogicRuntime(o,t);if(c.init(),t._formLogicRuntime=c,t.parentNode&&typeof MutationObserver<"u"){const s=new MutationObserver(function(p){for(const r of p)for(const u of r.removedNodes)if(u===t||u.nodeType===1&&u.contains&&u.contains(t)){c.destroy(),s.disconnect();return}});s.observe(t.parentNode,{childList:!0,subtree:!1})}}if(h&&F.wizard){const c=[];let s=[],p=o.title||"Step 1",r="";a.forEach(g=>{g.type==="page-break"?(c.push({title:p,description:r,fields:v(s,l)}),s=[],p=g.label||`Step ${c.length+1}`,r=g.description||""):g.type!=="spacer"&&s.push(g)}),c.push({title:p,description:r,fields:v(s,l)});const u=F.wizard(t,{schema:{steps:c},onSubmit:m});Promise.resolve(u).then(function(){w(t,a),D(t,a),e.honeypot!==!1&&i(t),n()})}else if(F.render){const c=F.render(t,v(a,l),b(a),{submitText:e.submitText||"Submit",layout:d,columns:l,onSubmit:m});Promise.resolve(c).then(function(){if(d==="grid"&&e.submitSpan==="full"){const s=t.querySelector(".form-buttons");s&&s.classList.add("col-span-full")}w(t,a),D(t,a),e.honeypot!==!1&&i(t),n()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
@@ -40,7 +40,12 @@ export async function settingsRoutes(fastify) {
40
40
  'theme', 'adminTheme', 'fontFamily', 'fontSize',
41
41
  'smtp', 'footer', 'analytics', 'social', 'locale',
42
42
  'layoutOptions', 'seo', 'backToTop', 'cookieConsent', 'breadcrumbs', 'autoTheme',
43
- 'adminBrand'
43
+ 'adminBrand',
44
+ // Keys that legitimately live in site.json (written elsewhere — e.g.
45
+ // `brand` by the menus migration) and are echoed back by GET /settings.
46
+ // They must be accepted here or any client that round-trips the GET
47
+ // payload into a save gets a 400 for keys it never touched.
48
+ 'brand', 'url', 'baseTheme'
44
49
  ]);
45
50
  const unknownKeys = Object.keys(data).filter(k => !ALLOWED_KEYS.has(k));
46
51
  if (unknownKeys.length > 0) {
@@ -89,19 +89,26 @@ export async function usersRoutes(fastify) {
89
89
  const target = await getUserById(id);
90
90
  if (!target) return reply.status(404).send({ error: 'User not found' });
91
91
 
92
- if (!canManageUser(actor, target)) {
92
+ // Self-edit is always allowed — the hierarchy guard only applies to
93
+ // OTHER users, otherwise nobody can edit a peer-level account (or
94
+ // even their own, since canManageUser is a strict level comparison).
95
+ // Role-escalation guards below still apply to self-edits.
96
+ const isSelf = actor.id === id;
97
+ if (!isSelf && !canManageUser(actor, target)) {
93
98
  return reply.code(403).send({ error: 'You cannot edit a user with that role' });
94
99
  }
95
100
 
96
101
  const updates = request.body || {};
97
- // Prevent role escalation beyond what the actor can manage
98
- if (updates.role && !canManageUser(actor, updates.role)) {
102
+ // Prevent role escalation beyond what the actor can manage.
103
+ // Keeping the target's existing role is never an escalation.
104
+ if (updates.role && updates.role !== target.role && !canManageUser(actor, updates.role)) {
99
105
  return reply.code(403).send({ error: 'You cannot assign that role' });
100
106
  }
101
- // Same check for any additionalRoles being assigned
107
+ // Same check for any additionalRoles being ADDED (already-held ones may be kept)
102
108
  if (Array.isArray(updates.additionalRoles)) {
109
+ const held = Array.isArray(target.additionalRoles) ? target.additionalRoles : [];
103
110
  for (const r of updates.additionalRoles) {
104
- if (!canManageUser(actor, r)) {
111
+ if (!held.includes(r) && !canManageUser(actor, r)) {
105
112
  return reply.code(403).send({ error: `You cannot assign the additional role "${r}"` });
106
113
  }
107
114
  }
@@ -173,12 +173,77 @@ function formatDate(input, fmt) {
173
173
  t => String(tokens[t]()));
174
174
  }
175
175
 
176
+ /**
177
+ * Build the `data-ctx` attribute that powers the public-site right-click
178
+ * search/filter menu (public/js/collection-context.js). The payload carries
179
+ * ONLY the fields the display actually renders — never the full entry data —
180
+ * so hidden/allowlisted-away fields can't leak through the attribute.
181
+ *
182
+ * @param {Array<{name: string, label?: string}>} fields - displayed fields
183
+ * @param {object[]} entries - collection entries {id, data}
184
+ * @returns {string} ` data-ctx="<base64 JSON>"` (leading space included)
185
+ */
186
+ function buildCtxAttr(fields, entries) {
187
+ const names = [...new Set(fields.map(f => f?.name).filter(Boolean))];
188
+ if (!names.length || !entries.length) return '';
189
+ const payload = {
190
+ fields: names.map(n => ({name: n, label: fields.find(f => f.name === n)?.label || n})),
191
+ entries: entries.map(e => ({
192
+ id: String(e.id ?? ''),
193
+ data: Object.fromEntries(names.map(n => [n, e.data?.[n] ?? '']))
194
+ }))
195
+ };
196
+ return ` data-ctx="${Buffer.from(JSON.stringify(payload), 'utf8').toString('base64')}"`;
197
+ }
198
+
199
+ /** Fields referenced by a block template's {{placeholders}} (system keys excluded). */
200
+ function blockTemplateFields(tpl) {
201
+ const names = new Set();
202
+ for (const m of String(tpl || '').matchAll(/\{\{\{?([\w]+)/g)) {
203
+ if (!['_id', '_createdAt', '_updatedAt'].includes(m[1])) names.add(m[1]);
204
+ }
205
+ return [...names].map(n => ({name: n, label: n}));
206
+ }
207
+
176
208
  function buildBlockStyleTag(blockName, css) {
177
209
  if (!css || !css.trim()) return '';
178
210
  const body = String(css).replace(/<\/style/gi, '<\\/style');
179
211
  return `<style>[data-block="${escapeAttr(blockName)}"] {\n${body}\n}</style>\n`;
180
212
  }
181
213
 
214
+ /**
215
+ * Interpolate a block template against a single collection entry.
216
+ *
217
+ * Triple-brace `{{{field}}}` — raw HTML pass-through (no escape). Use for
218
+ * fields that contain pre-rendered HTML. Must be processed BEFORE double-brace
219
+ * so the outer `{`/`}` of a triple match aren't consumed by the double-brace
220
+ * regex. Double-brace `{{field}}` — HTML-escaped (default, safe), with an
221
+ * optional `:format` suffix (e.g. `{{_createdAt:DD MMM YYYY}}`) that applies
222
+ * moment-style date formatting when the value parses as a Date.
223
+ *
224
+ * @param {string} blockTemplate - Raw HTML template with {{placeholders}}
225
+ * @param {object} e - Collection entry { id, data, meta }
226
+ * @returns {string}
227
+ */
228
+ function interpolateBlockTemplate(blockTemplate, e) {
229
+ let html = blockTemplate.replace(/\{\{\{(\w+)\}\}\}/g, (_, key) => {
230
+ return String(e.data?.[key] ?? '');
231
+ });
232
+ html = html.replace(/\{\{([\w_]+)(?::([^{}]+))?\}\}/g, (_, key, fmt) => {
233
+ let value;
234
+ if (key === '_id') value = e.id ?? '';
235
+ else if (key === '_createdAt') value = e.meta?.createdAt ?? '';
236
+ else if (key === '_updatedAt') value = e.meta?.updatedAt ?? '';
237
+ else value = e.data?.[key] ?? '';
238
+ if (fmt) {
239
+ const formatted = formatDate(value, fmt);
240
+ if (formatted !== null) return escapeHtmlText(formatted);
241
+ }
242
+ return escapeHtmlText(value);
243
+ });
244
+ return html;
245
+ }
246
+
182
247
  /**
183
248
  * Render collection entries using a reusable HTML block template.
184
249
  * Replaces {{fieldName}}, {{_id}}, {{_createdAt}}, {{_updatedAt}} placeholders.
@@ -194,38 +259,16 @@ function renderCollectionBlocks(entries, blockTemplate, emptyMsg, ctaOpts, cols,
194
259
  return `<div class="dm-collection-display dm-collection-empty"><p>${escapeHtmlText(emptyMsg)}</p></div>`;
195
260
  }
196
261
 
197
- // Only wrap entries when the block carries CSS — otherwise keep the existing
198
- // DOM structure unchanged (backward-compatible for pages using block display
199
- // before this feature existed).
262
+ // Every entry gets a neutral wrapper carrying data-entry-id (needed by the
263
+ // public right-click filter menu); the data-block attribute — which scopes
264
+ // the block's CSS — is only added when the block actually carries CSS.
200
265
  const hasCss = !!(blockCss && blockCss.trim());
201
- const wrapOpen = hasCss && blockName ? `<div data-block="${escapeAttr(blockName)}">` : '';
202
- const wrapClose = hasCss && blockName ? `</div>` : '';
266
+ const blockAttr = hasCss && blockName ? ` data-block="${escapeAttr(blockName)}"` : '';
203
267
 
204
268
  const items = entries.map(e => {
205
- // Triple-brace `{{{field}}}` — raw HTML pass-through (no escape).
206
- // Use for fields that contain pre-rendered HTML (bullet lists, code
207
- // blocks, embedded elements). Must be processed BEFORE double-brace
208
- // so the outer `{` and `}` of a triple match aren't consumed by the
209
- // double-brace regex.
210
- let html = blockTemplate.replace(/\{\{\{(\w+)\}\}\}/g, (_, key) => {
211
- return String(e.data?.[key] ?? '');
212
- });
213
- // Double-brace `{{field}}` — HTML-escaped (default, safe).
214
- // Optional `:format` suffix (e.g. `{{_createdAt:DD MMM YYYY}}`) applies
215
- // moment-style date formatting when the value parses as a Date; any
216
- // field that doesn't parse falls back to its raw value.
217
- html = html.replace(/\{\{([\w_]+)(?::([^{}]+))?\}\}/g, (_, key, fmt) => {
218
- let value;
219
- if (key === '_id') value = e.id ?? '';
220
- else if (key === '_createdAt') value = e.meta?.createdAt ?? '';
221
- else if (key === '_updatedAt') value = e.meta?.updatedAt ?? '';
222
- else value = e.data?.[key] ?? '';
223
- if (fmt) {
224
- const formatted = formatDate(value, fmt);
225
- if (formatted !== null) return escapeHtmlText(formatted);
226
- }
227
- return escapeHtmlText(value);
228
- });
269
+ const wrapOpen = `<div${blockAttr} data-entry-id="${escapeAttr(String(e.id ?? ''))}">`;
270
+ const wrapClose = `</div>`;
271
+ let html = interpolateBlockTemplate(blockTemplate, e);
229
272
 
230
273
  if (ctaOpts) {
231
274
  const ctaStyle = escapeAttr(ctaOpts.style || 'primary');
@@ -243,8 +286,9 @@ function renderCollectionBlocks(entries, blockTemplate, emptyMsg, ctaOpts, cols,
243
286
  ? `dm-collection-display dm-collection-blocks grid ${responsiveGridCols(validCols).join(' ')} gap-4`
244
287
  : 'dm-collection-display dm-collection-blocks';
245
288
 
289
+ const ctx = buildCtxAttr(blockTemplateFields(blockTemplate), entries);
246
290
  const styleTag = buildBlockStyleTag(blockName, blockCss);
247
- return `${styleTag}<div class="${wrapperClass}">\n${items.join('\n')}\n</div>`;
291
+ return `${styleTag}<div class="${wrapperClass}"${ctx}>\n${items.join('\n')}\n</div>`;
248
292
  }
249
293
 
250
294
  function renderBlockFromAttrs(tmpl, attrs, blockName = '') {
@@ -459,9 +503,9 @@ function renderCollectionCards(entries, visibleFields, titleField, columns, empt
459
503
  const iconHtml = ctaOpts.icon ? `<span data-icon="${escapeAttr(ctaOpts.icon)}"></span> ` : '';
460
504
  footer = `<div class="card-footer"><button class="${btnCls}" ${btnData}>${iconHtml}${escapeHtmlText(ctaOpts.label || 'Run')}</button></div>`;
461
505
  }
462
- return `<div class="card">${title ? `<div class="card-header">${title}</div>` : ''}<div class="card-body">${body || '&nbsp;'}</div>${footer}</div>`;
506
+ return `<div class="card" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${title ? `<div class="card-header">${title}</div>` : ''}<div class="card-body">${body || '&nbsp;'}</div>${footer}</div>`;
463
507
  }).join('\n');
464
- return `<div class="dm-collection-display grid ${responsiveGridCols(cols).join(' ')} gap-4">\n${cards}\n</div>`;
508
+ return `<div class="dm-collection-display grid ${responsiveGridCols(cols).join(' ')} gap-4"${buildCtxAttr(visibleFields, entries)}>\n${cards}\n</div>`;
465
509
  }
466
510
 
467
511
  function renderCollectionList(entries, visibleFields, titleField, emptyMsg, ctaOpts) {
@@ -486,9 +530,9 @@ function renderCollectionList(entries, visibleFields, titleField, emptyMsg, ctaO
486
530
  const iconHtml = ctaOpts.icon ? `<span data-icon="${escapeAttr(ctaOpts.icon)}"></span> ` : '';
487
531
  ctaHtml = `<button class="${btnCls}" ${btnData}>${iconHtml}${escapeHtmlText(ctaOpts.label || 'Run')}</button>`;
488
532
  }
489
- return `<div class="dm-collection-list-item">${title ? `<strong>${title}</strong>` : ''}${rest}${ctaHtml}</div>`;
533
+ return `<div class="dm-collection-list-item" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${title ? `<strong>${title}</strong>` : ''}${rest}${ctaHtml}</div>`;
490
534
  }).join('\n');
491
- return `<div class="dm-collection-display dm-collection-list">\n${items}\n</div>`;
535
+ return `<div class="dm-collection-display dm-collection-list"${buildCtxAttr(visibleFields, entries)}>\n${items}\n</div>`;
492
536
  }
493
537
 
494
538
  /**
@@ -511,7 +555,7 @@ function renderCollectionAccordion(entries, titleField, bodyField, multiple, emp
511
555
  const bodyVal = String(e.data?.[bodyField] ?? '');
512
556
  const bodyHtml = marked.parse(bodyVal);
513
557
  return (
514
- `<div class="accordion-item">\n` +
558
+ `<div class="accordion-item" data-entry-id="${escapeAttr(String(e.id ?? ''))}">\n` +
515
559
  ` <h3 class="accordion-header"><button class="accordion-button" type="button">${title}` +
516
560
  `<span class="accordion-icon" data-icon="chevron-down"></span></button></h3>\n` +
517
561
  ` <div class="accordion-body"><div class="accordion-content">${bodyHtml}</div></div>\n` +
@@ -519,7 +563,8 @@ function renderCollectionAccordion(entries, titleField, bodyField, multiple, emp
519
563
  );
520
564
  }).join('\n');
521
565
 
522
- return `<div class="dm-collection-display accordion"${multiAttr}>\n${items}\n</div>`;
566
+ const ctx = buildCtxAttr([{name: titleField}, {name: bodyField}], entries);
567
+ return `<div class="dm-collection-display accordion"${multiAttr}${ctx}>\n${items}\n</div>`;
523
568
  }
524
569
 
525
570
  /**
@@ -553,10 +598,167 @@ function renderCollectionTimeline(entries, opts) {
553
598
  const bodyHtml = bodyField
554
599
  ? marked.parse(String(e.data?.[bodyField] ?? ''))
555
600
  : '';
556
- return `<div class="dm-progression-item"${date}${status}${icon}><div class="dm-progression-item-title">${title}</div><div class="dm-progression-item-body">${bodyHtml}</div></div>`;
601
+ return `<div class="dm-progression-item" data-entry-id="${escapeAttr(String(e.id ?? ''))}"${date}${status}${icon}><div class="dm-progression-item-title">${title}</div><div class="dm-progression-item-body">${bodyHtml}</div></div>`;
602
+ }).join('\n');
603
+
604
+ const ctx = buildCtxAttr(
605
+ [{name: titleField}, {name: dateField}, {name: statusField}, {name: bodyField}].filter(f => f.name),
606
+ entries
607
+ );
608
+ return `<div class="dm-collection-display dm-progression" data-layout="${layout}" data-theme="${theme}" data-mode="${mode}"${ctx}>\n${items}\n</div>`;
609
+ }
610
+
611
+ /**
612
+ * Render a collection as a Domma carousel — one slide per entry.
613
+ * Output mirrors processCarouselBlocks so site.js's `.carousel` auto-init
614
+ * (Domma.elements.carousel) picks it up unchanged.
615
+ *
616
+ * Per-slide content comes either from a block template (opts.blockTemplate,
617
+ * interpolated per entry like display="block") or from image/title/body
618
+ * fields rendered in the standard carousel-slide structure.
619
+ *
620
+ * @param {object[]} entries
621
+ * @param {object} opts
622
+ * @param {string} [opts.imageField] - field holding the slide image URL
623
+ * @param {string} [opts.titleField] - field for the slide title
624
+ * @param {string} [opts.bodyField] - Markdown body field
625
+ * @param {string} [opts.blockTemplate] - optional block template (wins over fields)
626
+ * @param {string} [opts.blockName]
627
+ * @param {string} [opts.blockCss]
628
+ * @param {object} [opts.dataAttrs] - autoplay/interval/loop/animation passthrough
629
+ * @param {string} opts.emptyMsg
630
+ * @returns {string}
631
+ */
632
+ function renderCollectionCarousel(entries, opts) {
633
+ const {imageField, titleField, bodyField, blockTemplate, blockName, blockCss, dataAttrs = {}, emptyMsg} = opts;
634
+ if (!entries.length) {
635
+ return `<div class="dm-collection-display dm-collection-empty"><p>${escapeHtmlText(emptyMsg)}</p></div>`;
636
+ }
637
+
638
+ const passthrough = ['autoplay', 'interval', 'loop', 'animation', 'animation-duration', 'animation-easing']
639
+ .map(k => dataAttrs[k] ? ` data-${k}="${escapeAttr(String(dataAttrs[k]))}"` : '')
640
+ .join('');
641
+
642
+ const hasCss = !!(blockCss && blockCss.trim());
643
+ const slides = entries.map(e => {
644
+ let inner = '';
645
+ if (blockTemplate) {
646
+ const html = interpolateBlockTemplate(blockTemplate, e);
647
+ inner = hasCss && blockName ? `<div data-block="${escapeAttr(blockName)}">${html}</div>` : html;
648
+ } else {
649
+ const img = imageField && e.data?.[imageField]
650
+ ? `<img src="${escapeAttr(String(e.data[imageField]))}" alt="${escapeAttr(String(e.data?.[titleField] ?? ''))}">\n`
651
+ : '';
652
+ const title = titleField && e.data?.[titleField] != null
653
+ ? `<h2 class="carousel-slide-title">${escapeHtmlText(String(e.data[titleField]))}</h2>`
654
+ : '';
655
+ const bodyHtml = bodyField && e.data?.[bodyField] != null
656
+ ? `<div class="carousel-slide-description">${marked.parse(String(e.data[bodyField]))}</div>`
657
+ : '';
658
+ inner = img + (title || bodyHtml ? `<div class="carousel-slide-content">${title}${bodyHtml}</div>` : '');
659
+ }
660
+ return `<div class="carousel-slide" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${inner}</div>`;
661
+ }).join('\n');
662
+
663
+ const ctxFields = blockTemplate
664
+ ? blockTemplateFields(blockTemplate)
665
+ : [{name: titleField}, {name: bodyField}].filter(f => f.name);
666
+ const ctx = buildCtxAttr(ctxFields, entries);
667
+ const styleTag = blockTemplate ? buildBlockStyleTag(blockName, blockCss) : '';
668
+ return `${styleTag}<div class="dm-collection-display carousel"${passthrough}${ctx}><div class="carousel-track">${slides}</div></div>`;
669
+ }
670
+
671
+ /**
672
+ * Render a collection as a Domma list group — one item per entry.
673
+ * Output mirrors processListGroupBlocks (pure CSS component, no JS init).
674
+ *
675
+ * Per-item content comes either from a block template (interpolated per
676
+ * entry) or from title/body fields.
677
+ *
678
+ * @param {object[]} entries
679
+ * @param {object} opts
680
+ * @param {string} [opts.titleField] - field for the item's leading text
681
+ * @param {string} [opts.bodyField] - inline-Markdown body field
682
+ * @param {string} [opts.variant] - flush | numbered | horizontal
683
+ * @param {string} [opts.size] - sm | lg
684
+ * @param {string} [opts.itemVariant] - primary | secondary | success | danger | warning | info
685
+ * @param {boolean} [opts.action] - add .list-group-item-action per item
686
+ * @param {string} [opts.blockTemplate] - optional block template (wins over fields)
687
+ * @param {string} [opts.blockName]
688
+ * @param {string} [opts.blockCss]
689
+ * @param {string} opts.emptyMsg
690
+ * @returns {string}
691
+ */
692
+ /** Standard "block template not found" fragment. */
693
+ function blockNotFoundHtml(blockName) {
694
+ return `<div class="dm-collection-display dm-collection-empty"><p>Block template &ldquo;${escapeHtmlText(blockName || '')}&rdquo; not found.</p></div>`;
695
+ }
696
+
697
+ /**
698
+ * Load a block's template + CSS when a block attr is present.
699
+ * Returns {tpl: '', css: '', missing: false} when no block was requested,
700
+ * and {missing: true} when the named block can't be loaded — callers show
701
+ * blockNotFoundHtml() rather than silently falling back to field rendering.
702
+ *
703
+ * @param {string} [blockName]
704
+ * @returns {Promise<{tpl: string, css: string, missing: boolean}>}
705
+ */
706
+ async function loadOptionalBlockAssets(blockName) {
707
+ if (!blockName) return {tpl: '', css: '', missing: false};
708
+ try {
709
+ const [tpl, css] = await Promise.all([
710
+ loadBlockTemplate(blockName),
711
+ loadBlockCss(blockName),
712
+ ]);
713
+ return {tpl, css, missing: false};
714
+ } catch {
715
+ return {tpl: '', css: '', missing: true};
716
+ }
717
+ }
718
+
719
+ function renderCollectionListgroup(entries, opts) {
720
+ const {titleField, bodyField, variant, size, itemVariant, action, blockTemplate, blockName, blockCss, emptyMsg} = opts;
721
+ if (!entries.length) {
722
+ return `<div class="dm-collection-display dm-collection-empty"><p>${escapeHtmlText(emptyMsg)}</p></div>`;
723
+ }
724
+
725
+ const variantMap = {flush: 'list-group-flush', numbered: 'list-group-numbered', horizontal: 'list-group-horizontal'};
726
+ const sizeMap = {sm: 'list-group-sm', lg: 'list-group-lg'};
727
+ const wrapperClasses = ['dm-collection-display', 'list-group'];
728
+ if (variantMap[variant]) wrapperClasses.push(variantMap[variant]);
729
+ if (sizeMap[size]) wrapperClasses.push(sizeMap[size]);
730
+
731
+ const itemClasses = ['list-group-item'];
732
+ if (action) itemClasses.push('list-group-item-action');
733
+ if (itemVariant && /^(primary|secondary|success|danger|warning|info)$/.test(itemVariant)) {
734
+ itemClasses.push(`list-group-item-${itemVariant}`);
735
+ }
736
+ const itemClassStr = itemClasses.join(' ');
737
+
738
+ const hasCss = !!(blockCss && blockCss.trim());
739
+ const items = entries.map(e => {
740
+ let inner;
741
+ if (blockTemplate) {
742
+ const html = interpolateBlockTemplate(blockTemplate, e);
743
+ inner = hasCss && blockName ? `<div data-block="${escapeAttr(blockName)}">${html}</div>` : html;
744
+ } else {
745
+ const title = titleField && e.data?.[titleField] != null
746
+ ? `<strong>${escapeHtmlText(String(e.data[titleField]))}</strong>`
747
+ : '';
748
+ const body = bodyField && e.data?.[bodyField] != null
749
+ ? `${title ? ' ' : ''}${marked.parseInline(String(e.data[bodyField]))}`
750
+ : '';
751
+ inner = (title + body) || escapeHtmlText(String(e.id ?? ''));
752
+ }
753
+ return `<div class="${itemClassStr}" data-entry-id="${escapeAttr(String(e.id ?? ''))}">${inner}</div>`;
557
754
  }).join('\n');
558
755
 
559
- return `<div class="dm-collection-display dm-progression" data-layout="${layout}" data-theme="${theme}" data-mode="${mode}">\n${items}\n</div>`;
756
+ const ctxFields = blockTemplate
757
+ ? blockTemplateFields(blockTemplate)
758
+ : [{name: titleField}, {name: bodyField}].filter(f => f.name);
759
+ const ctx = buildCtxAttr(ctxFields, entries);
760
+ const styleTag = blockTemplate ? buildBlockStyleTag(blockName, blockCss) : '';
761
+ return `${styleTag}<div class="${wrapperClasses.join(' ')}"${ctx}>\n${items}\n</div>`;
560
762
  }
561
763
 
562
764
  /**
@@ -661,6 +863,36 @@ async function processViewBlocks(markdown, tagSet) {
661
863
  mode: timelineMode,
662
864
  emptyMsg,
663
865
  });
866
+ } else if (display === 'carousel') {
867
+ const {tpl, css, missing} = await loadOptionalBlockAssets(attrs.block);
868
+ replacement = missing
869
+ ? blockNotFoundHtml(attrs.block)
870
+ : renderCollectionCarousel(entries, {
871
+ imageField: attrs['image-field'] || '',
872
+ titleField: attrs['title-field'] || 'title',
873
+ bodyField: attrs['body-field'] || '',
874
+ blockTemplate: tpl,
875
+ blockName: attrs.block || '',
876
+ blockCss: css,
877
+ dataAttrs: attrs,
878
+ emptyMsg,
879
+ });
880
+ } else if (display === 'listgroup') {
881
+ const {tpl, css, missing} = await loadOptionalBlockAssets(attrs.block);
882
+ replacement = missing
883
+ ? blockNotFoundHtml(attrs.block)
884
+ : renderCollectionListgroup(entries, {
885
+ titleField: attrs['title-field'] || 'title',
886
+ bodyField: attrs['body-field'] || '',
887
+ variant: attrs.variant || '',
888
+ size: attrs.size || '',
889
+ itemVariant: attrs.itemvariant || '',
890
+ action: attrs.action === 'true',
891
+ blockTemplate: tpl,
892
+ blockName: attrs.block || '',
893
+ blockCss: css,
894
+ emptyMsg,
895
+ });
664
896
  } else if (display === 'block') {
665
897
  const blockName = attrs.block || viewConfig?.display?.block || '';
666
898
  if (blockName) {
@@ -671,7 +903,7 @@ async function processViewBlocks(markdown, tagSet) {
671
903
  ]);
672
904
  replacement = renderCollectionBlocks(entries, tpl, emptyMsg, ctaOpts, '', blockName, css);
673
905
  } catch {
674
- replacement = `<div class="dm-collection-display dm-collection-empty"><p>Block template &ldquo;${escapeHtmlText(blockName)}&rdquo; not found.</p></div>`;
906
+ replacement = blockNotFoundHtml(blockName);
675
907
  }
676
908
  }
677
909
  } else {
@@ -1118,6 +1350,36 @@ export async function renderCollectionFragment(attrs, { extraFilter = null } = {
1118
1350
  mode: timelineMode,
1119
1351
  emptyMsg,
1120
1352
  });
1353
+ } else if (display === 'carousel') {
1354
+ const {tpl, css, missing} = await loadOptionalBlockAssets(attrs.block);
1355
+ replacement = missing
1356
+ ? blockNotFoundHtml(attrs.block)
1357
+ : renderCollectionCarousel(entries, {
1358
+ imageField: attrs['image-field'] || '',
1359
+ titleField: attrs['title-field'] || 'title',
1360
+ bodyField: attrs['body-field'] || '',
1361
+ blockTemplate: tpl,
1362
+ blockName: attrs.block || '',
1363
+ blockCss: css,
1364
+ dataAttrs: attrs,
1365
+ emptyMsg,
1366
+ });
1367
+ } else if (display === 'listgroup') {
1368
+ const {tpl, css, missing} = await loadOptionalBlockAssets(attrs.block);
1369
+ replacement = missing
1370
+ ? blockNotFoundHtml(attrs.block)
1371
+ : renderCollectionListgroup(entries, {
1372
+ titleField: attrs['title-field'] || 'title',
1373
+ bodyField: attrs['body-field'] || '',
1374
+ variant: attrs.variant || '',
1375
+ size: attrs.size || '',
1376
+ itemVariant: attrs.itemvariant || '',
1377
+ action: attrs.action === 'true',
1378
+ blockTemplate: tpl,
1379
+ blockName: attrs.block || '',
1380
+ blockCss: css,
1381
+ emptyMsg,
1382
+ });
1121
1383
  } else if (display === 'block') {
1122
1384
  const blockName = attrs.block || '';
1123
1385
  if (blockName) {
@@ -1129,7 +1391,7 @@ export async function renderCollectionFragment(attrs, { extraFilter = null } = {
1129
1391
  const cols = attrs.cols || '';
1130
1392
  replacement = renderCollectionBlocks(entries, tpl, emptyMsg, ctaOpts, cols, blockName, css);
1131
1393
  } catch {
1132
- replacement = `<div class="dm-collection-display dm-collection-empty"><p>Block template &ldquo;${escapeHtmlText(blockName)}&rdquo; not found.</p></div>`;
1394
+ replacement = blockNotFoundHtml(blockName);
1133
1395
  }
1134
1396
  }
1135
1397
  } else {
@@ -116,7 +116,8 @@
116
116
  </script>
117
117
 
118
118
  <!-- Site initialisation -->
119
- <script src="/public/js/site.js?v=20260722-pickers" type="module"></script>
119
+ <script src="/public/js/site.js?v=20260722-collection-ctx" type="module"></script>
120
+ <script src="/public/js/collection-context.js?v=20260722-collection-ctx" defer></script>
120
121
 
121
122
  <!-- Interactive Collection Browser (used by [collection] shortcodes with
122
123
  searchable / filterable / sortable / paginate attributes) -->