domma-cms 0.71.0 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import{api as f}from"../api.js";import{isPathAllowed as se}from"./admin-scope.js";import{colourToCss as G,applyFloat as ie,applyHighlight as oe,itemHighlightAttrs as ae}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{groupPluginItems as re,stripItemByUrl as le,insertFoldersBeforeSystem as ce,pruneEmptySynthesisedFolders as de,MANAGE_PLUGINS_URL as ue}from"./sidebar-grouping.js";function g(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const me=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];function z(t,i){return i?!t||!t.length?!1:t.includes(i)?!0:["read","create","update","delete"].some(n=>t.includes(`${i}.${n}`)):!0}function J(t,i,n=null){const e=[];for(const s of t){if(s.hidden||s.permission&&!z(i,s.permission))continue;const a=typeof s.url=="string"?s.url:"";if(n&&a.startsWith("#/")&&!se(n,a.slice(1)))continue;const r=Array.isArray(s.items)&&s.items.length>0,o=r?J(s.items,i,n):[];r&&!o.length&&!a||e.push({...s,items:o})}return e}function fe(t,i){if(!i||!i.length)return t;const n=(e,s)=>{for(const a of e){if((a.text||"").toLowerCase()===s.toLowerCase())return a;if(Array.isArray(a.items)){const r=n(a.items,s);if(r)return r}}return null};for(const e of i)if(e.parent){const s=n(t,e.parent);s?(s.items=Array.isArray(s.items)?s.items:[],s.items.push(e.item)):t.push(e.item)}else t.push(e.item);return t}async function ge(t){if(!z(t,"projects"))return[];let i=[];try{i=await f.projects.list()}catch{return[]}const n={};await Promise.all(i.map(async s=>{try{const a=await f.projects.artefacts(s.slug);n[s.slug]=a}catch{n[s.slug]={}}}));const e=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return i.map(s=>{const r="#/projects/"+encodeURIComponent(s.slug),o=n[s.slug]||{},c=[{text:"Overview",url:r,icon:s.icon||"folder"}];for(const d of e){const y=o[d.key],b=Array.isArray(y)?y.length:0;b<=0||c.push({text:`${d.text} (${b})`,url:r+d.path,icon:d.icon})}c.push({text:"Settings",url:r+"/settings",icon:"settings"});const m=s.enabled===!1;return{text:s.name||s.slug,icon:s.icon||"folder",items:c,...m&&{colour:"neutral",badge:{text:"off"}}}})}function pe(t,i){for(const n of t)if((n.text||"").toLowerCase()==="projects"){n.items=Array.isArray(n.items)?n.items.slice():[],n.items.push(...i);return}}function Y(t,i){const n=Z.includes(i.tone)?i.tone:"info",e=i.label||String(i.count);i.details?j.set(t,i.details):j.delete(t);const s=C.some(a=>a.url===t)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${n}" data-count-for="${g(t)}"${s} data-badge-label="${g(e)}" aria-label="${g(e)}">${g(String(i.count))}</span>`}function Q(t){for(const i of t.querySelectorAll("details")){const n=i.querySelector(":scope > summary");if(!n)continue;let e=0,s=!1;const a=[];for(const d of i.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const y=Number(d.textContent)||0;y&&(e+=y,s||(s=d.classList.contains("sidebar-badge--danger")),a.push({text:d.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:d.dataset.badgeLabel,tone:d.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const r=`folder:${i.dataset.stateKey||n.textContent.trim()}`;let o=n.querySelector(":scope > .sidebar-badge--sum");if(!e){o?.remove(),j.delete(r);continue}o||(o=document.createElement("span"),n.querySelector(".sidebar-text")?.after(o));const c=n.querySelector(".sidebar-text")?.textContent.trim()||"This folder",m=a.map(d=>`${d.text}: ${d.meta}`).join(" \xB7 ");o.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${s?"danger":"info"}`,o.dataset.countFor=r,o.dataset.badgeLabel=m,o.setAttribute("aria-label",m),o.textContent=String(e),j.set(r,{title:c,items:a})}}const j=new Map;function he(t,i){if(!t.url||!i)return"";const n=i[t.url];return!n||!(n.count>0)?"":Y(t.url,n)}const Z=["info","attention","danger","live"],be={"#/pages":["page","pages"],"#/media":["file in the media library","files in the media library"],"#/menus":["menu","menus"],"#/context-menus":["context menu","context menus"],"#/theme":["element override in the theme","element overrides in the theme"],"#/collections":["collection","collections"],"#/forms":["form","forms"],"#/views":["view","views"],"#/actions":["action","actions"],"#/blocks":["block","blocks"],"#/components":["component","components"],"#/users":["user","users"],"#/plugins":["plugin installed","plugins installed"],"#/system/notifications":["unread notification","unread notifications"]};function _(t,{fallbackLabel:i="",tone:n="info"}={}){const e=Number(typeof t=="object"&&t!==null?t.count:t);if(!Number.isFinite(e)||e<0)return null;const s=typeof t?.label=="string"&&t.label||(i?`${e} - ${i}`:String(e)),a={count:Math.round(e),label:s,tone:Z.includes(t?.tone)?t.tone:n};return t?.details&&typeof t.details=="object"&&(a.details=t.details),a}function ye(t){const i=(t.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),n=(t.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${n?`project-${decodeURIComponent(n[1])}.`:""}${i}`}function ee(t,i){if(t&&t.type==="separator")return'<hr class="sidebar-divider">';if(t&&t.type==="spacer"){const b=["xs","sm","md","lg","xl","flex"].includes(t.size)?t.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${g(b)}" aria-hidden="true"></div>`}const n=t.icon?`<span data-icon="${g(t.icon)}"></span>`:"",e=g(t.text||""),s=t.url?g(t.url):"",a=Array.isArray(t.items)&&t.items.length>0,r=t.colour?G(t.colour):"",o=ae(t),c=[o.hover&&` data-menu-item-hover="${g(o.hover)}"`,o.active&&` data-menu-item-active="${g(o.active)}"`].filter(Boolean).join(""),m=[r&&`color:${g(r)}`,o.accent&&`--dm-menu-highlight:${g(o.accent)}`].filter(Boolean).join(";"),d=m?` style="${m}"`:"";let y="";if(t.badge&&t.badge.text!=null&&t.badge.text!==""){const b=t.badge.variant?G(t.badge.variant):"";y=`<span class="dm-menu-badge"${b?` style="background:${g(b)};color:var(--dm-text-inverse,#fff)"`:""}>${g(String(t.badge.text))}</span>`}if(a){const b=ye(t),w=S.get(b)!==!1,A=t.items.map(L=>ee(L,i)).join("");return`<details data-state-key="${g(b)}"${w?" open":""}>
2
- <summary${d}>${n} <span class="sidebar-text">${e}</span>${y}</summary>
3
- <div class="sidebar-children">${A}</div>
4
- </details>`}return`<a href="${s}" class="sidebar-link" data-url="${s}"${d}${c}>${n} <span class="sidebar-text">${e}</span>${he(t,i)}${y}</a>`}async function xe(t=[],i=[]){const n=p=>p.then(x=>Array.isArray(x)?x.length:Array.isArray(x?.entries)?x.entries.length:0).catch(()=>0),e=(p,x)=>z(i,p)?x():Promise.resolve(0),[s,a,r,o,c,m,d,y,b,w,A,L,U,T]=await Promise.all([e("pages",()=>n(f.pages.list())),e("media",()=>n(f.media.list())),e("menus",()=>n(f.menus.list())),e("context-menus",()=>n(f.contextMenus.list())),e("theme",()=>f.theme.get().then(p=>(p?.overrides||[]).length).catch(()=>0)),e("collections",()=>n(f.collections.list())),e("forms",()=>n(f.forms.list())),e("views",()=>n(f.views.list())),e("actions",()=>n(f.actions.list())),e("blocks",()=>n(f.blocks.list())),e("components",()=>n(f.components.list())),e("users",()=>n(f.users.list())),e("plugins",()=>n(f.plugins.list())),e("notifications",()=>f.system?.notifications?.unreadCount?.().then(p=>p?.count??0).catch(()=>0)??Promise.resolve(0))]);C=Ce(t);const k={};await Promise.all(C.map(async p=>{const x=await te(p);x&&(k[p.url]=x)}));const F={"#/pages":s,"#/media":a,"#/menus":r,"#/context-menus":o,"#/theme":c,"#/collections":m,"#/forms":d,"#/views":y,"#/actions":b,"#/blocks":w,"#/components":A,"#/users":L,"#/plugins":U,"#/system/notifications":T},N={};for(const[p,x]of Object.entries(F)){const[B,O]=be[p]??["item","items"],v=_(x);v&&(N[p]={...v,label:`${v.count} ${v.count===1?B:O}`})}return{...N,...k}}let C=[];const ve="dm:sidebar-count",$e=6e4;function Ce(t=[]){const i=[],n=e=>{if(e){e.url&&typeof e.countUrl=="string"&&e.countUrl&&i.push({url:e.url,countUrl:e.countUrl,text:e.text||"",countLabel:e.countLabel||""});for(const s of[...e.children||[],...e.items||[]])n(s)}};for(const e of t??[])n(e?.item??e);return i}async function te(t){try{const i=new Date,n=`${i.getFullYear()}-${String(i.getMonth()+1).padStart(2,"0")}-${String(i.getDate()).padStart(2,"0")}`,e=t.countUrl+(t.countUrl.includes("?")?"&":"?")+`today=${n}`;return _(await H.get(e),{fallbackLabel:t.countLabel||t.text})}catch{return null}}function ne(t,i,n){const e=[...t.querySelectorAll("a.sidebar-link")].find(a=>a.dataset.url===i);if(!e)return;const s=e.querySelector(".sidebar-badge[data-count-for]");if(!n||!(n.count>0))s?.remove();else{const a=document.createRange();a.selectNodeContents(e);const r=a.createContextualFragment(Y(i,n)).firstElementChild;s?s.replaceWith(r):e.querySelector(".sidebar-text")?.after(r)}W.set(i,n),Q(t)}const W=new Map;async function D(t,i=null){const n=i?C.filter(e=>e.url===i):C;await Promise.all(n.map(async e=>{const s=await te(e),a=W.get(e.url);JSON.stringify(a??null)!==JSON.stringify(s)&&ne(t,e.url,s)}))}function we(t){t.dataset.liveCounts||(t.dataset.liveCounts="1",window.addEventListener(ve,i=>{const n=i.detail||{};if(n.url)if(n.count!==void 0){const e=C.find(s=>s.url===n.url);ne(t,n.url,_(n,{fallbackLabel:e?.countLabel||e?.text||""}))}else D(t,n.url)}),setInterval(()=>{!document.hidden&&document.body.contains(t)&&D(t)},$e),document.addEventListener("visibilitychange",()=>{document.hidden||D(t)}),Ae(t))}function Se(t,i,n){const e=(r,o,c)=>{const m=document.createElement(r);return o&&(m.className=o),c!=null&&(m.textContent=String(c)),m};if(!n){t.textContent=i;return}t.appendChild(e("strong","dm-badge-pop-title",n.title||i)),(n.rows?.length||!n.items?.length)&&t.appendChild(e("p","dm-badge-pop-sub",i));const s=Array.isArray(n.rows)?n.rows.slice(0,6):[];if(s.length){const r=e("dl","dm-badge-pop-rows");for(const o of s)r.appendChild(e("dt",null,o.label)),r.appendChild(e("dd",o.tone==="danger"?"is-danger":null,o.value));t.appendChild(r)}const a=Array.isArray(n.items)?n.items.slice(0,6):[];if(a.length){const r=e("ul","dm-badge-pop-items");for(const o of a){const c=e("li",o.tone==="danger"?"is-danger":null);c.appendChild(e("span","dm-badge-pop-text",o.text)),o.meta&&c.appendChild(e("span","dm-badge-pop-meta",o.meta)),r.appendChild(c)}t.appendChild(r)}else n.empty&&t.appendChild(e("p","dm-badge-pop-empty",n.empty))}function Ae(t){let i=null,n=null;const e=()=>{clearTimeout(n),i?.remove(),i=null},s=a=>{e();const r=a?.dataset.badgeLabel;r&&(n=setTimeout(()=>{i=document.createElement("div"),i.className="dm-help-pop dm-badge-pop",i.setAttribute("role","tooltip"),Se(i,r,j.get(a.dataset.countFor)),document.body.appendChild(i);const o=a.getBoundingClientRect(),c=i.getBoundingClientRect(),m=Math.min(o.right+10,innerWidth-c.width-8);i.style.left=`${Math.max(8,m)}px`,i.style.top=`${Math.max(8,Math.min(o.top+o.height/2-c.height/2,innerHeight-c.height-8))}px`},150))};t.addEventListener("mouseover",a=>{const r=a.target.closest?.(".sidebar-badge[data-badge-label]");r&&s(r)}),t.addEventListener("mouseout",a=>{a.target.closest?.(".sidebar-badge[data-badge-label]")&&e()}),t.addEventListener("focusin",a=>{const r=a.target.closest?.("a, summary")?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");r?s(r):e()}),t.addEventListener("focusout",e),window.addEventListener("scroll",e,!0),window.addEventListener("hashchange",e)}async function Le(){try{const t=await f.settings.get();return t?.adminBrand?.title||t?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:t,permissions:i,adminScope:n=null}){const e=$(t).get(0);if(!e)return;let s,a=null,r=null,o=null,c=null,m=null;try{const l=await f.get("/sidebar/menu");s=Array.isArray(l?.items)?l.items:null,a=l?.variant||null,r=l?.position||null,o=l?.style||null,c=l?.float||null,m=l?.highlight||null}catch{s=null}(!s||!s.length)&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),s=me.slice());let d=[];try{d=await H.get("/api/sidebar/registered-items")||[]}catch{}s=le(s,ue);const{toolsFolder:y,managePluginsItem:b,parented:w}=re(d);s=fe(s,w),s=ce(s,[y,b]);const A=await ge(i);A.length&&pe(s,A),s=J(s,i,n),s=de(s);const[L,U]=await Promise.all([xe(d.filter(l=>z(i,l?.item?.permission)),i).catch(()=>({})),Le()]),T=a?` dm-admin-sidebar--${g(a)}`:"";function k(l,h="px"){if(l==null)return l;const u=String(l).trim();return/^\d+(\.\d+)?$/.test(u)?`${u}${h}`:u}let F="";if(o){const l=[];o.fontFamily&&l.push(`font-family: ${o.fontFamily}, sans-serif`),o.fontSize&&l.push(`font-size: ${k(o.fontSize)}`),o.fontWeight&&l.push(`font-weight: ${o.fontWeight}`),o.letterSpacing&&l.push(`letter-spacing: ${k(o.letterSpacing,"em")}`);const h=[];if(l.length&&h.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${l.join("; ")} }`),o.iconSize){const u=k(o.iconSize);h.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${u} !important; height: ${u} !important; }`)}h.length&&(F=`<style data-admin-sidebar-style>${h.join(" ")}</style>`)}const N=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${g(U)}</div>`,p=s.some(l=>l&&l.type==="spacer"&&l.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",x=`${F}<nav class="dm-admin-sidebar${T}${p}">${N}${s.map(l=>ee(l,L)).join("")}</nav>`,B=document.createRange();B.selectNodeContents(e);const O=B.createContextualFragment(x);e.replaceChildren(O),Domma.icons.scan(e);for(const l of C)W.set(l.url,L[l.url]??null);Q(e),we(e);const v=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(v)&&v>=180&&v<=480&&(e.style.width=v+"px"),e.removeAttribute("data-menu-hover"),e.removeAttribute("data-menu-active"),e.style.removeProperty("--dm-menu-highlight"),oe(e,{highlight:m});const E=document.querySelector(".dashboard-wrapper");if(["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(l=>{e.style[l]=""}),e.classList.remove("dm-menu-floating"),E&&(E.style.paddingLeft="",E.style.paddingRight=""),r==="floating"){ie(e,{position:"floating",float:c||{}});const h=c&&c.anchor||"TL",u=Number.isFinite(Number(c?.offsetX))?Number(c.offsetX):16,P=e.getBoundingClientRect().width||v||260;if(E){const I=Math.round(P+u*2)+"px";h==="R"||h==="TR"||h==="BR"?E.style.paddingRight=I:E.style.paddingLeft=I}}const R=document.createElement("div");R.className="dm-admin-sidebar-handle",R.setAttribute("aria-label","Resize sidebar"),e.appendChild(R),e.querySelectorAll("details").forEach(l=>{l.addEventListener("toggle",function(){const h=this.getAttribute("data-state-key");h&&S.set(h,this.open);const u=this.querySelector(":scope > .sidebar-children");if(u)if(this.open){const P=u.scrollHeight;u.style.maxHeight="0",requestAnimationFrame(()=>{u.style.maxHeight=P+"px",u.addEventListener("transitionend",function I(){u.style.maxHeight="none",u.removeEventListener("transitionend",I)})})}else{const P=u.scrollHeight;u.style.maxHeight=P+"px",requestAnimationFrame(()=>{u.style.maxHeight="0"})}})});let q=!1,X=0,K=0;R.addEventListener("mousedown",l=>{q=!0,X=l.clientX,K=e.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",l.preventDefault()}),document.addEventListener("mousemove",l=>{if(!q)return;const h=Math.max(180,Math.min(480,K+(l.clientX-X)));e.style.width=h+"px"}),document.addEventListener("mouseup",()=>{q&&(q=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(e.style.width,10)))});function V(){const l=location.hash||"#/";$(e).find(".sidebar-link").removeClass("active"),$(e).find(`.sidebar-link[data-url="${l}"]`).addClass("active")}V(),M.subscribe("router:afterChange",V)}
1
+ import{api as f}from"../api.js";import{isPathAllowed as ee}from"./admin-scope.js";import{colourToCss as X,applyFloat as te,applyHighlight as ne,itemHighlightAttrs as se}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{placeTools as ie,ensureLockedTools as oe,managePluginsItem as ae,stripItemByUrl as re,insertFoldersBeforeSystem as le,pruneEmptyToolsFolder as ce,MANAGE_PLUGINS_URL as de}from"./sidebar-grouping.js";function p(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const ue=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];function q(t,s){return s?!t||!t.length?!1:t.includes(s)?!0:["read","create","update","delete"].some(n=>t.includes(`${s}.${n}`)):!0}function K(t,s,n=null){const e=[];for(const i of t){if(i.hidden||i.permission&&!q(s,i.permission))continue;const r=typeof i.url=="string"?i.url:"";if(n&&r.startsWith("#/")&&!ee(n,r.slice(1)))continue;const l=Array.isArray(i.items)&&i.items.length>0,o=l?K(i.items,s,n):[];l&&!o.length&&!r||e.push({...i,items:o})}return e}async function me(t){if(!q(t,"projects"))return[];let s=[];try{s=await f.projects.list()}catch{return[]}const n={};await Promise.all(s.map(async i=>{try{const r=await f.projects.artefacts(i.slug);n[i.slug]=r}catch{n[i.slug]={}}}));const e=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return s.map(i=>{const l="#/projects/"+encodeURIComponent(i.slug),o=n[i.slug]||{},c=[{text:"Overview",url:l,icon:i.icon||"folder"}];for(const d of e){const x=o[d.key],h=Array.isArray(x)?x.length:0;h<=0||c.push({text:`${d.text} (${h})`,url:l+d.path,icon:d.icon})}c.push({text:"Settings",url:l+"/settings",icon:"settings"});const m=i.enabled===!1;return{text:i.name||i.slug,icon:i.icon||"folder",items:c,...m&&{colour:"neutral",badge:{text:"off"}}}})}function fe(t,s){for(const n of t)if(!n.hidden&&(n.key==="projects"||!n.key&&(n.text||"").toLowerCase()==="projects")){n.items=Array.isArray(n.items)?n.items.slice():[],n.items.push(...s);return}}function V(t,s){const n=J.includes(s.tone)?s.tone:"info",e=s.label||String(s.count);s.details?F.set(t,s.details):F.delete(t);const i=A.some(r=>r.url===t)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${n}" data-count-for="${p(t)}"${i} data-badge-label="${p(e)}" aria-label="${p(e)}">${p(String(s.count))}</span>`}function G(t){for(const s of t.querySelectorAll("details")){const n=s.querySelector(":scope > summary");if(!n)continue;let e=0,i=!1;const r=[];for(const d of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const x=Number(d.textContent)||0;x&&(e+=x,i||(i=d.classList.contains("sidebar-badge--danger")),r.push({text:d.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:d.dataset.badgeLabel,tone:d.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const l=`folder:${s.dataset.stateKey||n.textContent.trim()}`;let o=n.querySelector(":scope > .sidebar-badge--sum");if(!e){o?.remove(),F.delete(l);continue}o||(o=document.createElement("span"),n.querySelector(".sidebar-text")?.after(o));const c=n.querySelector(".sidebar-text")?.textContent.trim()||"This folder",m=r.map(d=>`${d.text}: ${d.meta}`).join(" \xB7 ");o.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${i?"danger":"info"}`,o.dataset.countFor=l,o.dataset.badgeLabel=m,o.setAttribute("aria-label",m),o.textContent=String(e),F.set(l,{title:c,items:r})}}const F=new Map;function ge(t,s){if(!t.url||!s)return"";const n=s[t.url];return!n||!(n.count>0)?"":V(t.url,n)}const J=["info","attention","danger","live"],pe={"#/pages":["page","pages"],"#/media":["file in the media library","files in the media library"],"#/menus":["menu","menus"],"#/context-menus":["context menu","context menus"],"#/theme":["element override in the theme","element overrides in the theme"],"#/collections":["collection","collections"],"#/forms":["form","forms"],"#/views":["view","views"],"#/actions":["action","actions"],"#/blocks":["block","blocks"],"#/components":["component","components"],"#/users":["user","users"],"#/plugins":["plugin installed","plugins installed"],"#/system/notifications":["unread notification","unread notifications"]};function I(t,{fallbackLabel:s="",tone:n="info"}={}){const e=Number(typeof t=="object"&&t!==null?t.count:t);if(!Number.isFinite(e)||e<0)return null;const i=typeof t?.label=="string"&&t.label||(s?`${e} - ${s}`:String(e)),r={count:Math.round(e),label:i,tone:J.includes(t?.tone)?t.tone:n};return t?.details&&typeof t.details=="object"&&(r.details=t.details),r}function he(t){const s=(t.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),n=(t.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${n?`project-${decodeURIComponent(n[1])}.`:""}${s}`}function Y(t,s){if(t&&t.type==="separator")return'<hr class="sidebar-divider">';if(t&&t.type==="spacer"){const h=["xs","sm","md","lg","xl","flex"].includes(t.size)?t.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${p(h)}" aria-hidden="true"></div>`}const n=t.icon?`<span data-icon="${p(t.icon)}"></span>`:"",e=p(t.text||""),i=t.url?p(t.url):"",r=Array.isArray(t.items)&&t.items.length>0,l=t.colour?X(t.colour):"",o=se(t),c=[o.hover&&` data-menu-item-hover="${p(o.hover)}"`,o.active&&` data-menu-item-active="${p(o.active)}"`].filter(Boolean).join(""),m=[l&&`color:${p(l)}`,o.accent&&`--dm-menu-highlight:${p(o.accent)}`].filter(Boolean).join(";"),d=m?` style="${m}"`:"";let x="";if(t.badge&&t.badge.text!=null&&t.badge.text!==""){const h=t.badge.variant?X(t.badge.variant):"";x=`<span class="dm-menu-badge"${h?` style="background:${p(h)};color:var(--dm-text-inverse,#fff)"`:""}>${p(String(t.badge.text))}</span>`}if(r){const h=he(t),L=S.get(h)!==!1,E=t.items.map(w=>Y(w,s)).join("");return`<details data-state-key="${p(h)}"${L?" open":""}>
2
+ <summary${d}>${n} <span class="sidebar-text">${e}</span>${x}</summary>
3
+ <div class="sidebar-children">${E}</div>
4
+ </details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${d}${c}>${n} <span class="sidebar-text">${e}</span>${ge(t,s)}${x}</a>`}async function be(t=[],s=[]){const n=b=>b.then(g=>Array.isArray(g)?g.length:Array.isArray(g?.entries)?g.entries.length:0).catch(()=>0),e=(b,g)=>q(s,b)?g():Promise.resolve(0),[i,r,l,o,c,m,d,x,h,L,E,w,N,T]=await Promise.all([e("pages",()=>n(f.pages.list())),e("media",()=>n(f.media.list())),e("menus",()=>n(f.menus.list())),e("context-menus",()=>n(f.contextMenus.list())),e("theme",()=>f.theme.get().then(b=>(b?.overrides||[]).length).catch(()=>0)),e("collections",()=>n(f.collections.list())),e("forms",()=>n(f.forms.list())),e("views",()=>n(f.views.list())),e("actions",()=>n(f.actions.list())),e("blocks",()=>n(f.blocks.list())),e("components",()=>n(f.components.list())),e("users",()=>n(f.users.list())),e("plugins",()=>n(f.plugins.list())),e("notifications",()=>f.system?.notifications?.unreadCount?.().then(b=>b?.count??0).catch(()=>0)??Promise.resolve(0))]);A=ve(t);const B={};await Promise.all(A.map(async b=>{const g=await Q(b);g&&(B[b.url]=g)}));const z={"#/pages":i,"#/media":r,"#/menus":l,"#/context-menus":o,"#/theme":c,"#/collections":m,"#/forms":d,"#/views":x,"#/actions":h,"#/blocks":L,"#/components":E,"#/users":w,"#/plugins":N,"#/system/notifications":T},j={};for(const[b,g]of Object.entries(z)){const[C,k]=pe[b]??["item","items"],v=I(g);v&&(j[b]={...v,label:`${v.count} ${v.count===1?C:k}`})}return{...j,...B}}let A=[];const ye="dm:sidebar-count",xe=6e4;function ve(t=[]){const s=[],n=e=>{if(e){e.url&&typeof e.countUrl=="string"&&e.countUrl&&s.push({url:e.url,countUrl:e.countUrl,text:e.text||"",countLabel:e.countLabel||""});for(const i of[...e.children||[],...e.items||[]])n(i)}};for(const e of t??[])n(e?.item??e);return s}async function Q(t){try{const s=new Date,n=`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(s.getDate()).padStart(2,"0")}`,e=t.countUrl+(t.countUrl.includes("?")?"&":"?")+`today=${n}`;return I(await H.get(e),{fallbackLabel:t.countLabel||t.text})}catch{return null}}function Z(t,s,n){const e=[...t.querySelectorAll("a.sidebar-link")].find(r=>r.dataset.url===s);if(!e)return;const i=e.querySelector(".sidebar-badge[data-count-for]");if(!n||!(n.count>0))i?.remove();else{const r=document.createRange();r.selectNodeContents(e);const l=r.createContextualFragment(V(s,n)).firstElementChild;i?i.replaceWith(l):e.querySelector(".sidebar-text")?.after(l)}U.set(s,n),G(t)}const U=new Map;async function O(t,s=null){const n=s?A.filter(e=>e.url===s):A;await Promise.all(n.map(async e=>{const i=await Q(e),r=U.get(e.url);JSON.stringify(r??null)!==JSON.stringify(i)&&Z(t,e.url,i)}))}function $e(t){t.dataset.liveCounts||(t.dataset.liveCounts="1",window.addEventListener(ye,s=>{const n=s.detail||{};if(n.url)if(n.count!==void 0){const e=A.find(i=>i.url===n.url);Z(t,n.url,I(n,{fallbackLabel:e?.countLabel||e?.text||""}))}else O(t,n.url)}),setInterval(()=>{!document.hidden&&document.body.contains(t)&&O(t)},xe),document.addEventListener("visibilitychange",()=>{document.hidden||O(t)}),Se(t))}function Ce(t,s,n){const e=(l,o,c)=>{const m=document.createElement(l);return o&&(m.className=o),c!=null&&(m.textContent=String(c)),m};if(!n){t.textContent=s;return}t.appendChild(e("strong","dm-badge-pop-title",n.title||s)),(n.rows?.length||!n.items?.length)&&t.appendChild(e("p","dm-badge-pop-sub",s));const i=Array.isArray(n.rows)?n.rows.slice(0,6):[];if(i.length){const l=e("dl","dm-badge-pop-rows");for(const o of i)l.appendChild(e("dt",null,o.label)),l.appendChild(e("dd",o.tone==="danger"?"is-danger":null,o.value));t.appendChild(l)}const r=Array.isArray(n.items)?n.items.slice(0,6):[];if(r.length){const l=e("ul","dm-badge-pop-items");for(const o of r){const c=e("li",o.tone==="danger"?"is-danger":null);c.appendChild(e("span","dm-badge-pop-text",o.text)),o.meta&&c.appendChild(e("span","dm-badge-pop-meta",o.meta)),l.appendChild(c)}t.appendChild(l)}else n.empty&&t.appendChild(e("p","dm-badge-pop-empty",n.empty))}function Se(t){let s=null,n=null;const e=()=>{clearTimeout(n),s?.remove(),s=null},i=r=>{e();const l=r?.dataset.badgeLabel;l&&(n=setTimeout(()=>{s=document.createElement("div"),s.className="dm-help-pop dm-badge-pop",s.setAttribute("role","tooltip"),Ce(s,l,F.get(r.dataset.countFor)),document.body.appendChild(s);const o=r.getBoundingClientRect(),c=s.getBoundingClientRect(),m=Math.min(o.right+10,innerWidth-c.width-8);s.style.left=`${Math.max(8,m)}px`,s.style.top=`${Math.max(8,Math.min(o.top+o.height/2-c.height/2,innerHeight-c.height-8))}px`},150))};t.addEventListener("mouseover",r=>{const l=r.target.closest?.(".sidebar-badge[data-badge-label]");l&&i(l)}),t.addEventListener("mouseout",r=>{r.target.closest?.(".sidebar-badge[data-badge-label]")&&e()}),t.addEventListener("focusin",r=>{const l=r.target.closest?.("a, summary")?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");l?i(l):e()}),t.addEventListener("focusout",e),window.addEventListener("scroll",e,!0),window.addEventListener("hashchange",e)}async function we(){try{const t=await f.settings.get();return t?.adminBrand?.title||t?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:t,permissions:s,adminScope:n=null}){const e=$(t).get(0);if(!e)return;let i,r=null,l=null,o=null,c=null,m=null;try{const a=await f.get("/sidebar/menu");i=Array.isArray(a?.items)?a.items:null,r=a?.variant||null,l=a?.position||null,o=a?.style||null,c=a?.float||null,m=a?.highlight||null}catch{i=null}(!i||!i.some(a=>a&&(!a.hidden||a.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),i=ue.slice());let d=[];try{d=await H.get("/api/sidebar/registered-items")||[]}catch{}i=re(i,de),i=ie(i,d),i=oe(i),i=le(i,[ae()]);const x=await me(s);x.length&&fe(i,x),i=K(i,s,n),i=ce(i);const[h,L]=await Promise.all([be(d.filter(a=>q(s,a?.item?.permission)),s).catch(()=>({})),we()]),E=r?` dm-admin-sidebar--${p(r)}`:"";function w(a,y="px"){if(a==null)return a;const u=String(a).trim();return/^\d+(\.\d+)?$/.test(u)?`${u}${y}`:u}let N="";if(o){const a=[];o.fontFamily&&a.push(`font-family: ${o.fontFamily}, sans-serif`),o.fontSize&&a.push(`font-size: ${w(o.fontSize)}`),o.fontWeight&&a.push(`font-weight: ${o.fontWeight}`),o.letterSpacing&&a.push(`letter-spacing: ${w(o.letterSpacing,"em")}`);const y=[];if(a.length&&y.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${a.join("; ")} }`),o.iconSize){const u=w(o.iconSize);y.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${u} !important; height: ${u} !important; }`)}y.length&&(N=`<style data-admin-sidebar-style>${y.join(" ")}</style>`)}const T=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${p(L)}</div>`,B=i.some(a=>a&&a.type==="spacer"&&a.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",z=`${N}<nav class="dm-admin-sidebar${E}${B}">${T}${i.map(a=>Y(a,h)).join("")}</nav>`,j=document.createRange();j.selectNodeContents(e);const b=j.createContextualFragment(z);e.replaceChildren(b),Domma.icons.scan(e);for(const a of A)U.set(a.url,h[a.url]??null);G(e),$e(e);const g=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(g)&&g>=180&&g<=480&&(e.style.width=g+"px"),e.removeAttribute("data-menu-hover"),e.removeAttribute("data-menu-active"),e.style.removeProperty("--dm-menu-highlight"),ne(e,{highlight:m});const C=document.querySelector(".dashboard-wrapper");if(["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(a=>{e.style[a]=""}),e.classList.remove("dm-menu-floating"),C&&(C.style.paddingLeft="",C.style.paddingRight=""),l==="floating"){te(e,{position:"floating",float:c||{}});const y=c&&c.anchor||"TL",u=Number.isFinite(Number(c?.offsetX))?Number(c.offsetX):16,P=e.getBoundingClientRect().width||g||260;if(C){const R=Math.round(P+u*2)+"px";y==="R"||y==="TR"||y==="BR"?C.style.paddingRight=R:C.style.paddingLeft=R}}const k=document.createElement("div");k.className="dm-admin-sidebar-handle",k.setAttribute("aria-label","Resize sidebar"),e.appendChild(k),e.querySelectorAll("details").forEach(a=>{a.addEventListener("toggle",function(){const y=this.getAttribute("data-state-key");y&&S.set(y,this.open);const u=this.querySelector(":scope > .sidebar-children");if(u)if(this.open){const P=u.scrollHeight;u.style.maxHeight="0",requestAnimationFrame(()=>{u.style.maxHeight=P+"px",u.addEventListener("transitionend",function R(){u.style.maxHeight="none",u.removeEventListener("transitionend",R)})})}else{const P=u.scrollHeight;u.style.maxHeight=P+"px",requestAnimationFrame(()=>{u.style.maxHeight="0"})}})});let v=!1,_=0,W=0;k.addEventListener("mousedown",a=>{v=!0,_=a.clientX,W=e.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",a.preventDefault()}),document.addEventListener("mousemove",a=>{if(!v)return;const y=Math.max(180,Math.min(480,W+(a.clientX-_)));e.style.width=y+"px"}),document.addEventListener("mouseup",()=>{v&&(v=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(e.style.width,10)))});function D(){const a=location.hash||"#/";$(e).find(".sidebar-link").removeClass("active"),$(e).find(`.sidebar-link[data-url="${a}"]`).addClass("active")}D(),M.subscribe("router:afterChange",D)}
@@ -46,6 +46,14 @@
46
46
  </div>
47
47
  </div>
48
48
  </div>
49
+ <div class="row mb-3">
50
+ <div class="col-6">
51
+ <label class="form-label" for="field-admin-home">Admin home</label>
52
+ <select id="field-admin-home" class="form-select"></select>
53
+ <small class="form-hint">The screen the admin opens on, and where Dashboard goes. Anyone without the
54
+ permission for it gets the built-in dashboard; a confined role keeps its own home (Roles &rsaquo; Admin area).</small>
55
+ </div>
56
+ </div>
49
57
  <div class="row mt-3">
50
58
  <div class="col-3">
51
59
  <label class="form-label">Default Spacer Size (px)</label>
@@ -1,5 +1,5 @@
1
- import{api as k}from"../api.js";import{colourToCss as xt,HIGHLIGHT_HOVER as rt,HIGHLIGHT_ACTIVE as pt,HIGHLIGHT_HOVER_LABELS as mt,HIGHLIGHT_ACTIVE_LABELS as ft}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{openIconPicker as At}from"../lib/card-builder.js";import{applyDrop as _t}from"../lib/menu-tree.js";let kt=1;const G=()=>"i_"+kt++,It=760;function ut(t,s=null,l=[]){for(const i of t||[]){const n=G();if(i&&i.type==="separator"){l.push({id:n,parentId:s,type:"separator"});continue}if(i&&i.type==="spacer"){l.push({id:n,parentId:s,type:"spacer",size:i.size||"md"});continue}l.push({id:n,parentId:s,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||"",highlight:i.highlight?{...i.highlight}:null}),Array.isArray(i.items)&&i.items.length&&ut(i.items,n,l)}return l}function Ht(t){const s=new Map;for(const i of t)s.has(i.parentId)||s.set(i.parentId,[]),s.get(i.parentId).push(i);function l(i){return(s.get(i)||[]).map(n=>{if(n.type==="separator")return{type:"separator"};if(n.type==="spacer")return{type:"spacer",...n.size&&n.size!=="md"&&{size:n.size}};const o=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},...n.highlight&&(n.highlight.hover||n.highlight.active||n.highlight.accent)?{highlight:{...n.highlight.hover&&{hover:n.highlight.hover},...n.highlight.active&&{active:n.highlight.active},...n.highlight.accent&&{accent:n.highlight.accent}}}:{},...o.length&&{items:o}}})}return l(null)}function h(t){return String(t||"").replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;")}function St(t,s){let l=0,i=s.find(n=>n.id===t);for(;i&&i.parentId;)l++,i=s.find(n=>n.id===i.parentId);return l}function ht(t,s){const{top:l,height:i}=t.getBoundingClientRect(),n=i?(s-l)/i:.5;return n<.25?"above":n>.75?"below":"into"}const gt=["me-drop-above","me-drop-below","me-drop-into"];function st(t){t.get(0).querySelectorAll(".menu-tree-row").forEach(s=>s.classList.remove(...gt))}function Ot(t,s,l){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",p=>{i=n.dataset.id,n.style.opacity="0.45",p.dataTransfer.effectAllowed="move";try{p.dataTransfer.setData("text/plain",i)}catch{}}),n.addEventListener("dragend",()=>{n.style.opacity="",n.draggable=!1,i=null,st(t)}),n.addEventListener("dragover",p=>{if(!i||i===n.dataset.id)return;p.preventDefault(),p.dataTransfer.dropEffect="move";const w=ht(n,p.clientY);st(t),n.classList.add(`me-drop-${w}`)}),n.addEventListener("dragleave",()=>n.classList.remove(...gt)),n.addEventListener("drop",p=>{p.preventDefault();const w=n.dataset.id,A=ht(n,p.clientY);if(st(t),!i)return;const y=_t(s(),i,w,A);i=null,y&&l()})})}function jt(t,s,l=()=>""){t.html("");function i(n){for(const o of s.filter(p=>p.parentId===n)){const p=St(o.id,s);if(o.type==="separator"){t.append(`
2
- <div class="menu-tree-row menu-tree-row--separator" data-id="${o.id}" style="margin-left:${p*28}px">
1
+ import{api as S}from"../api.js";import{colourToCss as It,HIGHLIGHT_HOVER as mt,HIGHLIGHT_ACTIVE as ut,HIGHLIGHT_HOVER_LABELS as ht,HIGHLIGHT_ACTIVE_LABELS as bt}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{openIconPicker as _t}from"../lib/card-builder.js";import{applyDrop as St}from"../lib/menu-tree.js";import{placeTools as Ht,ensureLockedTools as Ot,isLockedUrl as Lt,TOOLS_FOLDER_KEY as Tt}from"../lib/sidebar-grouping.js";import{flatten as jt,nest as Ct,uid as it}from"../lib/menu-rows.js";const Et=760,Rt="admin-sidebar";function g(t){return String(t||"").replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;")}function Nt(t,c){let r=0,i=c.find(a=>a.id===t);for(;i&&i.parentId;)r++,i=c.find(a=>a.id===i.parentId);return r}function gt(t,c){const{top:r,height:i}=t.getBoundingClientRect(),a=i?(c-r)/i:.5;return a<.25?"above":a>.75?"below":"into"}const vt=["me-drop-above","me-drop-below","me-drop-into"];function rt(t){t.get(0).querySelectorAll(".menu-tree-row").forEach(c=>c.classList.remove(...vt))}function Ft(t,c,r){let i=null;t.get(0).querySelectorAll(".menu-tree-row").forEach(a=>{const o=a.querySelector(".menu-tree-grip");a.draggable=!1,o&&(o.addEventListener("mousedown",()=>{a.draggable=!0}),o.addEventListener("mouseup",()=>{a.draggable=!1})),a.addEventListener("dragstart",m=>{i=a.dataset.id,a.style.opacity="0.45",m.dataTransfer.effectAllowed="move";try{m.dataTransfer.setData("text/plain",i)}catch{}}),a.addEventListener("dragend",()=>{a.style.opacity="",a.draggable=!1,i=null,rt(t)}),a.addEventListener("dragover",m=>{if(!i||i===a.dataset.id)return;m.preventDefault(),m.dataTransfer.dropEffect="move";const p=gt(a,m.clientY);rt(t),a.classList.add(`me-drop-${p}`)}),a.addEventListener("dragleave",()=>a.classList.remove(...vt)),a.addEventListener("drop",m=>{m.preventDefault();const p=a.dataset.id,x=gt(a,m.clientY);if(rt(t),!i)return;const k=St(c(),i,p,x);if(k){const v=c().find(L=>L.id===i);v&&(v.touched=!0)}i=null,k&&r()})})}function lt(t){const c=new Set,r=new Set,i=a=>{for(const o of t.filter(m=>m.parentId===a))!o.type&&Lt(o.url)&&!r.has(o.url)&&(r.add(o.url),c.add(o.id)),i(o.id)};return i(null),c}function zt(t,c,r=()=>"",i=!1){t.html("");const a=i?lt(c):new Set;function o(m){for(const p of c.filter(x=>x.parentId===m)){const x=Nt(p.id,c);if(p.type==="separator"){t.append(`
2
+ <div class="menu-tree-row menu-tree-row--separator" data-id="${p.id}" style="margin-left:${x*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,12 +10,12 @@ import{api as k}from"../api.js";import{colourToCss as xt,HIGHLIGHT_HOVER as rt,H
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}if(o.type==="spacer"){const R=["xs","sm","md","lg","xl","flex"].map(L=>`<option value="${L}"${(o.size||"md")===L?" selected":""}>${L==="flex"?"flexible":L}</option>`).join("");t.append(`
14
- <div class="menu-tree-row menu-tree-row--spacer" data-id="${o.id}" style="margin-left:${p*28}px">
13
+ `);continue}if(p.type==="spacer"){const M=["xs","sm","md","lg","xl","flex"].map(s=>`<option value="${s}"${(p.size||"md")===s?" selected":""}>${s==="flex"?"flexible":s}</option>`).join("");t.append(`
14
+ <div class="menu-tree-row menu-tree-row--spacer" data-id="${p.id}" style="margin-left:${x*28}px">
15
15
  <span class="menu-tree-grip" data-icon="move"></span>
16
16
  <span class="me-spacer-line">
17
17
  <span class="me-spacer-label">Spacer</span>
18
- <select class="form-input me-spacer-size" data-tooltip="Spacer size - flexible fills the leftover room">${R}</select>
18
+ <select class="form-input me-spacer-size" data-tooltip="Spacer size - flexible fills the leftover room">${M}</select>
19
19
  </span>
20
20
  <button class="btn btn-sm btn-ghost me-ins-item" data-tooltip="Add item below"><span data-icon="plus"></span></button>
21
21
  <button class="btn btn-sm btn-ghost me-ins-sep" data-tooltip="Add separator below"><span data-icon="minus"></span></button>
@@ -25,23 +25,23 @@ import{api as k}from"../api.js";import{colourToCss as xt,HIGHLIGHT_HOVER as rt,H
25
25
  <button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></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
- `);continue}const w=o.badge&&(o.badge.text||o.badge.countFrom)?`<span class="dm-menu-badge">${h(String(o.badge.text||"#"))}</span>`:"",A=xt(o.colour),y=A?` style="color:${h(A)}"`:"";t.append(`
29
- <div class="menu-tree-row menu-tree-row--slim${o.hidden?" menu-tree-row--hidden":""}" data-id="${o.id}" style="margin-left:${p*28}px">
28
+ `);continue}const k=p.badge&&(p.badge.text||p.badge.countFrom)?`<span class="dm-menu-badge">${g(String(p.badge.text||"#"))}</span>`:"",v=p.tool,L=v&&(v.persisted||p.touched),V=v?v.orphan?' <span class="badge badge-secondary me-tool-badge" data-tooltip="A placeholder for a Tool that is not enabled - it returns here when it is">Not enabled</span>':` <span class="badge badge-secondary me-tool-badge" data-tooltip="${L?"A Tool, placed here by this menu":"A Tool in its default place - move or edit it to place it here"}">${g(v.source||"Tool")}${L?"":" \xB7 default"}</span>`:"",q=a.has(p.id),Z=q?'<button class="btn btn-sm btn-ghost" disabled data-tooltip="Always in the sidebar - it can be moved, not removed"><span data-icon="lock"></span></button>':v&&!v.orphan?L?'<button class="btn btn-sm btn-ghost me-reset" data-tooltip="Back to its default place"><span data-icon="rotate-ccw"></span></button>':'<button class="btn btn-sm btn-ghost" disabled data-tooltip="A Tool cannot be removed - hide it instead"><span data-icon="trash"></span></button>':'<button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>',W=It(p.colour),C=W?` style="color:${g(W)}"`:"";t.append(`
29
+ <div class="menu-tree-row menu-tree-row--slim${p.hidden?" menu-tree-row--hidden":""}${v?" menu-tree-row--tool":""}${v&&!L?" menu-tree-row--default":""}" data-id="${p.id}" style="margin-left:${x*28}px">
30
30
  <span class="menu-tree-grip" data-icon="move"></span>
31
- <span class="me-row-label"${y}>${h(o.text||"(untitled)")}${w}</span>
32
- <span class="me-row-url">${h(o.url||"")}</span>
31
+ <span class="me-row-label"${C}>${g(p.text||v?.label||"(untitled)")}${k}${V}</span>
32
+ <span class="me-row-url">${g(p.url||"")}</span>
33
33
  <button class="btn btn-sm btn-ghost me-ins-item" data-tooltip="Add item below"><span data-icon="plus"></span></button>
34
34
  <button class="btn btn-sm btn-ghost me-ins-sep" data-tooltip="Add separator below"><span data-icon="minus"></span></button>
35
35
  <button class="btn btn-sm btn-ghost me-outdent" data-tooltip="Outdent"><span data-icon="chevron-left"></span></button>
36
36
  <button class="btn btn-sm btn-ghost me-indent" data-tooltip="Indent"><span data-icon="chevron-right"></span></button>
37
37
  <button class="btn btn-sm btn-ghost me-up" data-tooltip="Move up"><span data-icon="arrow-up"></span></button>
38
38
  <button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
39
- <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>
39
+ ${q?'<button class="btn btn-sm btn-ghost" disabled data-tooltip="Always shown"><span data-icon="eye-off"></span></button>':`<button class="btn btn-sm btn-ghost me-hidden ${p.hidden?"active":""}" data-tooltip="${p.hidden?"Show":"Hide"}"><span data-icon="eye-off"></span></button>`}
40
40
  <button class="btn btn-sm btn-secondary me-edit" data-tooltip="Edit"><span data-icon="edit"></span></button>
41
- <button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
41
+ ${Z}
42
42
  </div>
43
- `),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 j(t,s){t.find(".menu-tree-row").each(function(){const l=$(this).data("id"),i=s.find(y=>y.id===l);if(!i||i.type==="separator")return;if(i.type==="spacer"){const y=$(this).find(".me-spacer-size");y.length&&(i.size=y.val());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 p=$(this).find(".me-icon");p.length&&(i.icon=p.val());const w=$(this).find(".me-vis");w.length&&(i.visibility=w.val());const A=$(this).find(".me-perm");A.length&&(i.permission=A.val())})}function Rt(t,s){const l={};for(const i of s){const n=t.querySelector(`[name="${i}"]`);n&&(l[i]=n.type==="checkbox"?n.checked:n.value)}return l}function b(t,s={},...l){const i=document.createElement(t);Object.assign(i,s);for(const n of l)n!=null&&i.append(n);return i}function I(t,s,l){const i=b("label",{className:"form-field"},b("span",{className:"form-label",textContent:t}),s);return l&&i.append(b("small",{className:"form-hint",textContent:l,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),i}function Lt(t,s){const l=[["","- Everyone -"],["public","Public - anyone, incl. logged-out"],...(s||[]).map(n=>[n.name,`${n.label||n.name} and above`]),...(s||[]).filter(n=>n.level!==0).map(n=>[`=${n.name}`,`${n.label||n.name} only`]),["private","Private - super-admin only"]];t&&!l.some(([n])=>n===t)&&l.push([t,`${t} (custom)`]);const i=P(l,t||"");return i.name="visibility",i}function P(t,s){const l=b("select",{className:"form-select"});return t.forEach(([i,n])=>l.add(new Option(n,i))),l.value=s??"",l}function it(t,s){const l=typeof s=="string"&&s.startsWith("#"),i=P([["","- none -"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],l?"__custom__":s||""),n=b("input",{type:"color",className:"form-input me-colour-hex",value:l?s:"#000000"});n.style.display=l?"":"none",i.addEventListener("change",()=>{n.style.display=i.value==="__custom__"?"":"none"});const o=I(t,i);return o.append(n),o._read=()=>i.value==="__custom__"?n.value:i.value,o}function zt(t,s){const l=b("input",{className:"form-input me-icon",name:"icon",value:s||""}),i=b("span",{className:"me-icon-preview"});s&&i.setAttribute("data-icon",s);const n=b("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),o=p=>{p?i.setAttribute("data-icon",p):i.removeAttribute("data-icon"),Domma.icons.scan(i.parentNode||i)};return n.addEventListener("click",()=>At(p=>{l.value=p,o(p)})),l.addEventListener("input",()=>o(l.value.trim())),I(t,b("div",{className:"me-icon-field"},l,n,i))}function ot(...t){return b("div",{className:"me-field-grid"},...t)}async function Ct(t,s,l,i,n){const o=m=>b("h4",{className:"me-form-section",textContent:m}),p=I("Label",b("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),w=I("URL",b("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."),A=zt("Icon",t.icon||""),y=I("Visibility",Lt(t.visibility||"",i),'Who can see this item. "Role and above" shows it to that role and anyone more senior; "Role only" shows it to holders of that role alone (and the super-admin). Blank = everyone.'),R=P([["","- no permission gate -"],...(s||[]).map(m=>[m.key,m.label||m.key])],t.permission||"");R.name="permission";const L=I("Permission",R,"Hide unless the viewer holds this specific permission. Independent of Visibility - both must pass."),lt=I("Badge text",b("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),J=P([["","- none -"],...(l||[]).map(m=>[m.slug,m.title||m.slug])],t.badge?.countFrom||"");J.name="badgeCount";const at=I("Badge count from",J,"Show a live count of this collection\u2019s entries as the badge."),Q=it("Badge colour",t.badge?.variant||""),d=P([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");d.name="renderAs";const x=I("Render as",d,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),v=I("Pill style",P([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));v.querySelector("select").name="pillStyle";const Y=it("Pill colour",t.pill?.variant||""),W=b("div",{className:"me-pill-wrap"},ot(v,Y));W.style.display=t.pill?"":"none",d.addEventListener("change",()=>{W.style.display=d.value==="pill"?"":"none"});const D=it("Text colour",t.colour||""),X=P([["","- use the menu default -"],...rt.map(m=>[m,mt[m]||m])],t.highlight?.hover||"");X.name="hlHover";const N=I("Hover effect",X,"Overrides the menu-wide hover effect for this item only."),M=P([["","- use the menu default -"],...pt.map(m=>[m,ft[m]||m])],t.highlight?.active||"");M.name="hlActive";const U=I("When selected",M,"How this item is marked when it points at the page being viewed."),tt=it("Highlight colour",t.highlight?.accent||""),B=b("button",{className:"btn btn-primary",textContent:"Apply"}),V=b("div",{className:"me-item-form"},o("Link"),p,w,A,o("Access"),ot(y,L),o("Badge"),ot(lt,Q),at,o("Appearance"),x,W,D,o("Highlighting"),ot(N,U),tt,B),S=E.slideover({title:"Edit menu item",size:"lg",position:"right"});S.element.appendChild(V),S.open();const K=S.element.closest(".dm-slideover")||S.element.querySelector(".dm-slideover")||S.element;K.style.setProperty("width",It+"px","important"),K.style.setProperty("max-width","95vw","important"),Domma.icons.scan(V);const g=m=>V.querySelector(`[name="${m}"]`)?.value||"";B.addEventListener("click",()=>{t.text=g("text"),t.url=g("url"),t.icon=g("icon"),t.visibility=g("visibility"),t.permission=g("permission");const m=g("badgeText"),q=g("badgeCount"),et=Q._read();if(t.badge=m||q?{...m&&{text:m},...q&&{countFrom:q},...et&&{variant:et}}:null,g("renderAs")==="pill"){const nt=Y._read();t.pill={style:g("pillStyle")||"filled",...nt&&{variant:nt}}}else t.pill=null;t.colour=D._read();const F=g("hlHover"),T=g("hlActive"),z=tt._read();t.highlight=F||T||z?{...F&&{hover:F},...T&&{active:T},...z&&{accent:z}}:null,S.close(),n()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){const s=location.hash.match(/^#\/menus\/edit\/(.+)$/),l=s?decodeURIComponent(s[1]):null,i=!l,n=E.loader(t.get(0),{type:"dots"});let o,p,w,A,y,R,L;try{if(i)o={slug:"",name:"",description:"",items:[]};else try{o=await k.menus.get(l)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}p=await k.projects.list().catch(()=>[]),w=await H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),A=await k.collections.list().catch(()=>[]),y=await k.get("/collections/roles/entries?limit=100").catch(()=>null),R=await k.menuLocations.registry().catch(()=>[]),L=await k.pages.list().catch(()=>[])}finally{n.destroy()}const J=(Array.isArray(y)?y:y?.entries||[]).map(e=>({name:e.data?.name,label:e.data?.label,level:e.data?.level??99})).filter(e=>e.name).sort((e,a)=>a.level-e.level),at='<option value="">- no permission gate -</option>',Q=e=>at+(w.resources||[]).map(a=>{const c=(e||"")===a.key?" selected":"";return`<option value="${a.key}"${c}>${a.label||a.key}</option>`}).join("");t.find("#me-title").text(i?"New menu":`Edit "${o.slug}"`);let d=ut(o.items);const x=t.find("#me-items-list");function v(){jt(x,d,Q),Ot(x,()=>d,v)}v(),t.find("#me-add-item").on("click",()=>{j(x,d),d.push({id:G(),parentId:null,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}),v()}),t.find("#me-add-separator").on("click",()=>{j(x,d),d.push({id:G(),parentId:null,type:"separator"}),v()}),t.find("#me-add-spacer").on("click",()=>{j(x,d),d.push({id:G(),parentId:null,type:"spacer",size:"md"}),v()});function Y(e){return function(){j(x,d);const a=$(this).closest(".menu-tree-row").data("id"),c=d.findIndex(_=>_.id===a);if(c===-1)return;const u=d[c],r={separator:{type:"separator"},spacer:{type:"spacer",size:"md"}}[e],f=r?{id:G(),parentId:u.parentId,...r}:{id:G(),parentId:u.parentId,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1};d.splice(c+1,0,f),v()}}t.off("click",".me-ins-item").on("click",".me-ins-item",Y("item")),t.off("click",".me-ins-sep").on("click",".me-ins-sep",Y("separator")),t.off("click",".me-remove").on("click",".me-remove",function(){const e=$(this).closest(".menu-tree-row").data("id");d=d.filter(a=>a.id!==e&&a.parentId!==e),v()}),t.off("click",".me-indent").on("click",".me-indent",function(){j(x,d);const e=$(this).closest(".menu-tree-row").data("id"),a=d.find(r=>r.id===e);if(!a)return;const c=d.filter(r=>r.parentId===a.parentId),u=c.findIndex(r=>r.id===e);u<=0||["separator","spacer"].includes(c[u-1].type)||(a.parentId=c[u-1].id,v())}),t.off("click",".me-outdent").on("click",".me-outdent",function(){j(x,d);const e=$(this).closest(".menu-tree-row").data("id"),a=d.find(u=>u.id===e);if(!a||a.parentId==null)return;const c=d.find(u=>u.id===a.parentId);a.parentId=c?c.parentId:null,v()}),t.off("click",".me-up").on("click",".me-up",function(){j(x,d);const e=$(this).closest(".menu-tree-row").data("id"),a=d.filter(f=>f.parentId===d.find(_=>_.id===e)?.parentId),c=a.findIndex(f=>f.id===e);if(c<=0)return;const u=d.indexOf(a[c]),r=d.indexOf(a[c-1]);[d[u],d[r]]=[d[r],d[u]],v()}),t.off("click",".me-down").on("click",".me-down",function(){j(x,d);const e=$(this).closest(".menu-tree-row").data("id"),a=d.filter(f=>f.parentId===d.find(_=>_.id===e)?.parentId),c=a.findIndex(f=>f.id===e);if(c===-1||c>=a.length-1)return;const u=d.indexOf(a[c]),r=d.indexOf(a[c+1]);[d[u],d[r]]=[d[r],d[u]],v()}),t.off("click",".me-hidden").on("click",".me-hidden",function(){j(x,d);const e=$(this).closest(".menu-tree-row").data("id"),a=d.find(c=>c.id===e);a&&(a.hidden=!a.hidden),v()}),t.off("click",".me-edit").on("click",".me-edit",function(){const e=$(this).closest(".menu-tree-row").data("id"),a=d.find(c=>c.id===e);!a||["separator","spacer"].includes(a.type)||Ct(a,w.resources||[],A,J,()=>v())}),E.tabs(t.find("#me-tabs").get(0)),t.find("#me-variant").val(o.variant||""),t.find("#me-position").val(o.position||"");const W=t.find("#me-appearOnHover").get(0);W&&(W.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??""),t.find("#me-float-title").val(o.float?.title||"");for(const[e,a]of[["#me-float-collapsible",o.float?.collapsible],["#me-float-collapsed",o.float?.collapsed],["#me-float-movable",o.float?.movable],["#me-float-resizable",o.float?.resizable]]){const c=t.find(e).get(0);c&&(c.checked=a===!0)}const D=o.float?.accent||"",X=D.startsWith("#"),N=t.find("#me-float-accent"),M=t.find("#me-float-accent-hex");N.val(X?"__custom__":D),X&&M.val(D);const U=()=>{M.get(0).style.display=N.val()==="__custom__"?"":"none"};U(),N.on("change",U);const tt=()=>N.val()==="__custom__"?M.val():N.val(),B=()=>{const e=t.find("#me-position").val()==="floating";t.find(".dm-float-field").each(function(){this.style.display=e?"":"none"}),e&&U();const a=t.find("#me-orientation").val()==="vertical";t.find(".dm-vertical-field").each(function(){this.style.display=a?"":"none"});const c=!!(t.find("#me-float-collapsible").get(0)||{}).checked;t.find(".dm-float-collapsed-field").each(function(){this.style.display=e&&c?"":"none"})};t.find("#me-orientation").val(o.orientation||""),t.find("#me-align").val(o.align||""),t.find("#me-side").val(o.side||"left"),B(),t.find("#me-position").on("change",B),t.find("#me-orientation").on("change",B),t.find("#me-float-collapsible").on("change",B);const V=(e,a,c,u)=>{const r=t.find(e).get(0);r&&(r.innerHTML="",r.add(new Option(u,"")),a.forEach(f=>r.add(new Option(c[f]||f,f))))};V("#me-highlight-hover",rt,mt,"- theme default -"),V("#me-highlight-active",pt,ft,"- theme default -"),t.find("#me-highlight-hover").val(o.highlight?.hover||""),t.find("#me-highlight-active").val(o.highlight?.active||"");const S=o.highlight?.accent||"",K=S.startsWith("#"),g=t.find("#me-highlight-accent"),m=t.find("#me-highlight-accent-hex");g.val(K?"__custom__":S),K&&m.val(S);const q=()=>{m.get(0).style.display=g.val()==="__custom__"?"":"none"};q(),g.on("change",q);const et=()=>g.val()==="__custom__"?m.val():g.val();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 F=t.find("#me-bind-slot"),T=t.find("#me-bind-projects"),z=t.find("#me-bind-pages");F.html('<option value="">- not bound -</option>'+(R||[]).map(e=>`<option value="${h(e.slot)}">${h(e.label||e.slot)} (${h(e.slot)})</option>`).join("")),T.html(p.map(e=>`<option value="${h(e.slug)}">${h(e.name||e.slug)}</option>`).join("")),F.val(o.binding?.slot||"");const nt=new Set(o.binding?.projects||[]);Array.from(T.get(0).options).forEach(e=>{e.selected=nt.has(e.value)}),z.val((o.binding?.pages||[]).join(`
44
- `)),t.find("#me-bind-page-pick").html('<option value="">- pick a page -</option>'+L.map(e=>{const a=e.urlPath||e.url||"";return a?`<option value="${h(a)}">${h(e.title||a)} - ${h(a)}</option>`:""}).join("")),t.find("#me-bind-page-add").on("click",()=>{const e=t.find("#me-bind-page-pick").val();if(!e)return;const a=z.val().split(`
45
- `).map(c=>c.trim()).filter(Boolean);a.includes(e)||a.push(e),z.val(a.join(`
46
- `)),Z()});function dt(){const e=F.val();return e?{slot:e,projects:Array.from(T.get(0).selectedOptions).map(a=>a.value),pages:z.val().split(`
47
- `).map(a=>a.trim()).filter(Boolean)}:null}function Z(){const e=dt(),a=t.find("#me-bind-summary");if(!e){a.html('<span class="text-muted">Not bound - this menu renders only where it is mapped or placed by a shortcode.</span>');return}const c=[];e.projects.length&&c.push(`pages in ${e.projects.map(f=>`<code>${h(f)}</code>`).join(", ")}`),e.pages.length&&c.push(`pages matching ${e.pages.map(f=>`<code>${h(f)}</code>`).join(", ")}`);const u=e.pages.filter(f=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(f)),r=u.length?`<div class="text-danger" style="margin-top:.4rem">Not valid page patterns: ${u.map(f=>`<code>${h(f)}</code>`).join(", ")}</div>`:"";a.html(c.length?`Takes over the <code>${h(e.slot)}</code> slot on ${c.join(" and ")}.${r}`:`<span class="text-warning">Slot <code>${h(e.slot)}</code> selected but no project or page chosen - the binding will never match.</span>`)}F.on("change",Z),T.on("change",Z),z.on("input",Z),Z();const{getProjectFromHash:bt}=await import("../lib/project-context.js"),vt=i?bt():null,ct=t.find("#me-project");ct.html('<option value="">- none -</option>'+p.map(e=>`<option value="${h(e.slug)}">${h(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||""),ct.val(o.meta?.project||vt||""),Domma.icons.scan(t.get(0)),t.find("#me-save").on("click",async()=>{j(x,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:tt(),floatOpacity:t.find("#me-float-opacity").val(),floatTitle:t.find("#me-float-title").val().trim(),floatCollapsible:!!(t.find("#me-float-collapsible").get(0)||{}).checked,floatCollapsed:!!(t.find("#me-float-collapsed").get(0)||{}).checked,floatMovable:!!(t.find("#me-float-movable").get(0)||{}).checked,floatResizable:!!(t.find("#me-float-resizable").get(0)||{}).checked,highlightHover:t.find("#me-highlight-hover").val(),highlightActive:t.find("#me-highlight-active").val(),highlightAccent:et(),orientation:t.find("#me-orientation").val(),align:t.find("#me-align").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()},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()},c=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"],u=Object.fromEntries(Object.entries(e).filter(([O,C])=>c.includes(O)&&C)),r={};if(e.position==="floating"){e.floatAnchor&&(r.anchor=e.floatAnchor);for(const[O,C]of[["offsetX",e.floatOffsetX],["offsetY",e.floatOffsetY],["width",e.floatWidth],["radius",e.floatRadius],["opacity",e.floatOpacity]])C!==""&&C!=null&&Number.isFinite(Number(C))&&(r[O]=Number(C));e.floatShadow&&(r.shadow=e.floatShadow),e.floatAccent&&(r.accent=e.floatAccent),e.floatTitle&&(r.title=e.floatTitle),e.floatCollapsible&&(r.collapsible=!0),e.floatCollapsible&&e.floatCollapsed&&(r.collapsed=!0),e.floatMovable&&(r.movable=!0),e.floatResizable&&(r.resizable=!0)}const f={};e.highlightHover&&(f.hover=e.highlightHover),e.highlightActive&&(f.active=e.highlightActive),e.highlightAccent&&(f.accent=e.highlightAccent);const _={slug:a.slug,name:a.name,description:a.description,...e.variant&&{variant:e.variant},...e.position&&{position:e.position},...e.orientation&&{orientation:e.orientation},...e.orientation==="vertical"&&e.side&&{side:e.side},...e.orientation!=="vertical"&&e.align&&{align:e.align},...Object.keys(r).length&&{float:r},...Object.keys(f).length&&{highlight:f},...e.appearOnHover&&{appearOnHover:!0},...Object.keys(u).length&&{style:u},binding:dt(),items:Ht(d),meta:{...o.meta||{},project:a.project||null}};try{if(i)await k.menus.create(_);else if(_.slug!==o.slug){await k.menus.create(_);const O=await k.menuLocations.get();let C=!1;for(const[yt,wt]of Object.entries(O))wt===o.slug&&(O[yt]=_.slug,C=!0);C&&await k.menuLocations.save(O),await k.menus.remove(o.slug)}else await k.menus.update(o.slug,_);E.toast("Saved.",{type:"success"}),location.hash="#/menus/edit/"+encodeURIComponent(_.slug)}catch(O){E.toast(`Save failed: ${O.message||O}`,{type:"error"})}})}};
43
+ `),o(p.id)}}o(null),Domma.icons.scan(t.get(0)),t.get(0).querySelectorAll("[data-tooltip]").forEach(m=>{E.tooltip(m,{content:m.getAttribute("data-tooltip"),position:"top"})})}function j(t,c){t.find(".menu-tree-row").each(function(){const r=$(this).data("id"),i=c.find(k=>k.id===r);if(!i||i.type==="separator")return;if(i.type==="spacer"){const k=$(this).find(".me-spacer-size");k.length&&(i.size=k.val());return}const a=$(this).find(".me-text");a.length&&(i.text=a.val());const o=$(this).find(".me-url");o.length&&(i.url=o.val());const m=$(this).find(".me-icon");m.length&&(i.icon=m.val());const p=$(this).find(".me-vis");p.length&&(i.visibility=p.val());const x=$(this).find(".me-perm");x.length&&(i.permission=x.val())})}function Ut(t,c){const r={};for(const i of c){const a=t.querySelector(`[name="${i}"]`);a&&(r[i]=a.type==="checkbox"?a.checked:a.value)}return r}function w(t,c={},...r){const i=document.createElement(t);Object.assign(i,c);for(const a of r)a!=null&&i.append(a);return i}function O(t,c,r){const i=w("label",{className:"form-field"},w("span",{className:"form-label",textContent:t}),c);return r&&i.append(w("small",{className:"form-hint",textContent:r,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),i}function Pt(t,c){const r=[["","- Everyone -"],["public","Public - anyone, incl. logged-out"],...(c||[]).map(a=>[a.name,`${a.label||a.name} and above`]),...(c||[]).filter(a=>a.level!==0).map(a=>[`=${a.name}`,`${a.label||a.name} only`]),["private","Private - super-admin only"]];t&&!r.some(([a])=>a===t)&&r.push([t,`${t} (custom)`]);const i=D(r,t||"");return i.name="visibility",i}function D(t,c){const r=w("select",{className:"form-select"});return t.forEach(([i,a])=>r.add(new Option(a,i))),r.value=c??"",r}function dt(t,c){const r=typeof c=="string"&&c.startsWith("#"),i=D([["","- none -"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],r?"__custom__":c||""),a=w("input",{type:"color",className:"form-input me-colour-hex",value:r?c:"#000000"});a.style.display=r?"":"none",i.addEventListener("change",()=>{a.style.display=i.value==="__custom__"?"":"none"});const o=O(t,i);return o.append(a),o._read=()=>i.value==="__custom__"?a.value:i.value,o}function Bt(t,c){const r=w("input",{className:"form-input me-icon",name:"icon",value:c||""}),i=w("span",{className:"me-icon-preview"});c&&i.setAttribute("data-icon",c);const a=w("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),o=m=>{m?i.setAttribute("data-icon",m):i.removeAttribute("data-icon"),Domma.icons.scan(i.parentNode||i)};return a.addEventListener("click",()=>_t(m=>{r.value=m,o(m)})),r.addEventListener("input",()=>o(r.value.trim())),O(t,w("div",{className:"me-icon-field"},r,a,i))}function ct(...t){return w("div",{className:"me-field-grid"},...t)}async function Dt(t,c,r,i,a){const o=b=>w("h4",{className:"me-form-section",textContent:b}),m=O("Label",w("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),p=w("input",{className:"form-input",name:"url",value:t.url||""}),x=O("URL",p,t.tool||t.locked?"A Tool's address is its identity, so it cannot be changed here.":"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items.");(t.tool||t.locked)&&(p.readOnly=!0);const k=Bt("Icon",t.icon||""),v=Pt(t.visibility||"",i),L=O("Visibility",v,t.locked?"Always in the sidebar for everyone who may use it, so it cannot be restricted here.":'Who can see this item. "Role and above" shows it to that role and anyone more senior; "Role only" shows it to holders of that role alone (and the super-admin). Blank = everyone.');t.locked&&(v.disabled=!0);const V=D([["","- no permission gate -"],...(c||[]).map(b=>[b.key,b.label||b.key])],t.permission||"");V.name="permission";const q=O("Permission",V,t.tool||t.locked?"A Tool is guarded by its own permission, which always applies.":"Hide unless the viewer holds this specific permission. Independent of Visibility - both must pass.");(t.tool||t.locked)&&(V.disabled=!0);const Z=O("Badge text",w("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),W=D([["","- none -"],...(r||[]).map(b=>[b.slug,b.title||b.slug])],t.badge?.countFrom||"");W.name="badgeCount";const C=O("Badge count from",W,"Show a live count of this collection\u2019s entries as the badge."),M=dt("Badge colour",t.badge?.variant||""),s=D([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");s.name="renderAs";const A=O("Render as",s,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),y=O("Pill style",D([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));y.querySelector("select").name="pillStyle";const J=dt("Pill colour",t.pill?.variant||""),Y=w("div",{className:"me-pill-wrap"},ct(y,J));Y.style.display=t.pill?"":"none",s.addEventListener("change",()=>{Y.style.display=s.value==="pill"?"":"none"});const Q=dt("Text colour",t.colour||""),U=D([["","- use the menu default -"],...mt.map(b=>[b,ht[b]||b])],t.highlight?.hover||"");U.name="hlHover";const at=O("Hover effect",U,"Overrides the menu-wide hover effect for this item only."),N=D([["","- use the menu default -"],...ut.map(b=>[b,bt[b]||b])],t.highlight?.active||"");N.name="hlActive";const tt=O("When selected",N,"How this item is marked when it points at the page being viewed."),X=dt("Highlight colour",t.highlight?.accent||""),st=w("button",{className:"btn btn-primary",textContent:"Apply"}),F=w("div",{className:"me-item-form"},o("Link"),m,x,k,o("Access"),ct(L,q),o("Badge"),ct(Z,M),C,o("Appearance"),A,Y,Q,o("Highlighting"),ct(at,tt),X,st),R=E.slideover({title:"Edit menu item",size:"lg",position:"right"});R.element.appendChild(F),R.open();const K=R.element.closest(".dm-slideover")||R.element.querySelector(".dm-slideover")||R.element;K.style.setProperty("width",Et+"px","important"),K.style.setProperty("max-width","95vw","important"),Domma.icons.scan(F);const _=b=>F.querySelector(`[name="${b}"]`)?.value||"";st.addEventListener("click",()=>{t.text=_("text"),t.icon=_("icon"),t.locked||(t.visibility=_("visibility")),!t.tool&&!t.locked?(t.url=_("url"),t.permission=_("permission")):t.touched=!0;const b=_("badgeText"),G=_("badgeCount"),et=M._read();if(t.badge=b||G?{...b&&{text:b},...G&&{countFrom:G},...et&&{variant:et}}:null,_("renderAs")==="pill"){const B=J._read();t.pill={style:_("pillStyle")||"filled",...B&&{variant:B}}}else t.pill=null;t.colour=Q._read();const nt=_("hlHover"),z=_("hlActive"),P=X._read();t.highlight=nt||z||P?{...nt&&{hover:nt},...z&&{active:z},...P&&{accent:P}}:null,R.close(),a()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){const c=location.hash.match(/^#\/menus\/edit\/(.+)$/),r=c?decodeURIComponent(c[1]):null,i=!r,a=E.loader(t.get(0),{type:"dots"});let o,m,p,x,k,v,L;try{if(i)o={slug:"",name:"",description:"",items:[]};else try{o=await S.menus.get(r)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}m=await S.projects.list().catch(()=>[]),p=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),v=await S.menuLocations.registry().catch(()=>[]),L=await S.pages.list().catch(()=>[])}finally{a.destroy()}const q=(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,n)=>n.level-e.level),Z='<option value="">- no permission gate -</option>',W=e=>Z+(p.resources||[]).map(n=>{const l=(e||"")===n.key?" selected":"";return`<option value="${n.key}"${l}>${n.label||n.key}</option>`}).join("");t.find("#me-title").text(i?"New menu":`Edit "${o.slug}"`);const C=!i&&r===Rt;let M=[];C&&(M=await H.get("/api/sidebar/registered-items").catch(()=>(E.toast("Could not load the Tools - they are not shown, and saving keeps their places as they are.",{type:"warning",duration:8e3}),[])));let s=jt(C?Ot(Ht(o.items||[],M,{forEditor:!0}),{leaveInHiddenFolders:!0}):o.items);const A=t.find("#me-items-list");function y(){zt(A,s,W,C),Ft(A,()=>s,y)}y(),t.find("#me-add-item").on("click",()=>{j(A,s),s.push({id:it(),parentId:null,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}),y()}),t.find("#me-add-separator").on("click",()=>{j(A,s),s.push({id:it(),parentId:null,type:"separator"}),y()}),t.find("#me-add-spacer").on("click",()=>{j(A,s),s.push({id:it(),parentId:null,type:"spacer",size:"md"}),y()});function J(e){return function(){j(A,s);const n=$(this).closest(".menu-tree-row").data("id"),l=s.findIndex(f=>f.id===n);if(l===-1)return;const u=s[l],d={separator:{type:"separator"},spacer:{type:"spacer",size:"md"}}[e],h=d?{id:it(),parentId:u.parentId,...d}:{id:it(),parentId:u.parentId,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1};s.splice(l+1,0,h),y()}}t.off("click",".me-ins-item").on("click",".me-ins-item",J("item")),t.off("click",".me-ins-sep").on("click",".me-ins-sep",J("separator"));function Y(e){const n=u=>s.find(d=>!d.type&&!d.url&&!d.tool&&(d.key||"").toLowerCase()===u)||s.find(d=>!d.type&&!d.url&&!d.tool&&!d.key&&(d.text||"").toLowerCase()===u),l=n(String(e.tool.folder||"").toLowerCase())||n(Tt);e.parentId=l?l.id:null,s.splice(s.indexOf(e),1),s.push(e),Object.assign(e,{text:e.tool.defaults.text,icon:e.tool.defaults.icon,visibility:"",hidden:!1,badge:null,pill:null,colour:"",highlight:null,touched:!1}),e.tool={...e.tool,persisted:!1}}t.off("click",".me-remove").on("click",".me-remove",function(){const e=$(this).closest(".menu-tree-row").data("id"),n=s.find(f=>f.id===e),l=f=>{let I=f;for(;I;){if(I.id===e)return!0;I=s.find(T=>T.id===I.parentId)}return!1},u=C?lt(s):new Set;if(u.has(e))return;const d=s.filter(f=>f.id!==e&&f.tool&&l(f)),h=s.filter(f=>f.id!==e&&!f.tool&&u.has(f.id)&&l(f));s=s.filter(f=>!l(f)||d.includes(f)||h.includes(f));for(const f of h)f.parentId=n?.parentId??null;for(const f of d)f.tool.orphan?f.parentId=n?.parentId??null:Y(f);y()}),t.off("click",".me-indent").on("click",".me-indent",function(){j(A,s);const e=$(this).closest(".menu-tree-row").data("id"),n=s.find(d=>d.id===e);if(!n)return;const l=s.filter(d=>d.parentId===n.parentId),u=l.findIndex(d=>d.id===e);u<=0||["separator","spacer"].includes(l[u-1].type)||(n.parentId=l[u-1].id,n.touched=!0,y())}),t.off("click",".me-outdent").on("click",".me-outdent",function(){j(A,s);const e=$(this).closest(".menu-tree-row").data("id"),n=s.find(u=>u.id===e);if(!n||n.parentId==null)return;const l=s.find(u=>u.id===n.parentId);n.parentId=l?l.parentId:null,n.touched=!0,y()}),t.off("click",".me-up").on("click",".me-up",function(){j(A,s);const e=$(this).closest(".menu-tree-row").data("id"),n=s.filter(h=>h.parentId===s.find(f=>f.id===e)?.parentId),l=n.findIndex(h=>h.id===e);if(l<=0)return;n[l].touched=!0;const u=s.indexOf(n[l]),d=s.indexOf(n[l-1]);[s[u],s[d]]=[s[d],s[u]],y()}),t.off("click",".me-down").on("click",".me-down",function(){j(A,s);const e=$(this).closest(".menu-tree-row").data("id"),n=s.filter(h=>h.parentId===s.find(f=>f.id===e)?.parentId),l=n.findIndex(h=>h.id===e);if(l===-1||l>=n.length-1)return;n[l].touched=!0;const u=s.indexOf(n[l]),d=s.indexOf(n[l+1]);[s[u],s[d]]=[s[d],s[u]],y()}),t.off("click",".me-hidden").on("click",".me-hidden",function(){j(A,s);const e=$(this).closest(".menu-tree-row").data("id"),n=s.find(l=>l.id===e);n&&!(C&&lt(s).has(n.id))&&(n.hidden=!n.hidden,n.touched=!0),y()}),t.off("click",".me-reset").on("click",".me-reset",function(){j(A,s);const e=$(this).closest(".menu-tree-row").data("id"),n=s.find(l=>l.id===e);!n?.tool||n.tool.orphan||(Y(n),y())}),t.off("click",".me-edit").on("click",".me-edit",function(){const e=$(this).closest(".menu-tree-row").data("id"),n=s.find(l=>l.id===e);!n||["separator","spacer"].includes(n.type)||(n.locked=C&&lt(s).has(n.id),Dt(n,p.resources||[],x,q,()=>y()))}),E.tabs(t.find("#me-tabs").get(0)),t.find("#me-variant").val(o.variant||""),t.find("#me-position").val(o.position||"");const Q=t.find("#me-appearOnHover").get(0);Q&&(Q.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??""),t.find("#me-float-title").val(o.float?.title||"");for(const[e,n]of[["#me-float-collapsible",o.float?.collapsible],["#me-float-collapsed",o.float?.collapsed],["#me-float-movable",o.float?.movable],["#me-float-resizable",o.float?.resizable]]){const l=t.find(e).get(0);l&&(l.checked=n===!0)}const U=o.float?.accent||"",at=U.startsWith("#"),N=t.find("#me-float-accent"),tt=t.find("#me-float-accent-hex");N.val(at?"__custom__":U),at&&tt.val(U);const X=()=>{tt.get(0).style.display=N.val()==="__custom__"?"":"none"};X(),N.on("change",X);const st=()=>N.val()==="__custom__"?tt.val():N.val(),F=()=>{const e=t.find("#me-position").val()==="floating";t.find(".dm-float-field").each(function(){this.style.display=e?"":"none"}),e&&X();const n=t.find("#me-orientation").val()==="vertical";t.find(".dm-vertical-field").each(function(){this.style.display=n?"":"none"});const l=!!(t.find("#me-float-collapsible").get(0)||{}).checked;t.find(".dm-float-collapsed-field").each(function(){this.style.display=e&&l?"":"none"})};t.find("#me-orientation").val(o.orientation||""),t.find("#me-align").val(o.align||""),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-float-collapsible").on("change",F);const R=(e,n,l,u)=>{const d=t.find(e).get(0);d&&(d.innerHTML="",d.add(new Option(u,"")),n.forEach(h=>d.add(new Option(l[h]||h,h))))};R("#me-highlight-hover",mt,ht,"- theme default -"),R("#me-highlight-active",ut,bt,"- theme default -"),t.find("#me-highlight-hover").val(o.highlight?.hover||""),t.find("#me-highlight-active").val(o.highlight?.active||"");const K=o.highlight?.accent||"",_=K.startsWith("#"),b=t.find("#me-highlight-accent"),G=t.find("#me-highlight-accent-hex");b.val(_?"__custom__":K),_&&G.val(K);const et=()=>{G.get(0).style.display=b.val()==="__custom__"?"":"none"};et(),b.on("change",et);const nt=()=>b.val()==="__custom__"?G.val():b.val();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 z=t.find("#me-bind-slot"),P=t.find("#me-bind-projects"),B=t.find("#me-bind-pages");z.html('<option value="">- not bound -</option>'+(v||[]).map(e=>`<option value="${g(e.slot)}">${g(e.label||e.slot)} (${g(e.slot)})</option>`).join("")),P.html(m.map(e=>`<option value="${g(e.slug)}">${g(e.name||e.slug)}</option>`).join("")),z.val(o.binding?.slot||"");const yt=new Set(o.binding?.projects||[]);Array.from(P.get(0).options).forEach(e=>{e.selected=yt.has(e.value)}),B.val((o.binding?.pages||[]).join(`
44
+ `)),t.find("#me-bind-page-pick").html('<option value="">- pick a page -</option>'+L.map(e=>{const n=e.urlPath||e.url||"";return n?`<option value="${g(n)}">${g(e.title||n)} - ${g(n)}</option>`:""}).join("")),t.find("#me-bind-page-add").on("click",()=>{const e=t.find("#me-bind-page-pick").val();if(!e)return;const n=B.val().split(`
45
+ `).map(l=>l.trim()).filter(Boolean);n.includes(e)||n.push(e),B.val(n.join(`
46
+ `)),ot()});function pt(){const e=z.val();return e?{slot:e,projects:Array.from(P.get(0).selectedOptions).map(n=>n.value),pages:B.val().split(`
47
+ `).map(n=>n.trim()).filter(Boolean)}:null}function ot(){const e=pt(),n=t.find("#me-bind-summary");if(!e){n.html('<span class="text-muted">Not bound - this menu renders only where it is mapped or placed by a shortcode.</span>');return}const l=[];e.projects.length&&l.push(`pages in ${e.projects.map(h=>`<code>${g(h)}</code>`).join(", ")}`),e.pages.length&&l.push(`pages matching ${e.pages.map(h=>`<code>${g(h)}</code>`).join(", ")}`);const u=e.pages.filter(h=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(h)),d=u.length?`<div class="text-danger" style="margin-top:.4rem">Not valid page patterns: ${u.map(h=>`<code>${g(h)}</code>`).join(", ")}</div>`:"";n.html(l.length?`Takes over the <code>${g(e.slot)}</code> slot on ${l.join(" and ")}.${d}`:`<span class="text-warning">Slot <code>${g(e.slot)}</code> selected but no project or page chosen - the binding will never match.</span>`)}z.on("change",ot),P.on("change",ot),B.on("input",ot),ot();const{getProjectFromHash:wt}=await import("../lib/project-context.js"),kt=i?wt():null,ft=t.find("#me-project");ft.html('<option value="">- none -</option>'+m.map(e=>`<option value="${g(e.slug)}">${g(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||""),ft.val(o.meta?.project||kt||""),Domma.icons.scan(t.get(0)),t.find("#me-save").on("click",async()=>{j(A,s);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:st(),floatOpacity:t.find("#me-float-opacity").val(),floatTitle:t.find("#me-float-title").val().trim(),floatCollapsible:!!(t.find("#me-float-collapsible").get(0)||{}).checked,floatCollapsed:!!(t.find("#me-float-collapsed").get(0)||{}).checked,floatMovable:!!(t.find("#me-float-movable").get(0)||{}).checked,floatResizable:!!(t.find("#me-float-resizable").get(0)||{}).checked,highlightHover:t.find("#me-highlight-hover").val(),highlightActive:t.find("#me-highlight-active").val(),highlightAccent:nt(),orientation:t.find("#me-orientation").val(),align:t.find("#me-align").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()},n={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()},l=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"],u=Object.fromEntries(Object.entries(e).filter(([I,T])=>l.includes(I)&&T)),d={};if(e.position==="floating"){e.floatAnchor&&(d.anchor=e.floatAnchor);for(const[I,T]of[["offsetX",e.floatOffsetX],["offsetY",e.floatOffsetY],["width",e.floatWidth],["radius",e.floatRadius],["opacity",e.floatOpacity]])T!==""&&T!=null&&Number.isFinite(Number(T))&&(d[I]=Number(T));e.floatShadow&&(d.shadow=e.floatShadow),e.floatAccent&&(d.accent=e.floatAccent),e.floatTitle&&(d.title=e.floatTitle),e.floatCollapsible&&(d.collapsible=!0),e.floatCollapsible&&e.floatCollapsed&&(d.collapsed=!0),e.floatMovable&&(d.movable=!0),e.floatResizable&&(d.resizable=!0)}const h={};e.highlightHover&&(h.hover=e.highlightHover),e.highlightActive&&(h.active=e.highlightActive),e.highlightAccent&&(h.accent=e.highlightAccent);const f={slug:n.slug,name:n.name,description:n.description,...e.variant&&{variant:e.variant},...e.position&&{position:e.position},...e.orientation&&{orientation:e.orientation},...e.orientation==="vertical"&&e.side&&{side:e.side},...e.orientation!=="vertical"&&e.align&&{align:e.align},...Object.keys(d).length&&{float:d},...Object.keys(h).length&&{highlight:h},...e.appearOnHover&&{appearOnHover:!0},...Object.keys(u).length&&{style:u},binding:pt(),items:Ct(s),meta:{...o.meta||{},project:n.project||null}};try{if(i)await S.menus.create(f);else if(f.slug!==o.slug){await S.menus.create(f);const I=await S.menuLocations.get();let T=!1;for(const[xt,At]of Object.entries(I))At===o.slug&&(I[xt]=f.slug,T=!0);T&&await S.menuLocations.save(I),await S.menus.remove(o.slug)}else await S.menus.update(o.slug,f);E.toast("Saved.",{type:"success"}),location.hash="#/menus/edit/"+encodeURIComponent(f.slug)}catch(I){E.toast(`Save failed: ${I.message||I}`,{type:"error"})}})}};
@@ -1,3 +1,3 @@
1
- import{api as v,apiRequest as b}from"../api.js";export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(e){E.tabs(e.find("#settings-tabs").get(0));const p=E.loader(e.get(0),{type:"dots"}),l=await v.settings.get().catch(()=>({}));p.destroy(),e.find("#field-site-title").val(l.title||""),e.find("#field-tagline").val(l.tagline||""),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-footer-motion-toggle").prop("checked",l.footer?.showMotionToggle!==!1),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||""),y(l.smtp?.host||""),e.find("#field-smtp-host").on("input",function(){y(this.value)});const c=l.backToTop||{};e.find("#field-btt-enabled").prop("checked",c.enabled!==!1),e.find("#field-btt-threshold").val(c.scrollThreshold??300),e.find("#field-btt-position").val(c.position||"bottom-right"),e.find("#field-btt-offset").val(c.offset??32),e.find("#field-btt-bottom-offset").val(c.bottomOffset??c.offset??32),e.find("#field-btt-label").val(c.label||""),e.find("#field-btt-smooth").prop("checked",c.smooth!==!1);const i=l.cookieConsent||{};e.find("#field-cc-enabled").prop("checked",i.enabled!==!1),e.find("#field-cc-message").val(i.message||""),e.find("#field-cc-accept-all").val(i.acceptAllText||"Accept All"),e.find("#field-cc-reject-all").val(i.rejectAllText||"Reject All"),e.find("#field-cc-customize").val(i.customizeText||"Customize"),e.find("#field-cc-save-prefs").val(i.savePreferencesText||"Save Preferences"),e.find("#field-cc-privacy-text").val(i.privacyPolicyText||"Privacy Policy"),e.find("#field-cc-privacy-url").val(i.privacyPolicyUrl||""),e.find("#field-cc-cookie-text").val(i.cookiePolicyText||"Cookie Policy"),e.find("#field-cc-cookie-url").val(i.cookiePolicyUrl||""),e.find("#field-cc-position").val(i.position||"bottom"),e.find("#field-cc-layout").val(i.layout||"bar"),e.find("#field-cc-theme").val(i.theme||"dark"),e.find("#field-cc-show-functional").prop("checked",i.showFunctional!==!1),e.find("#field-cc-show-analytics").prop("checked",i.showAnalytics!==!1),e.find("#field-cc-show-marketing").prop("checked",i.showMarketing!==!1),e.find("#field-cc-version").val(i.consentVersion||"1.0");const m=l.breadcrumbs||{};e.find("#field-breadcrumbs-enabled").prop("checked",m.enabled===!0),e.find("#field-breadcrumbs-home-label").val(m.homeLabel||"Home"),e.find("#field-breadcrumbs-position").val(m.position||"TL"),e.find("#field-bc-offset-x").val(m.offsetX??8),e.find("#field-bc-offset-y").val(m.offsetY??8);function u(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)":""})}u(m.position||"TL"),e.find(".bc-pos-btn").on("click",function(){const t=this.dataset.pos;e.find("#field-breadcrumbs-position").val(t),u(t)});let n=null;try{const{css:t}=await b("/settings/custom-css");e.find("#field-custom-css").val(t||""),E.editor&&(n=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{}n&&n._editorEl&&n._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=n._editorEl,a=s.selectionStart!==s.selectionEnd;if((t.key==="c"||t.key==="x")&&!a){t.preventDefault();const d=s.value,o=s.selectionStart,r=d.lastIndexOf(`
2
- `,o-1)+1,h=d.indexOf(`
3
- `,o),g=h===-1?d.slice(r):d.slice(r,h+1);if(navigator.clipboard.writeText(g),t.key==="x"){const k=d.slice(0,r),x=h===-1?"":d.slice(h+1);s.value=k+x,s.selectionStart=s.selectionEnd=r,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={title:e.find("#field-site-title").val().trim(),tagline:e.find("#field-tagline").val().trim(),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(),showMotionToggle:e.find("#field-footer-motion-toggle").prop("checked"),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"}},s=e.find("#save-settings-btn");s.prop("disabled",!0);try{await v.settings.save(t),E.toast("Settings saved.",{type:"success"})}catch{E.toast("Failed to save settings.",{type:"error"})}finally{s.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),a=e.find("#send-test-email-btn");a.prop("disabled",!0),s&&(s.textContent="Sending\u2026",s.style.color="");try{const d=await b("/settings/test-email",{method:"POST",body:JSON.stringify({to:t||void 0})});s&&(s.textContent=d.message||"Test email sent.",s.style.color="var(--dm-success,#4ade80)")}catch(d){s&&(s.textContent=d.message||"Failed to send test email.",s.style.color="var(--dm-danger,#f87171)")}finally{a.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 v.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=n?n.getValue():e.find("#field-custom-css").val(),s=e.find("#save-css-btn");s.prop("disabled",!0);try{await b("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:t})}),E.toast("Custom CSS saved.",{type:"success"})}catch(a){E.toast(a.message||"Failed to save CSS.",{type:"error"})}finally{s.prop("disabled",!1)}});async function f(){try{const t=await b("/cache/status"),s=t.enabled?`enabled (${t.driver})`:"disabled",a=t.maxItems?` \xB7 ${t.size} / ${t.maxItems} entries`:"";e.find("#cache-status").text(s+a)}catch{e.find("#cache-status").text("unknown")}try{const{entries:t}=await b("/cache/keys");w(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 f(),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 b("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"}),await f()}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",f)}};function y(e){const p=document.getElementById("smtp-unconfigured-notice");p&&(p.style.display=String(e||"").trim()?"none":"",p.style.display===""&&Domma.icons.scan(p))}function w(e,p){const l=e.get(0);if(!l)return;for(;l.firstChild;)l.removeChild(l.firstChild);if(!p.length){const f=document.createElement("p");f.className="text-muted",f.style.fontSize=".875rem",f.textContent="Cache is empty.",l.appendChild(f);return}const c=[...p].reverse(),i=document.createElement("table");i.className="table table-compact";const m=document.createElement("thead"),u=document.createElement("tr");for(const f of["Key","Tags","Expires"]){const t=document.createElement("th");t.textContent=f,u.appendChild(t)}m.appendChild(u),i.appendChild(m);const n=document.createElement("tbody");for(const f of c){const t=document.createElement("tr"),s=document.createElement("td");s.style.fontFamily="monospace",s.style.fontSize="12px",s.textContent=f.key,t.appendChild(s);const a=document.createElement("td");a.style.fontSize="12px";for(const o of f.tags){const r=document.createElement("span");r.className="badge",r.style.marginRight="4px",r.style.fontSize="11px",r.textContent=o,a.appendChild(r)}t.appendChild(a);const d=document.createElement("td");if(d.style.fontSize="12px",d.style.color="var(--dm-text-muted, #6b7280)",f.expiresAt===null)d.textContent="no expiry";else{const o=f.expiresAt-Date.now();o<=0?d.textContent="expired":o<6e4?d.textContent=`${Math.round(o/1e3)}s`:o<36e5?d.textContent=`${Math.round(o/6e4)}m`:d.textContent=`${Math.round(o/36e5)}h`}t.appendChild(d),n.appendChild(t)}i.appendChild(n),l.appendChild(i)}
1
+ import{api as v,apiRequest as b}from"../api.js";import{placeTools as w}from"../lib/sidebar-grouping.js";async function C(e,f){if(!e)return;const[l,c]=await Promise.all([H.get("/api/sidebar/menu").catch(()=>({items:[]})),H.get("/api/sidebar/registered-items").catch(()=>[])]),i=new Map([["/","Dashboard (built in)"]]),n=(a,d)=>{for(const t of a||[]){if(!t||t.hidden||t.restricted)continue;const s=typeof t.url=="string"?t.url:"";s.startsWith("#/")&&s.length>2&&!i.has(s.slice(1))&&i.set(s.slice(1),`${d?`${d} \u203A `:""}${t.text||s}`),n(t.items,t.url?d:t.text||d)}};n(w(l?.items||[],c||[]),"");const m=/^\/[A-Za-z0-9\-_/]*$/.test(f||"")&&f.replace(/\/+$/,"")||"/";i.has(m)||i.set(m,`${m} (not in the sidebar)`);for(const[a,d]of i){const t=document.createElement("option");t.value=a,t.textContent=d,t.selected=a===m,e.appendChild(t)}}export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(e){E.tabs(e.find("#settings-tabs").get(0));const f=E.loader(e.get(0),{type:"dots"}),l=await v.settings.get().catch(()=>({}));f.destroy(),e.find("#field-site-title").val(l.title||""),e.find("#field-tagline").val(l.tagline||""),C(e.find("#field-admin-home").get(0),l.adminHome),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-footer-motion-toggle").prop("checked",l.footer?.showMotionToggle!==!1),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||""),y(l.smtp?.host||""),e.find("#field-smtp-host").on("input",function(){y(this.value)});const c=l.backToTop||{};e.find("#field-btt-enabled").prop("checked",c.enabled!==!1),e.find("#field-btt-threshold").val(c.scrollThreshold??300),e.find("#field-btt-position").val(c.position||"bottom-right"),e.find("#field-btt-offset").val(c.offset??32),e.find("#field-btt-bottom-offset").val(c.bottomOffset??c.offset??32),e.find("#field-btt-label").val(c.label||""),e.find("#field-btt-smooth").prop("checked",c.smooth!==!1);const i=l.cookieConsent||{};e.find("#field-cc-enabled").prop("checked",i.enabled!==!1),e.find("#field-cc-message").val(i.message||""),e.find("#field-cc-accept-all").val(i.acceptAllText||"Accept All"),e.find("#field-cc-reject-all").val(i.rejectAllText||"Reject All"),e.find("#field-cc-customize").val(i.customizeText||"Customize"),e.find("#field-cc-save-prefs").val(i.savePreferencesText||"Save Preferences"),e.find("#field-cc-privacy-text").val(i.privacyPolicyText||"Privacy Policy"),e.find("#field-cc-privacy-url").val(i.privacyPolicyUrl||""),e.find("#field-cc-cookie-text").val(i.cookiePolicyText||"Cookie Policy"),e.find("#field-cc-cookie-url").val(i.cookiePolicyUrl||""),e.find("#field-cc-position").val(i.position||"bottom"),e.find("#field-cc-layout").val(i.layout||"bar"),e.find("#field-cc-theme").val(i.theme||"dark"),e.find("#field-cc-show-functional").prop("checked",i.showFunctional!==!1),e.find("#field-cc-show-analytics").prop("checked",i.showAnalytics!==!1),e.find("#field-cc-show-marketing").prop("checked",i.showMarketing!==!1),e.find("#field-cc-version").val(i.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 m(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)":""})}m(n.position||"TL"),e.find(".bc-pos-btn").on("click",function(){const t=this.dataset.pos;e.find("#field-breadcrumbs-position").val(t),m(t)});let a=null;try{const{css:t}=await b("/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,r=s.selectionStart!==s.selectionEnd;if((t.key==="c"||t.key==="x")&&!r){t.preventDefault();const o=s.value,p=s.selectionStart,h=o.lastIndexOf(`
2
+ `,p-1)+1,u=o.indexOf(`
3
+ `,p),g=u===-1?o.slice(h):o.slice(h,u+1);if(navigator.clipboard.writeText(g),t.key==="x"){const k=o.slice(0,h),x=u===-1?"":o.slice(u+1);s.value=k+x,s.selectionStart=s.selectionEnd=h,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={title:e.find("#field-site-title").val().trim(),tagline:e.find("#field-tagline").val().trim(),...e.find("#field-admin-home option").length&&{adminHome:e.find("#field-admin-home").val()||"/"},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(),showMotionToggle:e.find("#field-footer-motion-toggle").prop("checked"),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"}},s=e.find("#save-settings-btn");s.prop("disabled",!0);try{await v.settings.save(t),document.dispatchEvent(new CustomEvent("dm:admin-home-changed")),E.toast("Settings saved.",{type:"success"})}catch{E.toast("Failed to save settings.",{type:"error"})}finally{s.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),r=e.find("#send-test-email-btn");r.prop("disabled",!0),s&&(s.textContent="Sending\u2026",s.style.color="");try{const o=await b("/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{r.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 v.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 b("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:t})}),E.toast("Custom CSS saved.",{type:"success"})}catch(r){E.toast(r.message||"Failed to save CSS.",{type:"error"})}finally{s.prop("disabled",!1)}});async function d(){try{const t=await b("/cache/status"),s=t.enabled?`enabled (${t.driver})`:"disabled",r=t.maxItems?` \xB7 ${t.size} / ${t.maxItems} entries`:"";e.find("#cache-status").text(s+r)}catch{e.find("#cache-status").text("unknown")}try{const{entries:t}=await b("/cache/keys");T(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 d(),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 b("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"}),await d()}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",d)}};function y(e){const f=document.getElementById("smtp-unconfigured-notice");f&&(f.style.display=String(e||"").trim()?"none":"",f.style.display===""&&Domma.icons.scan(f))}function T(e,f){const l=e.get(0);if(!l)return;for(;l.firstChild;)l.removeChild(l.firstChild);if(!f.length){const d=document.createElement("p");d.className="text-muted",d.style.fontSize=".875rem",d.textContent="Cache is empty.",l.appendChild(d);return}const c=[...f].reverse(),i=document.createElement("table");i.className="table table-compact";const n=document.createElement("thead"),m=document.createElement("tr");for(const d of["Key","Tags","Expires"]){const t=document.createElement("th");t.textContent=d,m.appendChild(t)}n.appendChild(m),i.appendChild(n);const a=document.createElement("tbody");for(const d of c){const t=document.createElement("tr"),s=document.createElement("td");s.style.fontFamily="monospace",s.style.fontSize="12px",s.textContent=d.key,t.appendChild(s);const r=document.createElement("td");r.style.fontSize="12px";for(const p of d.tags){const h=document.createElement("span");h.className="badge",h.style.marginRight="4px",h.style.fontSize="11px",h.textContent=p,r.appendChild(h)}t.appendChild(r);const o=document.createElement("td");if(o.style.fontSize="12px",o.style.color="var(--dm-text-muted, #6b7280)",d.expiresAt===null)o.textContent="no expiry";else{const p=d.expiresAt-Date.now();p<=0?o.textContent="expired":p<6e4?o.textContent=`${Math.round(p/1e3)}s`:p<36e5?o.textContent=`${Math.round(p/6e4)}m`:o.textContent=`${Math.round(p/36e5)}h`}t.appendChild(o),a.appendChild(t)}i.appendChild(a),l.appendChild(i)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.71.0",
3
+ "version": "0.72.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",
@@ -82,7 +82,7 @@
82
82
  "@fastify/rate-limit": "^10.3.0",
83
83
  "@fastify/static": "9.1.1",
84
84
  "bcryptjs": "^3.0.3",
85
- "domma-js": "0.44.1",
85
+ "domma-js": "0.44.3",
86
86
  "dotenv": "^17.2.3",
87
87
  "fastify": "5.8.5",
88
88
  "gray-matter": "^4.0.3",
@@ -9,8 +9,10 @@
9
9
  */
10
10
  import crypto from 'node:crypto';
11
11
  import {config, getConfig} from '../../config.js';
12
- import {hooks} from '../../services/hooks.js';
13
- import {authenticate} from '../../middleware/auth.js';
12
+ import {getRegisteredSidebarItems, hooks} from '../../services/hooks.js';
13
+ import {getMenu} from '../../services/menus.js';
14
+ import {resolveAdminHome} from '../../services/adminHome.js';
15
+ import {authenticate, checkVisibility} from '../../middleware/auth.js';
14
16
  import {getAdminScope, getPermissionsForRoles, getRoleLevel} from '../../services/roles.js';
15
17
  import {getEffectiveRegistry, GROUP_ORDER} from '../../services/permissionRegistry.js';
16
18
  import {getEffectiveRoles} from '../../services/userRoles.js';
@@ -126,7 +128,18 @@ export async function authRoutes(fastify) {
126
128
  const permissions = getPermissionsForRoles(roles, allResources);
127
129
  // adminScope: null = the whole admin; otherwise {home, allow} - the
128
130
  // screens this user's roles confine them to (see roles.getAdminScope).
129
- return {permissions, adminScope: getAdminScope(roles)};
131
+ const adminScope = getAdminScope(roles);
132
+ // adminHome: the screen `#/` opens for this user - see services/adminHome.js.
133
+ // The menu is read only when a site home has to be checked against it.
134
+ const adminHome = await resolveAdminHome({
135
+ scope: adminScope,
136
+ permissions,
137
+ siteHome: getConfig('site')?.adminHome,
138
+ registered: () => getRegisteredSidebarItems().map(e => e.item),
139
+ menuItems: async () => (await getMenu('admin-sidebar').catch(() => null))?.items || [],
140
+ canSee: (visibility) => checkVisibility(request.user, visibility)
141
+ });
142
+ return {permissions, adminScope, adminHome};
130
143
  });
131
144
 
132
145
  // GET /api/auth/permissions-registry - returns the full permissions registry,
@@ -11,6 +11,7 @@ import fs from 'fs/promises';
11
11
  import path from 'path';
12
12
  import {fileURLToPath} from 'url';
13
13
  import * as cache from '../../services/cache/index.js';
14
+ import {normaliseAdminPath} from '../../services/roles.js';
14
15
 
15
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
17
  const CUSTOM_CSS_FILE = path.resolve(__dirname, '../../../content/custom.css');
@@ -41,6 +42,8 @@ export async function settingsRoutes(fastify) {
41
42
  'smtp', 'footer', 'analytics', 'social', 'locale',
42
43
  'layoutOptions', 'seo', 'backToTop', 'cookieConsent', 'breadcrumbs', 'autoTheme',
43
44
  'adminBrand',
45
+ // The screen `#/` (Dashboard) opens - see services/adminHome.js.
46
+ 'adminHome',
44
47
  // Keys that legitimately live in site.json (written elsewhere - e.g.
45
48
  // `brand` by the menus migration) and are echoed back by GET /settings.
46
49
  // They must be accepted here or any client that round-trips the GET
@@ -51,6 +54,12 @@ export async function settingsRoutes(fastify) {
51
54
  if (unknownKeys.length > 0) {
52
55
  return reply.status(400).send({ error: `Unknown settings keys: ${unknownKeys.join(', ')}` });
53
56
  }
57
+ if ('adminHome' in data) {
58
+ // An admin route, or blank for the built-in dashboard.
59
+ const home = data.adminHome == null || data.adminHome === '' ? '/' : normaliseAdminPath(data.adminHome);
60
+ if (!home) return reply.status(400).send({error: 'adminHome must be an admin route such as /plugins/blog'});
61
+ data.adminHome = home;
62
+ }
54
63
  // Merge incoming data with existing config so partial updates (e.g. just adminBrand)
55
64
  // don't wipe unrelated keys. Nested objects get a shallow merge so sub-fields are
56
65
  // preserved even when only some sub-fields are sent.
@@ -9,7 +9,8 @@
9
9
  * `candidate`, say) used to get a 403 here and fall back to a hardcoded tree
10
10
  * holding a Menus link they could not open. Items gated by `visibility` are
11
11
  * dropped on the server, the same way the public navbar drops them; items
12
- * gated by `permission` are dropped by the renderer, which holds the list.
12
+ * gated by `permission`, and `hidden` items, are dropped by the renderer - it
13
+ * needs the hidden ones to know which Tools a menu has hidden.
13
14
  *
14
15
  * Auth middlewares are accepted as DI options so tests can supply no-ops.
15
16
  */
@@ -26,7 +27,11 @@ export async function sidebarRoutes(fastify, opts = {}) {
26
27
  fastify.get('/sidebar/menu', signedIn, async (request) => {
27
28
  const menu = await getMenu(ADMIN_SIDEBAR_SLUG);
28
29
  if (!menu) return {items: []};
29
- return {...menu, items: filterItemsForUser(menu.items || [], request.user)};
30
+ // Hidden items are kept: they are how a menu hides a Tool, and the
31
+ // renderer needs them to place Tools before it drops them. An item
32
+ // this user may not see (`visibility`) arrives as a label-less hidden
33
+ // stub for the same reason - see restrictedStub() in menus.js.
34
+ return {...menu, items: filterItemsForUser(menu.items || [], request.user, {keepHidden: true})};
30
35
  });
31
36
 
32
37
  fastify.get('/sidebar/registered-items', signedIn, async (request) => {
package/server/server.js CHANGED
@@ -280,8 +280,10 @@ try {
280
280
  }
281
281
 
282
282
  try {
283
- const {runMigration: runSidebarMigration, ensureSidebarItem, ensureDocumentationSubmenus} = await import('./services/sidebar-migration.js');
283
+ const {runMigration: runSidebarMigration, ensureSidebarItem, ensureDocumentationSubmenus, ensureFolderKeys} = await import('./services/sidebar-migration.js');
284
284
  await runSidebarMigration();
285
+ // Stable folder keys - what a Tool's default `folder` names.
286
+ await ensureFolderKeys();
285
287
  // Surface admin pages added after first boot on existing installs.
286
288
  await ensureSidebarItem({
287
289
  groupText: 'System',
@@ -510,19 +512,19 @@ await app.register(contactsRoutes, {prefix: '/api/contacts'});
510
512
  // Registered through the same channel plugins use, deliberately. The sidebar
511
513
  // menu (config/menus/admin-sidebar.json) is seeded once and file-existence
512
514
  // guarded, so adding an entry there would reach new sites only - every existing
513
- // site would simply never see Notes or Todo. Registered items are merged at
514
- // render time on every site, and groupPluginItems() files them under Tools,
515
- // which is exactly where a promoted plugin belongs.
515
+ // site would simply never see Notes or Todo. Registered items are placed at
516
+ // render time on every site (placeTools() in admin/js/lib/sidebar-grouping.js):
517
+ // in the Tools folder by default, or wherever an admin has moved them.
516
518
  //
517
519
  // `permission` is what hides each one from a role that cannot use it; the
518
520
  // renderer filters on it.
519
521
  const {registerSidebarItem} = await import('./services/hooks.js');
520
522
  // `countUrl` is the badge: the sidebar fetches it and shows {count, label, tone}
521
523
  // (see fetchCounts in admin/js/lib/sidebar-renderer.js).
522
- registerSidebarItem({item: {text: 'Notes', url: '#/notes', icon: 'edit-3', permission: 'notes', countUrl: '/api/notes/count'}});
523
- registerSidebarItem({item: {text: 'Todo', url: '#/todo', icon: 'check-square', permission: 'todo', countUrl: '/api/todo/count'}});
524
- registerSidebarItem({item: {text: 'Analytics', url: '#/analytics', icon: 'chart-bar', permission: 'analytics', countUrl: '/api/analytics/active-count'}});
525
- registerSidebarItem({item: {text: 'Contacts', url: '#/contacts', icon: 'users', permission: 'contacts'}});
524
+ registerSidebarItem({item: {source: 'Built in', text: 'Notes', url: '#/notes', icon: 'edit-3', permission: 'notes', countUrl: '/api/notes/count'}});
525
+ registerSidebarItem({item: {source: 'Built in', text: 'Todo', url: '#/todo', icon: 'check-square', permission: 'todo', countUrl: '/api/todo/count'}});
526
+ registerSidebarItem({item: {source: 'Built in', text: 'Analytics', url: '#/analytics', icon: 'chart-bar', permission: 'analytics', countUrl: '/api/analytics/active-count'}});
527
+ registerSidebarItem({item: {source: 'Built in', text: 'Contacts', url: '#/contacts', icon: 'users', permission: 'contacts'}});
526
528
 
527
529
  await registerPlugins(app);
528
530