domma-cms 0.40.2 → 0.41.1

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,5 +1,5 @@
1
- import{api as I}from"../api.js";import{colourToCss as Y}from"/public/js/menu-decor.mjs?v=20260817-floating-menus";import{openIconPicker as U}from"../lib/card-builder.js";let V=1;const C=()=>"i_"+V++,G=760;function X(t,d=null,l=[]){for(const e of t||[]){const n=C();if(e&&e.type==="separator"){l.push({id:n,parentId:d,type:"separator"});continue}l.push({id:n,parentId:d,text:e.text||"",url:e.url||"",icon:e.icon||"",visibility:e.visibility||"",permission:e.permission||"",hidden:!!e.hidden,bundled:!!e.bundled,badge:e.badge?{...e.badge}:null,pill:e.pill?{...e.pill}:null,colour:e.colour||""}),Array.isArray(e.items)&&e.items.length&&X(e.items,n,l)}return l}function K(t){const d=new Map;for(const e of t)d.has(e.parentId)||d.set(e.parentId,[]),d.get(e.parentId).push(e);function l(e){return(d.get(e)||[]).map(n=>{if(n.type==="separator")return{type:"separator"};const i=l(n.id);return{text:(n.text||"").trim(),url:(n.url||"").trim(),...n.icon&&{icon:n.icon.trim()},...n.visibility&&{visibility:n.visibility},...n.permission&&{permission:n.permission},...n.hidden&&{hidden:!0},...n.bundled&&{bundled:!0},...n.badge&&(n.badge.text||n.badge.countFrom)?{badge:{...n.badge.text&&{text:n.badge.text},...n.badge.countFrom&&{countFrom:n.badge.countFrom},...n.badge.variant&&{variant:n.badge.variant}}}:{},...n.pill&&n.pill.variant?{pill:{style:n.pill.style||"filled",variant:n.pill.variant}}:{},...n.colour&&{colour:n.colour},...i.length&&{items:i}}})}return l(null)}function _(t){return String(t||"").replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;")}function J(t,d){let l=0,e=d.find(n=>n.id===t);for(;e&&e.parentId;)l++,e=d.find(n=>n.id===e.parentId);return l}function y(t,d,l=()=>""){t.html("");function e(n){for(const i of d.filter(f=>f.parentId===n)){const f=J(i.id,d);if(i.type==="separator"){t.append(`
2
- <div class="menu-tree-row menu-tree-row--separator" data-id="${i.id}" style="margin-left:${f*28}px">
1
+ import{api as S}from"../api.js";import{colourToCss as rt}from"/public/js/menu-decor.mjs?v=20260817-menu-bindings";import{openIconPicker as ct}from"../lib/card-builder.js";import{applyDrop as pt}from"../lib/menu-tree.js";let mt=1;const q=()=>"i_"+mt++,ut=760;function et(t,l=null,a=[]){for(const i of t||[]){const n=q();if(i&&i.type==="separator"){a.push({id:n,parentId:l,type:"separator"});continue}a.push({id:n,parentId:l,text:i.text||"",url:i.url||"",icon:i.icon||"",visibility:i.visibility||"",permission:i.permission||"",hidden:!!i.hidden,bundled:!!i.bundled,badge:i.badge?{...i.badge}:null,pill:i.pill?{...i.pill}:null,colour:i.colour||""}),Array.isArray(i.items)&&i.items.length&&et(i.items,n,a)}return a}function ft(t){const l=new Map;for(const i of t)l.has(i.parentId)||l.set(i.parentId,[]),l.get(i.parentId).push(i);function a(i){return(l.get(i)||[]).map(n=>{if(n.type==="separator")return{type:"separator"};const o=a(n.id);return{text:(n.text||"").trim(),url:(n.url||"").trim(),...n.icon&&{icon:n.icon.trim()},...n.visibility&&{visibility:n.visibility},...n.permission&&{permission:n.permission},...n.hidden&&{hidden:!0},...n.bundled&&{bundled:!0},...n.badge&&(n.badge.text||n.badge.countFrom)?{badge:{...n.badge.text&&{text:n.badge.text},...n.badge.countFrom&&{countFrom:n.badge.countFrom},...n.badge.variant&&{variant:n.badge.variant}}}:{},...n.pill&&n.pill.variant?{pill:{style:n.pill.style||"filled",variant:n.pill.variant}}:{},...n.colour&&{colour:n.colour},...o.length&&{items:o}}})}return a(null)}function b(t){return String(t||"").replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;")}function bt(t,l){let a=0,i=l.find(n=>n.id===t);for(;i&&i.parentId;)a++,i=l.find(n=>n.id===i.parentId);return a}function nt(t,l){const{top:a,height:i}=t.getBoundingClientRect(),n=i?(l-a)/i:.5;return n<.25?"above":n>.75?"below":"into"}const it=["me-drop-above","me-drop-below","me-drop-into"];function G(t){t.get(0).querySelectorAll(".menu-tree-row").forEach(l=>l.classList.remove(...it))}function gt(t,l,a){let i=null;t.get(0).querySelectorAll(".menu-tree-row").forEach(n=>{const o=n.querySelector(".menu-tree-grip");n.draggable=!1,o&&(o.addEventListener("mousedown",()=>{n.draggable=!0}),o.addEventListener("mouseup",()=>{n.draggable=!1})),n.addEventListener("dragstart",c=>{i=n.dataset.id,n.style.opacity="0.45",c.dataTransfer.effectAllowed="move";try{c.dataTransfer.setData("text/plain",i)}catch{}}),n.addEventListener("dragend",()=>{n.style.opacity="",n.draggable=!1,i=null,G(t)}),n.addEventListener("dragover",c=>{if(!i||i===n.dataset.id)return;c.preventDefault(),c.dataTransfer.dropEffect="move";const h=nt(n,c.clientY);G(t),n.classList.add(`me-drop-${h}`)}),n.addEventListener("dragleave",()=>n.classList.remove(...it)),n.addEventListener("drop",c=>{c.preventDefault();const h=n.dataset.id,x=nt(n,c.clientY);if(G(t),!i)return;const k=pt(l(),i,h,x);i=null,k&&a()})})}function vt(t,l,a=()=>""){t.html("");function i(n){for(const o of l.filter(c=>c.parentId===n)){const c=bt(o.id,l);if(o.type==="separator"){t.append(`
2
+ <div class="menu-tree-row menu-tree-row--separator" data-id="${o.id}" style="margin-left:${c*28}px">
3
3
  <span class="menu-tree-grip" data-icon="move"></span>
4
4
  <span class="me-sep-line"><span class="me-sep-label">Separator</span></span>
5
5
  <button class="btn btn-sm btn-ghost me-ins-item" data-tooltip="Add item below"><span data-icon="plus"></span></button>
@@ -10,19 +10,23 @@ import{api as I}from"../api.js";import{colourToCss as Y}from"/public/js/menu-dec
10
10
  <button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
11
11
  <button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
12
12
  </div>
13
- `);continue}const w=i.badge&&(i.badge.text||i.badge.countFrom)?`<span class="dm-menu-badge">${_(String(i.badge.text||"#"))}</span>`:"",x=Y(i.colour),k=x?` style="color:${_(x)}"`:"";t.append(`
14
- <div class="menu-tree-row menu-tree-row--slim${i.hidden?" menu-tree-row--hidden":""}" data-id="${i.id}" style="margin-left:${f*28}px">
13
+ `);continue}const h=o.badge&&(o.badge.text||o.badge.countFrom)?`<span class="dm-menu-badge">${b(String(o.badge.text||"#"))}</span>`:"",x=rt(o.colour),k=x?` style="color:${b(x)}"`:"";t.append(`
14
+ <div class="menu-tree-row menu-tree-row--slim${o.hidden?" menu-tree-row--hidden":""}" data-id="${o.id}" style="margin-left:${c*28}px">
15
15
  <span class="menu-tree-grip" data-icon="move"></span>
16
- <span class="me-row-label"${k}>${_(i.text||"(untitled)")}${w}</span>
17
- <span class="me-row-url">${_(i.url||"")}</span>
16
+ <span class="me-row-label"${k}>${b(o.text||"(untitled)")}${h}</span>
17
+ <span class="me-row-url">${b(o.url||"")}</span>
18
18
  <button class="btn btn-sm btn-ghost me-ins-item" data-tooltip="Add item below"><span data-icon="plus"></span></button>
19
19
  <button class="btn btn-sm btn-ghost me-ins-sep" data-tooltip="Add separator below"><span data-icon="minus"></span></button>
20
20
  <button class="btn btn-sm btn-ghost me-outdent" data-tooltip="Outdent"><span data-icon="chevron-left"></span></button>
21
21
  <button class="btn btn-sm btn-ghost me-indent" data-tooltip="Indent"><span data-icon="chevron-right"></span></button>
22
22
  <button class="btn btn-sm btn-ghost me-up" data-tooltip="Move up"><span data-icon="arrow-up"></span></button>
23
23
  <button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
24
- <button class="btn btn-sm btn-ghost me-hidden ${i.hidden?"active":""}" data-tooltip="${i.hidden?"Show":"Hide"}"><span data-icon="eye-off"></span></button>
24
+ <button class="btn btn-sm btn-ghost me-hidden ${o.hidden?"active":""}" data-tooltip="${o.hidden?"Show":"Hide"}"><span data-icon="eye-off"></span></button>
25
25
  <button class="btn btn-sm btn-secondary me-edit" data-tooltip="Edit"><span data-icon="edit"></span></button>
26
26
  <button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
27
27
  </div>
28
- `),e(i.id)}}e(null),Domma.icons.scan(t.get(0)),t.get(0).querySelectorAll("[data-tooltip]").forEach(n=>{E.tooltip(n,{content:n.getAttribute("data-tooltip"),position:"top"})})}function O(t,d){t.find(".menu-tree-row").each(function(){const l=$(this).data("id"),e=d.find(k=>k.id===l);if(!e||e.type==="separator")return;const n=$(this).find(".me-text");n.length&&(e.text=n.val());const i=$(this).find(".me-url");i.length&&(e.url=i.val());const f=$(this).find(".me-icon");f.length&&(e.icon=f.val());const w=$(this).find(".me-vis");w.length&&(e.visibility=w.val());const x=$(this).find(".me-perm");x.length&&(e.permission=x.val())})}function ot(t,d){const l={};for(const e of d){const n=t.querySelector(`[name="${e}"]`);n&&(l[e]=n.type==="checkbox"?n.checked:n.value)}return l}function h(t,d={},...l){const e=document.createElement(t);Object.assign(e,d);for(const n of l)n!=null&&e.append(n);return e}function S(t,d,l){const e=h("label",{className:"form-field"},h("span",{className:"form-label",textContent:t}),d);return l&&e.append(h("small",{className:"form-hint",textContent:l,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),e}function Q(t,d){const l=[["","\u2014 Everyone \u2014"],["public","Public \u2014 anyone, incl. logged-out"],...(d||[]).map(n=>[n.name,`${n.label||n.name} and above`]),["private","Private \u2014 super-admin only"]];t&&!l.some(([n])=>n===t)&&l.push([t,`${t} (custom)`]);const e=A(l,t||"");return e.name="visibility",e}function A(t,d){const l=h("select",{className:"form-select"});return t.forEach(([e,n])=>l.add(new Option(n,e))),l.value=d??"",l}function M(t,d){const l=typeof d=="string"&&d.startsWith("#"),e=A([["","\u2014 none \u2014"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],l?"__custom__":d||""),n=h("input",{type:"color",className:"form-input me-colour-hex",value:l?d:"#000000"});n.style.display=l?"":"none",e.addEventListener("change",()=>{n.style.display=e.value==="__custom__"?"":"none"});const i=S(t,e);return i.append(n),i._read=()=>e.value==="__custom__"?n.value:e.value,i}function Z(t,d){const l=h("input",{className:"form-input me-icon",name:"icon",value:d||""}),e=h("span",{className:"me-icon-preview"});d&&e.setAttribute("data-icon",d);const n=h("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),i=f=>{f?e.setAttribute("data-icon",f):e.removeAttribute("data-icon"),Domma.icons.scan(e.parentNode||e)};return n.addEventListener("click",()=>U(f=>{l.value=f,i(f)})),l.addEventListener("input",()=>i(l.value.trim())),S(t,h("div",{className:"me-icon-field"},l,n,e))}function D(...t){return h("div",{className:"me-field-grid"},...t)}async function tt(t,d,l,e,n){const i=c=>h("h4",{className:"me-form-section",textContent:c}),f=S("Label",h("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),w=S("URL",h("input",{className:"form-input",name:"url",value:t.url||""}),"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items."),x=Z("Icon",t.icon||""),k=S("Visibility",Q(t.visibility||"",e),"Who can see this item, by role seniority. Picking a role shows it to that role and anyone more senior. Blank = everyone."),z=A([["","\u2014 no permission gate \u2014"],...(d||[]).map(c=>[c.key,c.label||c.key])],t.permission||"");z.name="permission";const W=S("Permission",z,"Hide unless the viewer holds this specific permission. Independent of Visibility \u2014 both must pass."),B=S("Badge text",h("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),g=A([["","\u2014 none \u2014"],...(l||[]).map(c=>[c.slug,c.title||c.slug])],t.badge?.countFrom||"");g.name="badgeCount";const s=S("Badge count from",g,"Show a live count of this collection\u2019s entries as the badge."),p=M("Badge colour",t.badge?.variant||""),F=A([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");F.name="renderAs";const L=S("Render as",F,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),N=S("Pill style",A([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));N.querySelector("select").name="pillStyle";const R=M("Pill colour",t.pill?.variant||""),P=h("div",{className:"me-pill-wrap"},D(N,R));P.style.display=t.pill?"":"none",F.addEventListener("change",()=>{P.style.display=F.value==="pill"?"":"none"});const j=M("Text colour",t.colour||""),o=h("button",{className:"btn btn-primary",textContent:"Apply"}),a=h("div",{className:"me-item-form"},i("Link"),f,w,x,i("Access"),D(k,W),i("Badge"),D(B,p),s,i("Appearance"),L,P,j,o),r=E.slideover({title:"Edit menu item",size:"lg",position:"right"});r.element.appendChild(a),r.open();const u=r.element.closest(".dm-slideover")||r.element.querySelector(".dm-slideover")||r.element;u.style.setProperty("width",G+"px","important"),u.style.setProperty("max-width","95vw","important"),Domma.icons.scan(a);const m=c=>a.querySelector(`[name="${c}"]`)?.value||"";o.addEventListener("click",()=>{t.text=m("text"),t.url=m("url"),t.icon=m("icon"),t.visibility=m("visibility"),t.permission=m("permission");const c=m("badgeText"),b=m("badgeCount"),v=p._read();if(t.badge=c||b?{...c&&{text:c},...b&&{countFrom:b},...v&&{variant:v}}:null,m("renderAs")==="pill"){const T=R._read();t.pill={style:m("pillStyle")||"filled",...T&&{variant:T}}}else t.pill=null;t.colour=j._read(),r.close(),n()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){const d=location.hash.match(/^#\/menus\/edit\/(.+)$/),l=d?decodeURIComponent(d[1]):null,e=!l,n=E.loader(t.get(0),{type:"dots"});let i,f,w,x,k;try{if(e)i={slug:"",name:"",description:"",items:[]};else try{i=await I.menus.get(l)}catch(o){E.toast(`Failed to load menu: ${o.message||o}`,{type:"error"}),location.hash="#/menus";return}f=await I.projects.list().catch(()=>[]),w=await H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),x=await I.collections.list().catch(()=>[]),k=await I.get("/collections/roles/entries?limit=100").catch(()=>null)}finally{n.destroy()}const W=(Array.isArray(k)?k:k?.entries||[]).map(o=>({name:o.data?.name,label:o.data?.label,level:o.data?.level??99})).filter(o=>o.name).sort((o,a)=>a.level-o.level),B='<option value="">\u2014 no permission gate \u2014</option>',g=o=>B+(w.resources||[]).map(a=>{const r=(o||"")===a.key?" selected":"";return`<option value="${a.key}"${r}>${a.label||a.key}</option>`}).join("");t.find("#me-title").text(e?"New menu":`Edit "${i.slug}"`);let s=X(i.items);const p=t.find("#me-items-list");y(p,s,g),t.find("#me-add-item").on("click",()=>{O(p,s),s.push({id:C(),parentId:null,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}),y(p,s,g)}),t.find("#me-add-separator").on("click",()=>{O(p,s),s.push({id:C(),parentId:null,type:"separator"}),y(p,s,g)});function F(o){return function(){O(p,s);const a=$(this).closest(".menu-tree-row").data("id"),r=s.findIndex(c=>c.id===a);if(r===-1)return;const u=s[r],m=o==="separator"?{id:C(),parentId:u.parentId,type:"separator"}:{id:C(),parentId:u.parentId,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1};s.splice(r+1,0,m),y(p,s,g)}}t.off("click",".me-ins-item").on("click",".me-ins-item",F("item")),t.off("click",".me-ins-sep").on("click",".me-ins-sep",F("separator")),t.off("click",".me-remove").on("click",".me-remove",function(){const o=$(this).closest(".menu-tree-row").data("id");s=s.filter(a=>a.id!==o&&a.parentId!==o),y(p,s,g)}),t.off("click",".me-indent").on("click",".me-indent",function(){O(p,s);const o=$(this).closest(".menu-tree-row").data("id"),a=s.find(m=>m.id===o);if(!a)return;const r=s.filter(m=>m.parentId===a.parentId),u=r.findIndex(m=>m.id===o);u<=0||r[u-1].type!=="separator"&&(a.parentId=r[u-1].id,y(p,s,g))}),t.off("click",".me-outdent").on("click",".me-outdent",function(){O(p,s);const o=$(this).closest(".menu-tree-row").data("id"),a=s.find(u=>u.id===o);if(!a||a.parentId==null)return;const r=s.find(u=>u.id===a.parentId);a.parentId=r?r.parentId:null,y(p,s,g)}),t.off("click",".me-up").on("click",".me-up",function(){O(p,s);const o=$(this).closest(".menu-tree-row").data("id"),a=s.filter(c=>c.parentId===s.find(b=>b.id===o)?.parentId),r=a.findIndex(c=>c.id===o);if(r<=0)return;const u=s.indexOf(a[r]),m=s.indexOf(a[r-1]);[s[u],s[m]]=[s[m],s[u]],y(p,s,g)}),t.off("click",".me-down").on("click",".me-down",function(){O(p,s);const o=$(this).closest(".menu-tree-row").data("id"),a=s.filter(c=>c.parentId===s.find(b=>b.id===o)?.parentId),r=a.findIndex(c=>c.id===o);if(r===-1||r>=a.length-1)return;const u=s.indexOf(a[r]),m=s.indexOf(a[r+1]);[s[u],s[m]]=[s[m],s[u]],y(p,s,g)}),t.off("click",".me-hidden").on("click",".me-hidden",function(){O(p,s);const o=$(this).closest(".menu-tree-row").data("id"),a=s.find(r=>r.id===o);a&&(a.hidden=!a.hidden),y(p,s,g)}),t.off("click",".me-edit").on("click",".me-edit",function(){const o=$(this).closest(".menu-tree-row").data("id"),a=s.find(r=>r.id===o);!a||a.type==="separator"||tt(a,w.resources||[],x,W,()=>y(p,s,g))}),E.tabs(t.find("#me-tabs").get(0)),t.find("#me-variant").val(i.variant||""),t.find("#me-position").val(i.position||"");const L=t.find("#me-appearOnHover").get(0);L&&(L.checked=i.appearOnHover===!0),t.find("#me-float-anchor").val(i.float?.anchor||"TL"),t.find("#me-float-offsetX").val(i.float?.offsetX??""),t.find("#me-float-offsetY").val(i.float?.offsetY??""),t.find("#me-float-width").val(i.float?.width??"");const N=()=>{const o=t.find("#me-position").val()==="floating";t.find(".dm-float-field").each(function(){this.style.display=o?"":"none"})};N(),t.find("#me-position").on("change",N),t.find("#me-fontFamily").val(i.style?.fontFamily||""),t.find("#me-fontSize").val(i.style?.fontSize||""),t.find("#me-fontWeight").val(i.style?.fontWeight||""),t.find("#me-letterSpacing").val(i.style?.letterSpacing||""),t.find("#me-iconSize").val(i.style?.iconSize||"");const{getProjectFromHash:R}=await import("../lib/project-context.js"),P=e?R():null,j=t.find("#me-project");j.html('<option value="">\u2014 none \u2014</option>'+f.map(o=>`<option value="${_(o.slug)}">${_(o.name||o.slug)}</option>`).join("")),t.find("#me-slug").val(i.slug||""),t.find("#me-name").val(i.name||""),t.find("#me-description").val(i.description||""),j.val(i.meta?.project||P||""),Domma.icons.scan(t.get(0)),t.find("#me-save").on("click",async()=>{O(p,s);const o={variant:t.find("#me-variant").val(),position:t.find("#me-position").val(),appearOnHover:!!(t.find("#me-appearOnHover").get(0)||{}).checked,floatAnchor:t.find("#me-float-anchor").val(),floatOffsetX:t.find("#me-float-offsetX").val(),floatOffsetY:t.find("#me-float-offsetY").val(),floatWidth:t.find("#me-float-width").val(),fontFamily:t.find("#me-fontFamily").val(),fontSize:t.find("#me-fontSize").val(),fontWeight:t.find("#me-fontWeight").val(),letterSpacing:t.find("#me-letterSpacing").val(),iconSize:t.find("#me-iconSize").val()},a={slug:t.find("#me-slug").val().trim(),name:t.find("#me-name").val().trim(),description:t.find("#me-description").val().trim(),project:t.find("#me-project").val()},r=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"],u=Object.fromEntries(Object.entries(o).filter(([b,v])=>r.includes(b)&&v)),m={};if(o.position==="floating"){o.floatAnchor&&(m.anchor=o.floatAnchor);for(const[b,v]of[["offsetX",o.floatOffsetX],["offsetY",o.floatOffsetY],["width",o.floatWidth]])v!==""&&v!=null&&Number.isFinite(Number(v))&&(m[b]=Number(v))}const c={slug:a.slug,name:a.name,description:a.description,...o.variant&&{variant:o.variant},...o.position&&{position:o.position},...Object.keys(m).length&&{float:m},...o.appearOnHover&&{appearOnHover:!0},...Object.keys(u).length&&{style:u},items:K(s),meta:{...i.meta||{},project:a.project||null}};try{if(e)await I.menus.create(c);else if(c.slug!==i.slug){await I.menus.create(c);const b=await I.menuLocations.get();let v=!1;for(const[T,q]of Object.entries(b))q===i.slug&&(b[T]=c.slug,v=!0);v&&await I.menuLocations.save(b),await I.menus.remove(i.slug)}else await I.menus.update(i.slug,c);E.toast("Saved.",{type:"success"}),location.hash="#/menus/edit/"+encodeURIComponent(c.slug)}catch(b){E.toast(`Save failed: ${b.message||b}`,{type:"error"})}})}};
28
+ `),i(o.id)}}i(null),Domma.icons.scan(t.get(0)),t.get(0).querySelectorAll("[data-tooltip]").forEach(n=>{E.tooltip(n,{content:n.getAttribute("data-tooltip"),position:"top"})})}function F(t,l){t.find(".menu-tree-row").each(function(){const a=$(this).data("id"),i=l.find(k=>k.id===a);if(!i||i.type==="separator")return;const n=$(this).find(".me-text");n.length&&(i.text=n.val());const o=$(this).find(".me-url");o.length&&(i.url=o.val());const c=$(this).find(".me-icon");c.length&&(i.icon=c.val());const h=$(this).find(".me-vis");h.length&&(i.visibility=h.val());const x=$(this).find(".me-perm");x.length&&(i.permission=x.val())})}function _t(t,l){const a={};for(const i of l){const n=t.querySelector(`[name="${i}"]`);n&&(a[i]=n.type==="checkbox"?n.checked:n.value)}return a}function g(t,l={},...a){const i=document.createElement(t);Object.assign(i,l);for(const n of a)n!=null&&i.append(n);return i}function j(t,l,a){const i=g("label",{className:"form-field"},g("span",{className:"form-label",textContent:t}),l);return a&&i.append(g("small",{className:"form-hint",textContent:a,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),i}function ht(t,l){const a=[["","\u2014 Everyone \u2014"],["public","Public \u2014 anyone, incl. logged-out"],...(l||[]).map(n=>[n.name,`${n.label||n.name} and above`]),["private","Private \u2014 super-admin only"]];t&&!a.some(([n])=>n===t)&&a.push([t,`${t} (custom)`]);const i=B(a,t||"");return i.name="visibility",i}function B(t,l){const a=g("select",{className:"form-select"});return t.forEach(([i,n])=>a.add(new Option(n,i))),a.value=l??"",a}function K(t,l){const a=typeof l=="string"&&l.startsWith("#"),i=B([["","\u2014 none \u2014"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],a?"__custom__":l||""),n=g("input",{type:"color",className:"form-input me-colour-hex",value:a?l:"#000000"});n.style.display=a?"":"none",i.addEventListener("change",()=>{n.style.display=i.value==="__custom__"?"":"none"});const o=j(t,i);return o.append(n),o._read=()=>i.value==="__custom__"?n.value:i.value,o}function yt(t,l){const a=g("input",{className:"form-input me-icon",name:"icon",value:l||""}),i=g("span",{className:"me-icon-preview"});l&&i.setAttribute("data-icon",l);const n=g("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),o=c=>{c?i.setAttribute("data-icon",c):i.removeAttribute("data-icon"),Domma.icons.scan(i.parentNode||i)};return n.addEventListener("click",()=>ct(c=>{a.value=c,o(c)})),a.addEventListener("input",()=>o(a.value.trim())),j(t,g("div",{className:"me-icon-field"},a,n,i))}function Z(...t){return g("div",{className:"me-field-grid"},...t)}async function wt(t,l,a,i,n){const o=f=>g("h4",{className:"me-form-section",textContent:f}),c=j("Label",g("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),h=j("URL",g("input",{className:"form-input",name:"url",value:t.url||""}),"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items."),x=yt("Icon",t.icon||""),k=j("Visibility",ht(t.visibility||"",i),"Who can see this item, by role seniority. Picking a role shows it to that role and anyone more senior. Blank = everyone."),D=B([["","\u2014 no permission gate \u2014"],...(l||[]).map(f=>[f.key,f.label||f.key])],t.permission||"");D.name="permission";const Y=j("Permission",D,"Hide unless the viewer holds this specific permission. Independent of Visibility \u2014 both must pass."),J=j("Badge text",g("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),X=B([["","\u2014 none \u2014"],...(a||[]).map(f=>[f.slug,f.title||f.slug])],t.badge?.countFrom||"");X.name="badgeCount";const V=j("Badge count from",X,"Show a live count of this collection\u2019s entries as the badge."),U=K("Badge colour",t.badge?.variant||""),d=B([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");d.name="renderAs";const w=j("Render as",d,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),v=j("Pill style",B([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));v.querySelector("select").name="pillStyle";const z=K("Pill colour",t.pill?.variant||""),C=g("div",{className:"me-pill-wrap"},Z(v,z));C.style.display=t.pill?"":"none",d.addEventListener("change",()=>{C.style.display=d.value==="pill"?"":"none"});const R=K("Text colour",t.colour||""),W=g("button",{className:"btn btn-primary",textContent:"Apply"}),A=g("div",{className:"me-item-form"},o("Link"),c,h,x,o("Access"),Z(k,Y),o("Badge"),Z(J,U),V,o("Appearance"),w,C,R,W),_=E.slideover({title:"Edit menu item",size:"lg",position:"right"});_.element.appendChild(A),_.open();const T=_.element.closest(".dm-slideover")||_.element.querySelector(".dm-slideover")||_.element;T.style.setProperty("width",ut+"px","important"),T.style.setProperty("max-width","95vw","important"),Domma.icons.scan(A);const I=f=>A.querySelector(`[name="${f}"]`)?.value||"";W.addEventListener("click",()=>{t.text=I("text"),t.url=I("url"),t.icon=I("icon"),t.visibility=I("visibility"),t.permission=I("permission");const f=I("badgeText"),L=I("badgeCount"),N=U._read();if(t.badge=f||L?{...f&&{text:f},...L&&{countFrom:L},...N&&{variant:N}}:null,I("renderAs")==="pill"){const P=z._read();t.pill={style:I("pillStyle")||"filled",...P&&{variant:P}}}else t.pill=null;t.colour=R._read(),_.close(),n()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){const l=location.hash.match(/^#\/menus\/edit\/(.+)$/),a=l?decodeURIComponent(l[1]):null,i=!a,n=E.loader(t.get(0),{type:"dots"});let o,c,h,x,k,D,Y;try{if(i)o={slug:"",name:"",description:"",items:[]};else try{o=await S.menus.get(a)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}c=await S.projects.list().catch(()=>[]),h=await H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),x=await S.collections.list().catch(()=>[]),k=await S.get("/collections/roles/entries?limit=100").catch(()=>null),D=await S.menuLocations.registry().catch(()=>[]),Y=await S.pages.list().catch(()=>[])}finally{n.destroy()}const X=(Array.isArray(k)?k:k?.entries||[]).map(e=>({name:e.data?.name,label:e.data?.label,level:e.data?.level??99})).filter(e=>e.name).sort((e,s)=>s.level-e.level),V='<option value="">\u2014 no permission gate \u2014</option>',U=e=>V+(h.resources||[]).map(s=>{const r=(e||"")===s.key?" selected":"";return`<option value="${s.key}"${r}>${s.label||s.key}</option>`}).join("");t.find("#me-title").text(i?"New menu":`Edit "${o.slug}"`);let d=et(o.items);const w=t.find("#me-items-list");function v(){vt(w,d,U),gt(w,()=>d,v)}v(),t.find("#me-add-item").on("click",()=>{F(w,d),d.push({id:q(),parentId:null,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}),v()}),t.find("#me-add-separator").on("click",()=>{F(w,d),d.push({id:q(),parentId:null,type:"separator"}),v()});function z(e){return function(){F(w,d);const s=$(this).closest(".menu-tree-row").data("id"),r=d.findIndex(p=>p.id===s);if(r===-1)return;const m=d[r],u=e==="separator"?{id:q(),parentId:m.parentId,type:"separator"}:{id:q(),parentId:m.parentId,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1};d.splice(r+1,0,u),v()}}t.off("click",".me-ins-item").on("click",".me-ins-item",z("item")),t.off("click",".me-ins-sep").on("click",".me-ins-sep",z("separator")),t.off("click",".me-remove").on("click",".me-remove",function(){const e=$(this).closest(".menu-tree-row").data("id");d=d.filter(s=>s.id!==e&&s.parentId!==e),v()}),t.off("click",".me-indent").on("click",".me-indent",function(){F(w,d);const e=$(this).closest(".menu-tree-row").data("id"),s=d.find(u=>u.id===e);if(!s)return;const r=d.filter(u=>u.parentId===s.parentId),m=r.findIndex(u=>u.id===e);m<=0||r[m-1].type!=="separator"&&(s.parentId=r[m-1].id,v())}),t.off("click",".me-outdent").on("click",".me-outdent",function(){F(w,d);const e=$(this).closest(".menu-tree-row").data("id"),s=d.find(m=>m.id===e);if(!s||s.parentId==null)return;const r=d.find(m=>m.id===s.parentId);s.parentId=r?r.parentId:null,v()}),t.off("click",".me-up").on("click",".me-up",function(){F(w,d);const e=$(this).closest(".menu-tree-row").data("id"),s=d.filter(p=>p.parentId===d.find(y=>y.id===e)?.parentId),r=s.findIndex(p=>p.id===e);if(r<=0)return;const m=d.indexOf(s[r]),u=d.indexOf(s[r-1]);[d[m],d[u]]=[d[u],d[m]],v()}),t.off("click",".me-down").on("click",".me-down",function(){F(w,d);const e=$(this).closest(".menu-tree-row").data("id"),s=d.filter(p=>p.parentId===d.find(y=>y.id===e)?.parentId),r=s.findIndex(p=>p.id===e);if(r===-1||r>=s.length-1)return;const m=d.indexOf(s[r]),u=d.indexOf(s[r+1]);[d[m],d[u]]=[d[u],d[m]],v()}),t.off("click",".me-hidden").on("click",".me-hidden",function(){F(w,d);const e=$(this).closest(".menu-tree-row").data("id"),s=d.find(r=>r.id===e);s&&(s.hidden=!s.hidden),v()}),t.off("click",".me-edit").on("click",".me-edit",function(){const e=$(this).closest(".menu-tree-row").data("id"),s=d.find(r=>r.id===e);!s||s.type==="separator"||wt(s,h.resources||[],x,X,()=>v())}),E.tabs(t.find("#me-tabs").get(0)),t.find("#me-variant").val(o.variant||""),t.find("#me-position").val(o.position||"");const C=t.find("#me-appearOnHover").get(0);C&&(C.checked=o.appearOnHover===!0),t.find("#me-float-anchor").val(o.float?.anchor||"TL"),t.find("#me-float-offsetX").val(o.float?.offsetX??""),t.find("#me-float-offsetY").val(o.float?.offsetY??""),t.find("#me-float-width").val(o.float?.width??""),t.find("#me-float-radius").val(o.float?.radius??""),t.find("#me-float-shadow").val(o.float?.shadow||""),t.find("#me-float-opacity").val(o.float?.opacity??"");const R=o.float?.accent||"",W=R.startsWith("#"),A=t.find("#me-float-accent"),_=t.find("#me-float-accent-hex");A.val(W?"__custom__":R),W&&_.val(R);const T=()=>{_.get(0).style.display=A.val()==="__custom__"?"":"none"};T(),A.on("change",T);const I=()=>A.val()==="__custom__"?_.val():A.val(),f=()=>{const e=t.find("#me-position").val()==="floating";t.find(".dm-float-field").each(function(){this.style.display=e?"":"none"}),e&&T();const s=t.find("#me-orientation").val()==="vertical";t.find(".dm-vertical-field").each(function(){this.style.display=s?"":"none"})};t.find("#me-orientation").val(o.orientation||""),t.find("#me-side").val(o.side||"left"),f(),t.find("#me-position").on("change",f),t.find("#me-orientation").on("change",f),t.find("#me-fontFamily").val(o.style?.fontFamily||""),t.find("#me-fontSize").val(o.style?.fontSize||""),t.find("#me-fontWeight").val(o.style?.fontWeight||""),t.find("#me-letterSpacing").val(o.style?.letterSpacing||""),t.find("#me-iconSize").val(o.style?.iconSize||"");const L=t.find("#me-bind-slot"),N=t.find("#me-bind-projects"),P=t.find("#me-bind-pages");L.html('<option value="">\u2014 not bound \u2014</option>'+(D||[]).map(e=>`<option value="${b(e.slot)}">${b(e.label||e.slot)} (${b(e.slot)})</option>`).join("")),N.html(c.map(e=>`<option value="${b(e.slug)}">${b(e.name||e.slug)}</option>`).join("")),L.val(o.binding?.slot||"");const ot=new Set(o.binding?.projects||[]);Array.from(N.get(0).options).forEach(e=>{e.selected=ot.has(e.value)}),P.val((o.binding?.pages||[]).join(`
29
+ `)),t.find("#me-bind-page-pick").html('<option value="">\u2014 pick a page \u2014</option>'+Y.map(e=>{const s=e.urlPath||e.url||"";return s?`<option value="${b(s)}">${b(e.title||s)} \u2014 ${b(s)}</option>`:""}).join("")),t.find("#me-bind-page-add").on("click",()=>{const e=t.find("#me-bind-page-pick").val();if(!e)return;const s=P.val().split(`
30
+ `).map(r=>r.trim()).filter(Boolean);s.includes(e)||s.push(e),P.val(s.join(`
31
+ `)),M()});function Q(){const e=L.val();return e?{slot:e,projects:Array.from(N.get(0).selectedOptions).map(s=>s.value),pages:P.val().split(`
32
+ `).map(s=>s.trim()).filter(Boolean)}:null}function M(){const e=Q(),s=t.find("#me-bind-summary");if(!e){s.html('<span class="text-muted">Not bound \u2014 this menu renders only where it is mapped or placed by a shortcode.</span>');return}const r=[];e.projects.length&&r.push(`pages in ${e.projects.map(p=>`<code>${b(p)}</code>`).join(", ")}`),e.pages.length&&r.push(`pages matching ${e.pages.map(p=>`<code>${b(p)}</code>`).join(", ")}`);const m=e.pages.filter(p=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(p)),u=m.length?`<div class="text-danger" style="margin-top:.4rem">Not valid page patterns: ${m.map(p=>`<code>${b(p)}</code>`).join(", ")}</div>`:"";s.html(r.length?`Takes over the <code>${b(e.slot)}</code> slot on ${r.join(" and ")}.${u}`:`<span class="text-warning">Slot <code>${b(e.slot)}</code> selected but no project or page chosen \u2014 the binding will never match.</span>`)}L.on("change",M),N.on("change",M),P.on("input",M),M();const{getProjectFromHash:st}=await import("../lib/project-context.js"),at=i?st():null,tt=t.find("#me-project");tt.html('<option value="">\u2014 none \u2014</option>'+c.map(e=>`<option value="${b(e.slug)}">${b(e.name||e.slug)}</option>`).join("")),t.find("#me-slug").val(o.slug||""),t.find("#me-name").val(o.name||""),t.find("#me-description").val(o.description||""),tt.val(o.meta?.project||at||""),Domma.icons.scan(t.get(0)),t.find("#me-save").on("click",async()=>{F(w,d);const e={variant:t.find("#me-variant").val(),position:t.find("#me-position").val(),appearOnHover:!!(t.find("#me-appearOnHover").get(0)||{}).checked,floatAnchor:t.find("#me-float-anchor").val(),floatOffsetX:t.find("#me-float-offsetX").val(),floatOffsetY:t.find("#me-float-offsetY").val(),floatWidth:t.find("#me-float-width").val(),floatRadius:t.find("#me-float-radius").val(),floatShadow:t.find("#me-float-shadow").val(),floatAccent:I(),floatOpacity:t.find("#me-float-opacity").val(),orientation:t.find("#me-orientation").val(),side:t.find("#me-side").val(),fontFamily:t.find("#me-fontFamily").val(),fontSize:t.find("#me-fontSize").val(),fontWeight:t.find("#me-fontWeight").val(),letterSpacing:t.find("#me-letterSpacing").val(),iconSize:t.find("#me-iconSize").val()},s={slug:t.find("#me-slug").val().trim(),name:t.find("#me-name").val().trim(),description:t.find("#me-description").val().trim(),project:t.find("#me-project").val()},r=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"],m=Object.fromEntries(Object.entries(e).filter(([y,O])=>r.includes(y)&&O)),u={};if(e.position==="floating"){e.floatAnchor&&(u.anchor=e.floatAnchor);for(const[y,O]of[["offsetX",e.floatOffsetX],["offsetY",e.floatOffsetY],["width",e.floatWidth],["radius",e.floatRadius],["opacity",e.floatOpacity]])O!==""&&O!=null&&Number.isFinite(Number(O))&&(u[y]=Number(O));e.floatShadow&&(u.shadow=e.floatShadow),e.floatAccent&&(u.accent=e.floatAccent)}const p={slug:s.slug,name:s.name,description:s.description,...e.variant&&{variant:e.variant},...e.position&&{position:e.position},...e.orientation&&{orientation:e.orientation},...e.orientation==="vertical"&&e.side&&{side:e.side},...Object.keys(u).length&&{float:u},...e.appearOnHover&&{appearOnHover:!0},...Object.keys(m).length&&{style:m},binding:Q(),items:ft(d),meta:{...o.meta||{},project:s.project||null}};try{if(i)await S.menus.create(p);else if(p.slug!==o.slug){await S.menus.create(p);const y=await S.menuLocations.get();let O=!1;for(const[lt,dt]of Object.entries(y))dt===o.slug&&(y[lt]=p.slug,O=!0);O&&await S.menuLocations.save(y),await S.menus.remove(o.slug)}else await S.menus.update(o.slug,p);E.toast("Saved.",{type:"success"}),location.hash="#/menus/edit/"+encodeURIComponent(p.slug)}catch(y){E.toast(`Save failed: ${y.message||y}`,{type:"error"})}})}};
@@ -1,5 +1,5 @@
1
- import{api as o}from"../api.js";import{filterByProject as b,getProjectFromHash as y}from"../lib/project-context.js";import{keepTableIconsAlive as f}from"../lib/table-icons.js";function l(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}export const menusView={templateUrl:"/admin/js/templates/menus.html",async onMount(s){const p=E.loader(s.get(0),{type:"dots"});let c,r;try{[c,r]=await Promise.all([o.menus.list().catch(()=>[]),o.menuLocations.get().catch(()=>({}))])}finally{p.destroy()}const d=y(),m=d?b(c,d):c,n={};for(const[e,t]of Object.entries(r||{}))t&&(n[t]||(n[t]=[]),n[t].push(e));T.create("#menus-table",{data:m,emptyMessage:'No menus yet. Click "New menu" to create one.',columns:[{key:"slug",title:"Slug",sortable:!0,render:e=>`<code>${l(e)}</code>`},{key:"name",title:"Name",sortable:!0,render:(e,t)=>`<a href="#/menus/edit/${encodeURIComponent(t.slug)}" style="font-weight:600;">${l(e||t.slug)}</a>`},{key:"itemsCount",title:"Items",render:(e,t)=>String(t.itemsCount??(Array.isArray(t.items)?t.items.length:0))},{key:"mapped",title:"Mapped",render:(e,t)=>{const a=n[t.slug]||[];return a.length?a.map(i=>`<span class="badge badge-info">${l(i)}</span>`).join(" "):'<span class="text-muted">\u2014</span>'}},{key:"bundled",title:"Bundled",render:(e,t)=>t.meta?.bundled?'<span class="badge badge-secondary">bundled</span>':""},{key:"_actions",title:"Actions",render:(e,t)=>{const a=(n[t.slug]||[]).length>0,i=a?'data-tooltip="Unmap this menu before deleting"':"",g=a?"disabled":"",u=l(t.slug);return`
1
+ import{api as i}from"../api.js";import{filterByProject as b,getProjectFromHash as y}from"../lib/project-context.js";import{keepTableIconsAlive as f}from"../lib/table-icons.js";function o(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}export const menusView={templateUrl:"/admin/js/templates/menus.html",async onMount(s){const m=E.loader(s.get(0),{type:"dots"});let r,u;try{[r,u]=await Promise.all([i.menus.list().catch(()=>[]),i.menuLocations.get().catch(()=>({}))])}finally{m.destroy()}const p=y(),g=p?b(r,p):r,l={};for(const[e,t]of Object.entries(u||{}))t&&(l[t]||(l[t]=[]),l[t].push(e));T.create("#menus-table",{data:g,emptyMessage:'No menus yet. Click "New menu" to create one.',columns:[{key:"slug",title:"Slug",sortable:!0,render:e=>`<code>${o(e)}</code>`},{key:"name",title:"Name",sortable:!0,render:(e,t)=>`<a href="#/menus/edit/${encodeURIComponent(t.slug)}" style="font-weight:600;">${o(e||t.slug)}</a>`},{key:"itemsCount",title:"Items",render:(e,t)=>String(t.itemsCount??(Array.isArray(t.items)?t.items.length:0))},{key:"mapped",title:"Mapped",render:(e,t)=>{const n=l[t.slug]||[];return n.length?n.map(a=>`<span class="badge badge-info">${o(a)}</span>`).join(" "):'<span class="text-muted">\u2014</span>'}},{key:"binding",title:"Bound to",render:(e,t)=>{const n=t.binding;if(!n?.slot)return'<span class="text-muted">\u2014</span>';const a=[...(n.projects||[]).map(c=>`project:${c}`),...n.pages||[]],d=a.length?a.join(", "):"nothing \u2014 binding never matches";return`<span class="badge badge-warning" data-tooltip="${o(d)}">${o(n.slot)}</span>`}},{key:"bundled",title:"Bundled",render:(e,t)=>t.meta?.bundled?'<span class="badge badge-secondary">bundled</span>':""},{key:"_actions",title:"Actions",render:(e,t)=>{const n=(l[t.slug]||[]).length>0,a=n?'data-tooltip="Unmap this menu before deleting"':"",d=n?"disabled":"",c=o(t.slug);return`
2
2
  <a class="btn btn-sm btn-ghost" href="#/menus/edit/${encodeURIComponent(t.slug)}" data-tooltip="Edit"><span data-icon="edit" data-icon-size="14"></span></a>
3
- <button class="btn btn-sm btn-ghost btn-duplicate" data-slug="${u}" data-tooltip="Duplicate"><span data-icon="copy" data-icon-size="14"></span></button>
4
- <button class="btn btn-sm btn-ghost btn-delete" data-slug="${u}" ${g} ${i}><span data-icon="trash" data-icon-size="14"></span></button>
5
- `}}]}),Domma.icons.scan("#menus-table"),f(s.get(0)),document.querySelectorAll("#menus-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})}),s.off("click",".btn-duplicate").on("click",".btn-duplicate",async function(){const e=$(this).data("slug");try{const t=await o.menus.duplicate(e);E.toast(`Duplicated as "${t.slug}".`,{type:"success"}),location.reload()}catch(t){E.toast(`Duplicate failed: ${t.message||t}`,{type:"error"})}}),s.off("click",".btn-delete").on("click",".btn-delete",async function(){if(this.disabled)return;const e=$(this).data("slug");if(await E.confirm(`Delete menu "${e}"? This cannot be undone.`))try{await o.menus.remove(e),E.toast("Menu deleted.",{type:"success"}),location.reload()}catch(a){E.toast(`Delete failed: ${a.message||a}`,{type:"error"})}})}};
3
+ <button class="btn btn-sm btn-ghost btn-duplicate" data-slug="${c}" data-tooltip="Duplicate"><span data-icon="copy" data-icon-size="14"></span></button>
4
+ <button class="btn btn-sm btn-ghost btn-delete" data-slug="${c}" ${d} ${a}><span data-icon="trash" data-icon-size="14"></span></button>
5
+ `}}]}),Domma.icons.scan("#menus-table"),f(s.get(0)),document.querySelectorAll("#menus-table [data-tooltip]").forEach(e=>{E.tooltip(e,{content:e.getAttribute("data-tooltip"),position:"top"})}),s.off("click",".btn-duplicate").on("click",".btn-duplicate",async function(){const e=$(this).data("slug");try{const t=await i.menus.duplicate(e);E.toast(`Duplicated as "${t.slug}".`,{type:"success"}),location.reload()}catch(t){E.toast(`Duplicate failed: ${t.message||t}`,{type:"error"})}}),s.off("click",".btn-delete").on("click",".btn-delete",async function(){if(this.disabled)return;const e=$(this).data("slug");if(await E.confirm(`Delete menu "${e}"? This cannot be undone.`))try{await i.menus.remove(e),E.toast("Menu deleted.",{type:"success"}),location.reload()}catch(n){E.toast(`Delete failed: ${n.message||n}`,{type:"error"})}})}};
@@ -1,4 +1,4 @@
1
- import{api as d}from"../api.js";import{createSimpleEditor as V}from"../lib/simple-editor.js";import{languageForFile as q}from"../lib/syntax-highlight.js";export const pluginCodeView={templateUrl:"/admin/js/templates/plugin-code.html",async onMount(o){const b=window.location.hash.match(/\/plugins\/code\/([a-z0-9-]+)/),l=b?b[1]:null;if(!l){E.toast("No plugin specified.",{type:"error"}),R.navigate("/plugins");return}o.find("#pcode-title").text(`${l} \u2014 Code`),document.title=`${l} Code - Domma CMS`;const k=o.find("#pcode-save").get(0),S=o.find("#pcode-saveall").get(0),C=o.find("#pcode-delete").get(0),x=o.find("#pcode-rename").get(0),K=o.find("#pcode-status").get(0),F=o.find("#pcode-cursor").get(0),L=o.find("#pcode-current").get(0),$=o.find("#pcode-filelist"),P=o.find("#pcode-editor").get(0);let m=[],n=null,c=null;const r=new Map,v=e=>r.has(e)&&r.get(e).content!==r.get(e).saved,p=()=>[...r.keys()].filter(v);function f(e){K.textContent=e}function g(){k.disabled=!n||!v(n),S.disabled=p().length===0,C.disabled=!n,x.disabled=!n}function h(){$.empty();const e=$.get(0),a=new Map;m.forEach(t=>{const s=t.path.includes("/")?t.path.slice(0,t.path.lastIndexOf("/")):"";a.has(s)||a.set(s,[]),a.get(s).push(t)}),[...a.keys()].sort((t,s)=>t.localeCompare(s)).forEach(t=>{if(t!==""){const s=document.createElement("div");s.className="pcode-dir",s.textContent=t+"/",e.appendChild(s)}a.get(t).forEach(s=>{const i=document.createElement("button");if(i.className="pcode-file"+(s.path===n?" active":""),i.style.paddingLeft=t===""?".5rem":"1rem",i.textContent=s.path.slice(t===""?0:t.length+1),i.title=s.path,v(s.path)){const w=document.createElement("span");w.className="pcode-dirty",w.textContent="\u25CF",i.appendChild(w)}i.addEventListener("click",()=>y(s.path)),e.appendChild(i)})})}async function u(){try{m=(await d.plugins.files(l)).files||[],h()}catch(e){E.toast(`Failed to list files: ${e.message}`,{type:"error"})}}function O(e,a){c&&c.destroy(),c=V(P,{initialValue:a,language:q(e),autoIndent:!0,onChange:t=>{r.get(e).content=t,g(),h()},onCursor:({line:t,col:s})=>{F.textContent=`Ln ${t}, Col ${s}`}}),c.textarea.addEventListener("keydown",t=>{(t.ctrlKey||t.metaKey)&&t.key==="s"&&(t.preventDefault(),t.shiftKey?N():j())}),c.focus()}async function y(e){if(e!==n){if(!r.has(e))try{const a=await d.plugins.readFile(l,e);r.set(e,{content:a.content,saved:a.content})}catch(a){E.toast(`Failed to open ${e}: ${a.message}`,{type:"error"});return}n=e,L.textContent=e+(q(e)?"":" (plain text)"),O(e,r.get(e).content),F.textContent="Ln 1, Col 1",g(),h()}}async function D(e){const a=r.get(e),t=await d.plugins.writeFile(l,e,a.content);return a.saved=a.content,t.restartRequired}async function j(){if(!(!n||!v(n))){try{const e=await D(n);f(e?`Saved ${n} \u2014 server file: restart to apply.`:`Saved ${n} \u2014 hard-refresh the admin tab to see it.`),E.toast(e?"Saved. Restart the server to apply.":"Saved.",{type:e?"warning":"success"})}catch(e){E.toast(`Save failed: ${e.message}`,{type:"error"})}g(),h(),u()}}async function N(){const e=p();if(!e.length)return;let a=!1;const t=[];for(const s of e)try{a=await D(s)||a}catch(i){t.push(`${s}: ${i.message}`)}t.length?E.toast(`Some saves failed \u2014 ${t.join("; ")}`,{type:"error"}):(f(a?`Saved ${e.length} file(s) \u2014 server files included: restart to apply.`:`Saved ${e.length} file(s) \u2014 hard-refresh the admin tab to see them.`),E.toast(`Saved ${e.length} file(s).`,{type:a?"warning":"success"})),g(),h(),u()}k.addEventListener("click",j),S.addEventListener("click",N);async function B(e,a){const t=i=>String(i||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/"/g,"&quot;");return await E.confirm(`<form id="pcode-path-form"><div class="form-group">
1
+ import{api as d}from"../api.js";import{reportRestart as V}from"../lib/server-restart.js";import{createSimpleEditor as A}from"../lib/simple-editor.js";import{languageForFile as q}from"../lib/syntax-highlight.js";export const pluginCodeView={templateUrl:"/admin/js/templates/plugin-code.html",async onMount(i){const b=window.location.hash.match(/\/plugins\/code\/([a-z0-9-]+)/),l=b?b[1]:null;if(!l){E.toast("No plugin specified.",{type:"error"}),R.navigate("/plugins");return}i.find("#pcode-title").text(`${l} \u2014 Code`),document.title=`${l} Code - Domma CMS`;const C=i.find("#pcode-save").get(0),k=i.find("#pcode-saveall").get(0),S=i.find("#pcode-delete").get(0),x=i.find("#pcode-rename").get(0),K=i.find("#pcode-status").get(0),F=i.find("#pcode-cursor").get(0),L=i.find("#pcode-current").get(0),$=i.find("#pcode-filelist"),P=i.find("#pcode-editor").get(0);let m=[],n=null,c=null;const r=new Map,h=e=>r.has(e)&&r.get(e).content!==r.get(e).saved,p=()=>[...r.keys()].filter(h);function v(e){K.textContent=e}function f(){C.disabled=!n||!h(n),k.disabled=p().length===0,S.disabled=!n,x.disabled=!n}function g(){$.empty();const e=$.get(0),a=new Map;m.forEach(t=>{const s=t.path.includes("/")?t.path.slice(0,t.path.lastIndexOf("/")):"";a.has(s)||a.set(s,[]),a.get(s).push(t)}),[...a.keys()].sort((t,s)=>t.localeCompare(s)).forEach(t=>{if(t!==""){const s=document.createElement("div");s.className="pcode-dir",s.textContent=t+"/",e.appendChild(s)}a.get(t).forEach(s=>{const o=document.createElement("button");if(o.className="pcode-file"+(s.path===n?" active":""),o.style.paddingLeft=t===""?".5rem":"1rem",o.textContent=s.path.slice(t===""?0:t.length+1),o.title=s.path,h(s.path)){const w=document.createElement("span");w.className="pcode-dirty",w.textContent="\u25CF",o.appendChild(w)}o.addEventListener("click",()=>y(s.path)),e.appendChild(o)})})}async function u(){try{m=(await d.plugins.files(l)).files||[],g()}catch(e){E.toast(`Failed to list files: ${e.message}`,{type:"error"})}}function O(e,a){c&&c.destroy(),c=A(P,{initialValue:a,language:q(e),autoIndent:!0,onChange:t=>{r.get(e).content=t,f(),g()},onCursor:({line:t,col:s})=>{F.textContent=`Ln ${t}, Col ${s}`}}),c.textarea.addEventListener("keydown",t=>{(t.ctrlKey||t.metaKey)&&t.key==="s"&&(t.preventDefault(),t.shiftKey?N():j())}),c.focus()}async function y(e){if(e!==n){if(!r.has(e))try{const a=await d.plugins.readFile(l,e);r.set(e,{content:a.content,saved:a.content})}catch(a){E.toast(`Failed to open ${e}: ${a.message}`,{type:"error"});return}n=e,L.textContent=e+(q(e)?"":" (plain text)"),O(e,r.get(e).content),F.textContent="Ln 1, Col 1",f(),g()}}async function D(e){const a=r.get(e),t=await d.plugins.writeFile(l,e,a.content);return a.saved=a.content,t.restartRequired}async function j(){if(!(!n||!h(n))){try{const e=await D(n);v(e?`Saved ${n} \u2014 server file: restart to apply.`:`Saved ${n} \u2014 hard-refresh the admin tab to see it.`),E.toast(e?"Saved. Restart the server to apply.":"Saved.",{type:e?"warning":"success"})}catch(e){E.toast(`Save failed: ${e.message}`,{type:"error"})}f(),g(),u()}}async function N(){const e=p();if(!e.length)return;let a=!1;const t=[];for(const s of e)try{a=await D(s)||a}catch(o){t.push(`${s}: ${o.message}`)}t.length?E.toast(`Some saves failed \u2014 ${t.join("; ")}`,{type:"error"}):(v(a?`Saved ${e.length} file(s) \u2014 server files included: restart to apply.`:`Saved ${e.length} file(s) \u2014 hard-refresh the admin tab to see them.`),E.toast(`Saved ${e.length} file(s).`,{type:a?"warning":"success"})),f(),g(),u()}C.addEventListener("click",j),k.addEventListener("click",N);async function B(e,a){const t=o=>String(o||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/"/g,"&quot;");return await E.confirm(`<form id="pcode-path-form"><div class="form-group">
2
2
  <label class="form-label">File path (inside the plugin)</label>
3
3
  <input class="form-control" type="text" name="path" value="${t(a)}" placeholder="admin/views/cart.js">
4
- </div></form>`,{title:e,confirmText:"OK",html:!0})&&document.querySelector('#pcode-path-form input[name="path"]')?.value.trim()||null}o.find("#pcode-newfile").on("click",async()=>{const e=await B("New File");if(e)try{await d.plugins.writeFile(l,e,""),r.set(e,{content:"",saved:""}),await u(),await y(e)}catch(a){E.toast(`Could not create file: ${a.message}`,{type:"error"})}}),x.addEventListener("click",async()=>{if(!n)return;if(["plugin.json","plugin.js","config.js"].includes(n)){E.toast(`${n} is required and cannot be renamed.`,{type:"error"});return}const e=await B("Rename File",n);if(!(!e||e===n))try{await d.plugins.writeFile(l,e,r.get(n)?.content??""),await d.plugins.deleteFile(l,n),r.set(e,{content:r.get(n).content,saved:r.get(n).content}),r.delete(n),n=null,await u(),await y(e),E.toast("Renamed.",{type:"success"})}catch(a){E.toast(`Rename failed: ${a.message}`,{type:"error"}),u()}}),C.addEventListener("click",async()=>{if(n&&await E.confirm(`Delete ${n}? This cannot be undone.`))try{await d.plugins.deleteFile(l,n),r.delete(n),E.toast(`Deleted ${n}.`,{type:"success"}),n=null,L.textContent="Select a file",c&&(c.destroy(),c=null),g(),await u()}catch(e){E.toast(`Delete failed: ${e.message}`,{type:"error"})}}),o.find("#pcode-back").on("click",async()=>{p().length&&!await E.confirm(`Discard unsaved changes in ${p().length} file(s)?`)||R.navigate("/plugins")});const M=e=>{p().length&&(e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",M),window.addEventListener("hashchange",function e(){window.removeEventListener("beforeunload",M),window.removeEventListener("hashchange",e)}),o.find("#pcode-restart").on("click",async()=>{const e=p().length;if(await E.confirm((e?`${e} file(s) have unsaved changes \u2014 they will NOT be included. `:"")+"Restart the server now? Server-side plugin changes apply on boot. The admin will be unreachable for a few seconds.",{title:"Restart Server",confirmText:"Restart"}))try{(await d.plugins.restart()).supervised||E.toast("No supervisor detected \u2014 if the server does not come back, start it manually.",{type:"warning"}),f("Restarting\u2026 the page will report back when the server returns.");const s=Date.now(),i=setInterval(async()=>{try{await H.get("/api/auth/setup-status"),clearInterval(i),f("Server is back. Server-side changes are live."),E.toast("Server restarted \u2014 changes are live.",{type:"success"})}catch{Date.now()-s>3e4&&(clearInterval(i),f("Server did not come back within 30s \u2014 check it manually."),E.toast("Server not responding after 30s.",{type:"error"}))}},1500)}catch(t){E.toast(`Restart failed: ${t.message}`,{type:"error"})}}),await u();const T=m.find(e=>e.path==="plugin.js")||m[0];T&&await y(T.path),I.scan()}};
4
+ </div></form>`,{title:e,confirmText:"OK",html:!0})&&document.querySelector('#pcode-path-form input[name="path"]')?.value.trim()||null}i.find("#pcode-newfile").on("click",async()=>{const e=await B("New File");if(e)try{await d.plugins.writeFile(l,e,""),r.set(e,{content:"",saved:""}),await u(),await y(e)}catch(a){E.toast(`Could not create file: ${a.message}`,{type:"error"})}}),x.addEventListener("click",async()=>{if(!n)return;if(["plugin.json","plugin.js","config.js"].includes(n)){E.toast(`${n} is required and cannot be renamed.`,{type:"error"});return}const e=await B("Rename File",n);if(!(!e||e===n))try{await d.plugins.writeFile(l,e,r.get(n)?.content??""),await d.plugins.deleteFile(l,n),r.set(e,{content:r.get(n).content,saved:r.get(n).content}),r.delete(n),n=null,await u(),await y(e),E.toast("Renamed.",{type:"success"})}catch(a){E.toast(`Rename failed: ${a.message}`,{type:"error"}),u()}}),S.addEventListener("click",async()=>{if(n&&await E.confirm(`Delete ${n}? This cannot be undone.`))try{await d.plugins.deleteFile(l,n),r.delete(n),E.toast(`Deleted ${n}.`,{type:"success"}),n=null,L.textContent="Select a file",c&&(c.destroy(),c=null),f(),await u()}catch(e){E.toast(`Delete failed: ${e.message}`,{type:"error"})}}),i.find("#pcode-back").on("click",async()=>{p().length&&!await E.confirm(`Discard unsaved changes in ${p().length} file(s)?`)||R.navigate("/plugins")});const M=e=>{p().length&&(e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",M),window.addEventListener("hashchange",function e(){window.removeEventListener("beforeunload",M),window.removeEventListener("hashchange",e)}),i.find("#pcode-restart").on("click",async()=>{const e=p().length;if(await E.confirm((e?`${e} file(s) have unsaved changes \u2014 they will NOT be included. `:"")+"Restart the server now? Server-side plugin changes apply on boot. The admin will be unreachable for a few seconds.",{title:"Restart Server",confirmText:"Restart"}))try{const t=await d.plugins.restart();t.supervised||E.toast("No supervisor detected \u2014 if the server does not come back, start it manually.",{type:"warning"}),await V(t,{onStatus:v})}catch(t){E.toast(`Restart failed: ${t.message}`,{type:"error"})}}),await u();const T=m.find(e=>e.path==="plugin.js")||m[0];T&&await y(T.path),I.scan()}};
@@ -1,39 +1,39 @@
1
- import{api as g,getUser as v}from"../api.js";import{pluginMarketplaceView as h}from"./plugin-marketplace.js";export const pluginsView={templateUrl:"/admin/js/templates/plugins.html",async onMount(s){s.find("#new-plugin-btn").on("click",()=>x(s));let u=!1;s.find('[data-tab="marketplace"]').on("click",async function(){if(u)return;const a=s.find("#marketplace-mount");try{const n=await H.get(h.templateUrl),p=new DOMParser().parseFromString(n,"text/html");for(;p.body.firstChild;)a.get(0).appendChild(p.body.firstChild);await h.onMount(a),u=!0}catch{E.toast("Failed to load marketplace.",{type:"error"})}}),E.tabs(s.find("#plugin-tabs").get(0));const r=E.loader(s.get(0),{type:"dots"});let e=[];try{e=await g.plugins.list()}catch(a){E.toast(`Failed to load plugins: ${a.message}`,{type:"error"})}if(r.destroy(),!e.length){s.find("#plugins-empty").show();return}f(s,e),Domma.icons.scan()}};function x(s){const u=E.slideover({title:"New Plugin",size:"sm",position:"right"}),r=document.createElement("div");r.style.cssText="padding:.25rem 0 .5rem;";const e=v(),a=document.createElement("div");F.create({displayName:{type:"string",label:"Display Name",placeholder:"e.g. Event Manager",required:!0},slug:{type:"string",label:"Slug",placeholder:"event-manager",required:!0,help:"Folder name under plugins/ \u2014 lowercase letters, digits, hyphens."},description:{type:"string",label:"Description",placeholder:"What does this plugin do?"},author:{type:"string",label:"Author"},icon:{type:"string",label:"Icon",placeholder:"box",help:"A Domma icon name (as used in data-icon)."}},{author:e?.name||e?.username||"",icon:"box"},{showSubmitButton:!1}).renderTo(a),r.appendChild(a);const n=document.createElement("p");n.className="text-muted",n.style.cssText="font-size:.8rem;margin:.5rem 0 0;",n.textContent="The plugin is scaffolded from the built-in template with server routes, an admin view and a CLAUDE.md, and enabled automatically. Restart the server to activate it.",r.appendChild(n);const l=document.createElement("div");l.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const p=document.createElement("button");p.className="btn btn-ghost",p.textContent="Cancel";const i=document.createElement("button");i.className="btn btn-primary",i.textContent="Create Plugin",l.appendChild(p),l.appendChild(i),r.appendChild(l),u.element.appendChild(r),u.open();const m=a.querySelector('input[name="displayName"]'),t=a.querySelector('input[name="slug"]');setTimeout(()=>m?.focus(),50);let d=!1;t?.addEventListener("input",()=>{d=!0}),m?.addEventListener("input",()=>{d||!t||(t.value=m.value.toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,40))});async function b(){const c={};for(const y of["displayName","slug","description","author","icon"])c[y]=a.querySelector(`input[name="${y}"]`)?.value.trim()||"";if(!c.displayName||!c.slug){E.toast("Display name and slug are required.",{type:"error"});return}i.disabled=!0,i.textContent="Creating\u2026";try{await g.plugins.scaffold(c),u.close(),E.toast(`Plugin "${c.displayName}" created. Restart the server to activate it.`,{type:"success"});const y=await g.plugins.list();y.length&&(s.find("#plugins-empty").hide(),f(s,y))}catch(y){E.toast(y.message||"Failed to create plugin.",{type:"error"}),i.disabled=!1,i.textContent="Create Plugin"}}p.addEventListener("click",()=>u.close()),i.addEventListener("click",b),t?.addEventListener("keydown",c=>{c.key==="Enter"&&b()})}function f(s,u){const r=s.find("#plugins-grid").empty();u.forEach(e=>{const a=e.enabled?"badge-success":"badge-secondary",n=e.enabled?"Enabled":"Disabled",l=e.enabled?"Disable":"Enable",p=e.enabled?"btn-ghost":"btn-primary",i=e.bundled?"var(--dm-info,#2563eb)":"var(--dm-secondary,#6b7280)",m=e.bundled?"Bundled \u2014 click to unbundle":"Not bundled \u2014 click to bundle",t=`<button class="badge badge-outline btn-bundle-plugin" data-name="${e.name}" data-bundled="${e.bundled?"1":"0"}" title="${m}" style="font-size:0.65rem;padding:1px 6px;cursor:pointer;background:none;border:1px solid;color:${i};border-color:${i};">${e.bundled?"Bundled":"Unbundled"}</button>`,d=e.author?`by ${o(e.author)}`:"",b=e.date?o(e.date):"",c=`
1
+ import{api as b,getUser as h}from"../api.js";import{pluginMarketplaceView as v}from"./plugin-marketplace.js";import{reportRestart as x}from"../lib/server-restart.js";export const pluginsView={templateUrl:"/admin/js/templates/plugins.html",async onMount(s){s.find("#new-plugin-btn").on("click",()=>k(s));let p=!1;s.find('[data-tab="marketplace"]').on("click",async function(){if(p)return;const a=s.find("#marketplace-mount");try{const n=await H.get(v.templateUrl),u=new DOMParser().parseFromString(n,"text/html");for(;u.body.firstChild;)a.get(0).appendChild(u.body.firstChild);await v.onMount(a),p=!0}catch{E.toast("Failed to load marketplace.",{type:"error"})}}),E.tabs(s.find("#plugin-tabs").get(0));const r=E.loader(s.get(0),{type:"dots"});let e=[];try{e=await b.plugins.list()}catch(a){E.toast(`Failed to load plugins: ${a.message}`,{type:"error"})}if(r.destroy(),!e.length){s.find("#plugins-empty").show();return}f(s,e),Domma.icons.scan()}};function k(s){const p=E.slideover({title:"New Plugin",size:"sm",position:"right"}),r=document.createElement("div");r.style.cssText="padding:.25rem 0 .5rem;";const e=h(),a=document.createElement("div");F.create({displayName:{type:"string",label:"Display Name",placeholder:"e.g. Event Manager",required:!0},slug:{type:"string",label:"Slug",placeholder:"event-manager",required:!0,help:"Folder name under plugins/ \u2014 lowercase letters, digits, hyphens."},description:{type:"string",label:"Description",placeholder:"What does this plugin do?"},author:{type:"string",label:"Author"},icon:{type:"string",label:"Icon",placeholder:"box",help:"A Domma icon name (as used in data-icon)."}},{author:e?.name||e?.username||"",icon:"box"},{showSubmitButton:!1}).renderTo(a),r.appendChild(a);const n=document.createElement("p");n.className="text-muted",n.style.cssText="font-size:.8rem;margin:.5rem 0 0;",n.textContent="The plugin is scaffolded from the built-in template with server routes, an admin view and a CLAUDE.md, and enabled automatically. Restart the server to activate it.",r.appendChild(n);const l=document.createElement("div");l.style.cssText="display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem;";const u=document.createElement("button");u.className="btn btn-ghost",u.textContent="Cancel";const o=document.createElement("button");o.className="btn btn-primary",o.textContent="Create Plugin",l.appendChild(u),l.appendChild(o),r.appendChild(l),p.element.appendChild(r),p.open();const m=a.querySelector('input[name="displayName"]'),t=a.querySelector('input[name="slug"]');setTimeout(()=>m?.focus(),50);let d=!1;t?.addEventListener("input",()=>{d=!0}),m?.addEventListener("input",()=>{d||!t||(t.value=m.value.toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,40))});async function g(){const c={};for(const y of["displayName","slug","description","author","icon"])c[y]=a.querySelector(`input[name="${y}"]`)?.value.trim()||"";if(!c.displayName||!c.slug){E.toast("Display name and slug are required.",{type:"error"});return}o.disabled=!0,o.textContent="Creating\u2026";try{await b.plugins.scaffold(c),p.close(),E.toast(`Plugin "${c.displayName}" created. Restart the server to activate it.`,{type:"success"});const y=await b.plugins.list();y.length&&(s.find("#plugins-empty").hide(),f(s,y))}catch(y){E.toast(y.message||"Failed to create plugin.",{type:"error"}),o.disabled=!1,o.textContent="Create Plugin"}}u.addEventListener("click",()=>p.close()),o.addEventListener("click",g),t?.addEventListener("keydown",c=>{c.key==="Enter"&&g()})}function f(s,p){const r=s.find("#plugins-grid").empty();p.forEach(e=>{const a=e.enabled?"badge-success":"badge-secondary",n=e.enabled?"Enabled":"Disabled",l=e.enabled?"Disable":"Enable",u=e.enabled?"btn-ghost":"btn-primary",o=e.bundled?"var(--dm-info,#2563eb)":"var(--dm-secondary,#6b7280)",m=e.bundled?"Bundled \u2014 click to unbundle":"Not bundled \u2014 click to bundle",t=`<button class="badge badge-outline btn-bundle-plugin" data-name="${e.name}" data-bundled="${e.bundled?"1":"0"}" title="${m}" style="font-size:0.65rem;padding:1px 6px;cursor:pointer;background:none;border:1px solid;color:${o};border-color:${o};">${e.bundled?"Bundled":"Unbundled"}</button>`,d=e.author?`by ${i(e.author)}`:"",g=e.date?i(e.date):"",c=`
2
2
  <div class="card plugin-card" data-plugin="${e.name}">
3
3
  <div class="card-body">
4
4
  <div class="plugin-header">
5
5
  <div class="plugin-icon"><span data-icon="${e.icon||"package"}"></span></div>
6
6
  <div class="plugin-meta">
7
- <div class="plugin-name">${o(e.displayName)}</div>
8
- <div class="plugin-version">v${o(e.version)}${d?` &middot; ${d}`:""}</div>
7
+ <div class="plugin-name">${i(e.displayName)}</div>
8
+ <div class="plugin-version">v${i(e.version)}${d?` &middot; ${d}`:""}</div>
9
9
  </div>
10
10
  <div style="display:flex;gap:.35rem;align-items:center;">
11
11
  ${t}
12
12
  <span class="badge ${a}">${n}</span>
13
13
  </div>
14
14
  </div>
15
- <p class="plugin-desc">${o(e.description)}</p>
16
- ${b?`<p class="plugin-date text-muted" style="font-size:.8rem;margin:0">Released ${b}</p>`:""}
15
+ <p class="plugin-desc">${i(e.description)}</p>
16
+ ${g?`<p class="plugin-date text-muted" style="font-size:.8rem;margin:0">Released ${g}</p>`:""}
17
17
  </div>
18
18
  <div class="plugin-footer">
19
19
  <span class="text-muted" style="font-size:.8rem">Restart server after changes</span>
20
20
  <div class="plugin-footer-actions">
21
21
  <button class="btn btn-sm btn-ghost btn-plugin-code" data-name="${e.name}"><span data-icon="code"></span> Code</button>
22
22
  ${e.settingsSchema?`<button class="btn btn-sm btn-ghost btn-plugin-settings" data-name="${e.name}"><span data-icon="settings"></span> Settings</button>`:""}
23
- <button class="btn btn-sm ${p} btn-toggle-plugin"
23
+ <button class="btn btn-sm ${u} btn-toggle-plugin"
24
24
  data-name="${e.name}"
25
25
  data-enabled="${e.enabled?"1":"0"}">
26
26
  <span data-icon="${e.enabled?"pause":"play"}"></span> ${l}
27
27
  </button>
28
28
  </div>
29
29
  </div>
30
- </div>`;r.append(c)}),Domma.icons.scan(),Domma.effects.reveal(".plugin-card",{animation:"fade",stagger:60,duration:400}),Domma.effects.ripple(".btn-toggle-plugin"),r.on("click",".btn-plugin-code",function(){R.navigate(`/plugins/code/${$(this).data("name")}`)}),r.on("click",".btn-plugin-settings",async function(){const e=$(this).data("name"),a=u.find(t=>t.name===e);if(!a?.settingsSchema)return;const l=`<form id="plugin-settings-form">${a.settingsSchema.map(t=>{const d=a.settings?.[t.key]??t.default??"";if(t.type==="select"){const b=t.options.map(c=>`<option value="${o(c.value)}"${String(d)===String(c.value)?" selected":""}>${o(c.label)}</option>`).join("");return`<div class="form-group">
31
- <label class="form-label">${o(t.label)}</label>
32
- <select class="form-control" name="${o(t.key)}">${b}</select>
30
+ </div>`;r.append(c)}),Domma.icons.scan(),Domma.effects.reveal(".plugin-card",{animation:"fade",stagger:60,duration:400}),Domma.effects.ripple(".btn-toggle-plugin"),r.on("click",".btn-plugin-code",function(){R.navigate(`/plugins/code/${$(this).data("name")}`)}),r.on("click",".btn-plugin-settings",async function(){const e=$(this).data("name"),a=p.find(t=>t.name===e);if(!a?.settingsSchema)return;const l=`<form id="plugin-settings-form">${a.settingsSchema.map(t=>{const d=a.settings?.[t.key]??t.default??"";if(t.type==="select"){const g=t.options.map(c=>`<option value="${i(c.value)}"${String(d)===String(c.value)?" selected":""}>${i(c.label)}</option>`).join("");return`<div class="form-group">
31
+ <label class="form-label">${i(t.label)}</label>
32
+ <select class="form-control" name="${i(t.key)}">${g}</select>
33
33
  </div>`}return t.type==="number"?`<div class="form-group">
34
- <label class="form-label">${o(t.label)}</label>
35
- <input class="form-control" type="number" name="${o(t.key)}" value="${o(String(d))}"${t.min!=null?` min="${t.min}"`:""}${t.max!=null?` max="${t.max}"`:""}>
34
+ <label class="form-label">${i(t.label)}</label>
35
+ <input class="form-control" type="number" name="${i(t.key)}" value="${i(String(d))}"${t.min!=null?` min="${t.min}"`:""}${t.max!=null?` max="${t.max}"`:""}>
36
36
  </div>`:`<div class="form-group">
37
- <label class="form-label">${o(t.label)}</label>
38
- <input class="form-control" type="text" name="${o(t.key)}" value="${o(String(d))}">
39
- </div>`}).join("")}</form>`;if(!await E.confirm(l,{title:`${o(a.displayName)} Settings`,confirmText:"Save",cancelText:"Cancel",html:!0}))return;const i=document.getElementById("plugin-settings-form");if(!i)return;const m={};a.settingsSchema.forEach(t=>{const d=i.elements[t.key];d&&(m[t.key]=t.type==="number"?Number(d.value):d.value)});try{await g.plugins.update(e,{settings:m}),a.settings={...a.settings,...m},E.toast("Settings saved.",{type:"success"})}catch(t){E.toast(`Failed to save: ${t.message}`,{type:"error"})}}),r.on("click",".btn-bundle-plugin",async function(){const e=$(this),a=e.data("name"),n=e.data("bundled")!==1&&e.data("bundled")!=="1";e.prop("disabled",!0).text(n?"Bundling\u2026":"Unbundling\u2026");try{await g.plugins.update(a,{bundled:n}),E.toast(`Plugin ${n?"marked as bundled":"unbundled"}.`,{type:"success"});const l=await g.plugins.list();f(s,l)}catch(l){E.toast(`Failed: ${l.message}`,{type:"error"}),e.prop("disabled",!1)}}),r.on("click",".btn-toggle-plugin",async function(){const e=$(this),a=e.data("name"),n=e.data("enabled")!==1&&e.data("enabled")!=="1";e.prop("disabled",!0).text(n?"Enabling\u2026":"Disabling\u2026");try{await g.plugins.update(a,{enabled:n}),E.toast(`Plugin ${n?"enabled":"disabled"}. Restart the server to apply changes.`,{type:"success"});const l=await g.plugins.list();f(s,l)}catch(l){E.toast(`Failed: ${l.message}`,{type:"error"}),e.prop("disabled",!1)}})}function o(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}
37
+ <label class="form-label">${i(t.label)}</label>
38
+ <input class="form-control" type="text" name="${i(t.key)}" value="${i(String(d))}">
39
+ </div>`}).join("")}</form>`;if(!await E.confirm(l,{title:`${i(a.displayName)} Settings`,confirmText:"Save",cancelText:"Cancel",html:!0}))return;const o=document.getElementById("plugin-settings-form");if(!o)return;const m={};a.settingsSchema.forEach(t=>{const d=o.elements[t.key];d&&(m[t.key]=t.type==="number"?Number(d.value):d.value)});try{await b.plugins.update(e,{settings:m}),a.settings={...a.settings,...m},E.toast("Settings saved.",{type:"success"})}catch(t){E.toast(`Failed to save: ${t.message}`,{type:"error"})}}),r.on("click",".btn-bundle-plugin",async function(){const e=$(this),a=e.data("name"),n=e.data("bundled")!==1&&e.data("bundled")!=="1";e.prop("disabled",!0).text(n?"Bundling\u2026":"Unbundling\u2026");try{await b.plugins.update(a,{bundled:n}),E.toast(`Plugin ${n?"marked as bundled":"unbundled"}.`,{type:"success"});const l=await b.plugins.list();f(s,l)}catch(l){E.toast(`Failed: ${l.message}`,{type:"error"}),e.prop("disabled",!1)}}),r.on("click",".btn-toggle-plugin",async function(){const e=$(this),a=e.data("name"),n=e.data("enabled")!==1&&e.data("enabled")!=="1";e.prop("disabled",!0).text(n?"Enabling\u2026":"Disabling\u2026");try{const l=await b.plugins.update(a,{enabled:n});E.toast(`Plugin ${n?"enabled":"disabled"}.`,{type:"success"}),await x(l,{onStatus:o=>e.text(o.slice(0,24))});const u=await b.plugins.list();f(s,u)}catch(l){E.toast(`Failed: ${l.message}`,{type:"error"}),e.prop("disabled",!1)}})}function i(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.40.2",
3
+ "version": "0.41.1",
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",