domma-cms 1.0.0 → 1.2.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 h}from"../api.js";import{isPathAllowed as J}from"./admin-scope.js";import{colourToCss as U,applyFloat as Y,applyHighlight as Q,itemHighlightAttrs as ee}from"/public/js/menu-decor.mjs?v=20261009-hover-darken";import{placeTools as te,ensureLockedTools as ne,managePluginsItem as se,stripItemByUrl as ie,insertFoldersBeforeSystem as ae,pruneEmptyToolsFolder as oe,sidebarStateKey as re,canAny as B,filterByPermission as le,MANAGE_PLUGINS_URL as z}from"./sidebar-grouping.js";import{arrangeButtonMarkup as ce,canArrange as de,startArrange as ue}from"./sidebar-arrange.js";import{collapseButtonMarkup as me,wireSidebarSize as fe,COLLAPSED_WIDTH as ge}from"./sidebar-size.js";function p(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const pe=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];async function he(e){if(!B(e,"projects"))return[];let s=[];try{s=await h.projects.listWithCounts()}catch{return[]}if(!Array.isArray(s))return[];const i=[{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(t=>{const d="#/projects/"+encodeURIComponent(t.slug),c=t.counts||{},o=[{text:"Overview",url:d,icon:t.icon||"folder"}];for(const r of i){const l=Number(c[r.key])||0;l<=0||o.push({text:`${r.text} (${l})`,url:d+r.path,icon:r.icon})}o.push({text:"Settings",url:d+"/settings",icon:"settings"});const m=t.enabled===!1;return{text:t.name||t.slug,key:`project.${t.slug}`,icon:t.icon||"folder",items:o,...m&&{colour:"neutral",badge:{text:"off"}}}})}function be(e,s){for(const i of e)if(!i.hidden&&(i.key==="projects"||!i.key&&(i.text||"").toLowerCase()==="projects")){i.items=Array.isArray(i.items)?i.items.slice():[],i.items.push(...s);return}}function O(e,s){const i=D.includes(s.tone)?s.tone:"info",t=s.label||String(s.count);s.details?E.set(e,s.details):E.delete(e);const n=!s.inventory&&A.some(d=>d.url===e&&!d.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${i}" data-count-for="${p(e)}"${n} data-badge-label="${p(t)}" aria-label="${p(t)}">${p(String(s.count))}</span>`}function W(e){for(const s of e.querySelectorAll("details")){const i=s.querySelector(":scope > summary");if(!i)continue;let t=0,n=!1;const d=[];for(const l of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const u=Number(l.textContent)||0;u&&(t+=u,n||(n=l.classList.contains("sidebar-badge--danger")),d.push({text:l.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:l.dataset.badgeLabel,tone:l.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const c=`folder:${s.dataset.stateKey||i.textContent.trim()}`;let o=i.querySelector(":scope > .sidebar-badge--sum");if(!t){o?.remove(),E.delete(c);continue}o||(o=document.createElement("span"),i.querySelector(".sidebar-text")?.after(o));const m=i.querySelector(".sidebar-text")?.textContent.trim()||"This folder",r=d.map(l=>`${l.text}: ${l.meta}`).join(" \xB7 ");o.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${n?"danger":"info"}`,o.dataset.countFor=c,o.dataset.badgeLabel=r,o.setAttribute("aria-label",r),o.textContent=String(t),E.set(c,{title:m,items:d})}}const E=new Map;function ye(e,s){if(!e.url||!s)return"";const i=s[e.url];return!i||!(i.count>0)&&!i.showZero?"":O(e.url,i)}const D=["info","attention","danger","live"],ve={"#/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 _(e,{fallbackLabel:s="",tone:i="info"}={}){const t=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(t)||t<0)return null;const n=typeof e?.label=="string"&&e.label||(s?`${t} - ${s}`:String(t)),d={count:Math.round(t),label:n,tone:D.includes(e?.tone)?e.tone:i};return e?.details&&typeof e.details=="object"&&(d.details=e.details),e?.inventory===!0&&(d.inventory=!0),e?.showZero===!0&&(d.showZero=!0),d}function G(e,s,i=null){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="spacer"){const b=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${p(b)}" aria-hidden="true"></div>`}const t=String(e.text||"?").trim().charAt(0).toUpperCase()||"?",n=e.icon?`<span data-icon="${p(e.icon)}"></span>`:`<span class="sidebar-initial" aria-hidden="true">${p(t)}</span>`,d=p(e.text||""),c=e.url?p(e.url):"",o=Array.isArray(e.items)&&e.items.length>0,m=e.colour?U(e.colour):"",r=ee(e),l=[r.hover&&` data-menu-item-hover="${p(r.hover)}"`,r.active&&` data-menu-item-active="${p(r.active)}"`].filter(Boolean).join(""),u=[m&&`color:${p(m)}`,r.accent&&`--dm-menu-highlight:${p(r.accent)}`].filter(Boolean).join(";"),y=u?` style="${u}"`:"";let v="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const b=e.badge.variant?U(e.badge.variant):"";v=`<span class="dm-menu-badge"${b?` style="background:${p(b)};color:var(--dm-text-inverse,#fff)"`:""}>${p(String(e.badge.text))}</span>`}if(o){const b=re(e,i),C=S.get(b)!==!1,L=e.items.map(j=>G(j,s,b)).join("");return`<details data-state-key="${p(b)}"${C?" open":""}>
2
- <summary${y}>${n} <span class="sidebar-text">${d}</span>${v}</summary>
3
- <div class="sidebar-children">${L}</div>
4
- </details>`}return`<a href="${c}" class="sidebar-link" data-url="${c}"${y}${l}>${n} <span class="sidebar-text">${d}</span>${ye(e,s)}${v}</a>`}async function xe(e=[],s=[],i=null){const t=g=>g.then(a=>Array.isArray(a)?a.length:Array.isArray(a?.entries)?a.entries.length:0).catch(()=>0);A=Ce(e,i);const n=new Set(A.map(g=>g.url)),d={pages:"#/pages",media:"#/media",menus:"#/menus","context-menus":"#/context-menus",theme:"#/theme",collections:"#/collections",forms:"#/forms",views:"#/views",actions:"#/actions",blocks:"#/blocks",components:"#/components",users:"#/users",plugins:"#/plugins",notifications:"#/system/notifications"},c=(g,a)=>B(s,g)&&!n.has(d[g])?a():Promise.resolve(0),[o,m,r,l,u,y,v,b,C,L,j,F,N,P]=await Promise.all([c("pages",()=>t(h.pages.list())),c("media",()=>t(h.media.list())),c("menus",()=>t(h.menus.list())),c("context-menus",()=>t(h.contextMenus.list())),c("theme",()=>h.theme.get().then(g=>(g?.overrides||[]).length).catch(()=>0)),c("collections",()=>t(h.collections.list())),c("forms",()=>t(h.forms.list())),c("views",()=>t(h.views.list())),c("actions",()=>t(h.actions.list())),c("blocks",()=>t(h.blocks.list())),c("components",()=>t(h.components.list())),c("users",()=>t(h.users.list())),c("plugins",()=>t(h.plugins.list())),c("notifications",()=>h.system?.notifications?.unreadCount?.().then(g=>g?.count??0).catch(()=>0)??Promise.resolve(0))]),q={};await Promise.all(A.map(async g=>{const a=await K(g);a&&(q[g.url]=a)}));const T={"#/pages":o,"#/media":m,"#/menus":r,"#/context-menus":l,"#/theme":u,"#/collections":y,"#/forms":v,"#/views":b,"#/actions":C,"#/blocks":L,"#/components":j,"#/users":F,"#/plugins":N,"#/system/notifications":P},k={};for(const[g,a]of Object.entries(T)){if(n.has(g))continue;const[x,f]=ve[g]??["item","items"],w=_(a);w&&(k[g]={...w,label:`${w.count} ${w.count===1?x:f}`})}return{...k,...q}}let A=[];const $e="dm:sidebar-count",Se=6e4;function Ce(e=[],s=null){const i=[],t=n=>{if(n&&!(s&&typeof n.url=="string"&&n.url.startsWith("#/")&&!J(s,n.url.slice(1)))){n.url&&typeof n.countUrl=="string"&&n.countUrl&&i.push({url:n.url,countUrl:n.countUrl,text:n.text||"",countLabel:n.countLabel||"",inventory:n.inventory===!0});for(const d of[...n.children||[],...n.items||[]])t(d)}};for(const n of e??[])t(n?.item??n);return i}async function K(e){try{const s=new Date,i=`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(s.getDate()).padStart(2,"0")}`,t=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${i}`;return _(await H.get(t),{fallbackLabel:e.countLabel||e.text})}catch{return null}}function V(e,s,i){const t=[...e.querySelectorAll("a.sidebar-link")].find(d=>d.dataset.url===s);if(!t)return;const n=t.querySelector(".sidebar-badge[data-count-for]");if(!i||!(i.count>0)&&!i.showZero)n?.remove();else{const d=document.createRange();d.selectNodeContents(t);const c=d.createContextualFragment(O(s,i)).firstElementChild;n?n.replaceWith(c):t.querySelector(".sidebar-text")?.after(c)}R.set(s,i),W(e)}const R=new Map;async function I(e,s=null){const i=s?A.filter(t=>t.url===s):A;await Promise.all(i.map(async t=>{const n=await K(t),d=R.get(t.url);JSON.stringify(d??null)!==JSON.stringify(n)&&V(e,t.url,n)}))}function we(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener($e,s=>{const i=s.detail||{};if(i.url)if(i.count!==void 0){const t=A.find(n=>n.url===i.url);V(e,i.url,_(i,{fallbackLabel:t?.countLabel||t?.text||""}))}else I(e,i.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&I(e)},Se),document.addEventListener("visibilitychange",()=>{document.hidden||I(e)}),ke(e))}function Z(e,s,i){const t=(c,o,m)=>{const r=document.createElement(c);return o&&(r.className=o),m!=null&&(r.textContent=String(m)),r};if(!i){e.textContent=s;return}e.appendChild(t("strong","dm-badge-pop-title",i.title||s)),(i.rows?.length||!i.items?.length)&&e.appendChild(t("p","dm-badge-pop-sub",s));const n=Array.isArray(i.rows)?i.rows.slice(0,6):[];if(n.length){const c=t("dl","dm-badge-pop-rows");for(const o of n)c.appendChild(t("dt",null,o.label)),c.appendChild(t("dd",o.tone==="danger"?"is-danger":null,o.value));e.appendChild(c)}const d=Array.isArray(i.items)?i.items.slice(0,6):[];if(d.length){const c=t("ul","dm-badge-pop-items");for(const o of d){const m=t("li",o.tone==="danger"?"is-danger":null);m.appendChild(t("span","dm-badge-pop-text",o.text)),o.meta&&m.appendChild(t("span","dm-badge-pop-meta",o.meta)),c.appendChild(m)}e.appendChild(c)}else i.empty&&e.appendChild(t("p","dm-badge-pop-empty",i.empty))}function ke(e){let s=null,i=null;const t=()=>e.classList.contains("is-collapsed"),n=()=>{clearTimeout(i),s?.remove(),s=null},d=(r,l)=>{n(),i=setTimeout(()=>{s=document.createElement("div"),s.className="dm-help-pop dm-badge-pop",s.setAttribute("role","tooltip"),l(s),document.body.appendChild(s);const u=r.getBoundingClientRect(),y=s.getBoundingClientRect(),v=Math.min(u.right+10,innerWidth-y.width-8);s.style.left=`${Math.max(8,v)}px`,s.style.top=`${Math.max(8,Math.min(u.top+u.height/2-y.height/2,innerHeight-y.height-8))}px`},150)},c=r=>{const l=r?.dataset.badgeLabel;if(!l)return n();d(r,u=>Z(u,l,E.get(r.dataset.countFor)))},o=r=>{const l=r.querySelector(":scope > .sidebar-text")?.textContent.trim()||"",u=r.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");d(r,y=>{const v=u&&E.get(u.dataset.countFor);if(v)return Z(y,u.dataset.badgeLabel,{...v,title:v.title||l});if(!u){y.textContent=l;return}const b=document.createElement("strong");b.className="dm-badge-pop-title",b.textContent=l;const C=document.createElement("p");C.className="dm-badge-pop-sub",C.textContent=u.dataset.badgeLabel,y.append(b,C)})},m=r=>{const l=r.closest?.("a.sidebar-link, summary");return l&&e.contains(l)?l:null};e.addEventListener("mouseover",r=>{if(t()){const u=m(r.target);u&&!u.contains(r.relatedTarget)&&o(u);return}const l=r.target.closest?.(".sidebar-badge[data-badge-label]");l&&c(l)}),e.addEventListener("mouseout",r=>{if(t()){const l=m(r.target);l&&!l.contains(r.relatedTarget)&&n();return}r.target.closest?.(".sidebar-badge[data-badge-label]")&&n()}),e.addEventListener("focusin",r=>{const l=r.target.closest?.("a, summary");if(t()&&l&&e.contains(l)&&l.querySelector(":scope > .sidebar-text"))return o(l);const u=l?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");u?c(u):n()}),e.addEventListener("focusout",n),e.addEventListener("click",n),window.addEventListener("scroll",n,!0),window.addEventListener("hashchange",n)}async function Ae(e=[]){if(!B(e,"settings"))return"Admin";try{const s=await h.settings.get();return s?.adminBrand?.title||s?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:s,adminScope:i=null}){const t=$(e).get(0);if(!t)return;let n,d=null,c=null,o=null,m=null,r=null;try{const a=await h.get("/sidebar/menu");n=Array.isArray(a?.items)?a.items:null,d=a?.variant||null,c=a?.position||null,o=a?.style||null,m=a?.float||null,r=a?.highlight||null}catch{n=null}(!n||!n.some(a=>a&&(!a.hidden||a.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),n=pe.slice());let l=[];try{l=await H.get("/api/sidebar/registered-items")||[]}catch{}n=ie(n,z),n=te(n,l.filter(a=>(a?.item??a)?.url!==z)),n=ne(n),n=ae(n,[se()]);const u=await he(s);u.length&&be(n,u),n=le(n,s,i),n=oe(n);const[y,v]=await Promise.all([xe(l.filter(a=>B(s,a?.item?.permission)),s,i).catch(()=>({})),Ae(s)]),b=d?` dm-admin-sidebar--${p(d)}`:"";function C(a,x="px"){if(a==null)return a;const f=String(a).trim();return/^\d+(\.\d+)?$/.test(f)?`${f}${x}`:f}let L="";if(o){const a=[];o.fontFamily&&a.push(`font-family: ${o.fontFamily}, sans-serif`),o.fontSize&&a.push(`font-size: ${C(o.fontSize)}`),o.fontWeight&&a.push(`font-weight: ${o.fontWeight}`),o.letterSpacing&&a.push(`letter-spacing: ${C(o.letterSpacing,"em")}`);const x=[];if(a.length&&x.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 f=C(o.iconSize);x.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${f} !important; height: ${f} !important; }`)}x.length&&(L=`<style data-admin-sidebar-style>${x.join(" ")}</style>`)}const j=de(s)?ce():"",F=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span><span class="dm-admin-sidebar-title">${p(v)}</span>${j}${me()}</div>`,N=n.some(a=>a&&a.type==="spacer"&&a.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",P=`${L}<nav class="dm-admin-sidebar${b}${N}">${F}${n.map(a=>G(a,y)).join("")}</nav>`,q=document.createRange();q.selectNodeContents(t);const T=q.createContextualFragment(P);t.replaceChildren(T),Domma.icons.scan(t),t.querySelector(".dm-admin-sidebar-arrange")?.addEventListener("click",()=>ue(t));for(const a of A)R.set(a.url,y[a.url]??null);W(t),we(t),t.removeAttribute("data-menu-hover"),t.removeAttribute("data-menu-active"),t.style.removeProperty("--dm-menu-highlight"),Q(t,{highlight:r});const k=document.querySelector(".dashboard-wrapper");["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(a=>{t.style[a]=""}),t.classList.remove("dm-menu-floating"),k&&(k.style.paddingLeft="",k.style.paddingRight=""),t.__dmGutter=()=>{if(c!=="floating"||!k)return;const a=m&&m.anchor||"TL",x=Number.isFinite(Number(m?.offsetX))?Number(m.offsetX):16,f=t.classList.contains("is-collapsed")?ge:parseInt(t.style.width,10)||t.getBoundingClientRect().width||260,w=Math.round(f+x*2)+"px";a==="R"||a==="TR"||a==="BR"?k.style.paddingRight=w:k.style.paddingLeft=w},c==="floating"&&Y(t,{position:"floating",float:m||{}}),t.querySelectorAll("details").forEach(a=>{a.addEventListener("toggle",function(){const x=this.getAttribute("data-state-key");x&&S.set(x,this.open);const f=this.querySelector(":scope > .sidebar-children");if(f)if(this.open){const w=f.scrollHeight;f.style.maxHeight="0",requestAnimationFrame(()=>{f.style.maxHeight=w+"px",f.addEventListener("transitionend",function X(){f.style.maxHeight="none",f.removeEventListener("transitionend",X)})})}else{const w=f.scrollHeight;f.style.maxHeight=w+"px",requestAnimationFrame(()=>{f.style.maxHeight="0"})}})}),fe(t),t.dataset.routeWired||(t.dataset.routeWired="1",M.subscribe("router:afterChange",()=>t.__dmSetActive?.()));function g(){const a=location.hash||"#/";$(t).find(".sidebar-link").removeClass("active"),$(t).find(`.sidebar-link[data-url="${a}"]`).addClass("active")}g(),t.__dmSetActive=g}
1
+ import{api as h,getUser as O}from"../api.js";import{isPathAllowed as ie}from"./admin-scope.js";import{colourToCss as K,applyFloat as ae,applyHighlight as re,itemHighlightAttrs as le}from"/public/js/menu-decor.mjs?v=20261009-hover-darken";import{placeTools as ce,ensureLockedTools as de,managePluginsItem as ue,stripItemByUrl as me,insertFoldersBeforeSystem as fe,pruneEmptyToolsFolder as pe,sidebarStateKey as ge,canAny as _,filterByPermission as he,MANAGE_PLUGINS_URL as V}from"./sidebar-grouping.js";import{arrangeButtonMarkup as be,canArrange as ye,startArrange as ve}from"./sidebar-arrange.js";import{collapseButtonMarkup as xe,wireSidebarSize as $e,COLLAPSED_WIDTH as we}from"./sidebar-size.js";import{FREQUENT_MAX as X,addFrequent as Se,frequentFolder as Ce,moveFrequent as Z,readFrequent as U,removeFrequent as ke,writeFrequent as Ae}from"./sidebar-frequent.js";function g(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const Le=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];async function qe(e){if(!_(e,"projects"))return[];let n=[];try{n=await h.projects.listWithCounts()}catch{return[]}if(!Array.isArray(n))return[];const o=[{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 n.map(t=>{const d="#/projects/"+encodeURIComponent(t.slug),a=t.counts||{},i=[{text:"Overview",url:d,icon:t.icon||"folder"}];for(const l of o){const c=Number(a[l.key])||0;c<=0||i.push({text:`${l.text} (${c})`,url:d+l.path,icon:l.icon})}i.push({text:"Settings",url:d+"/settings",icon:"settings"});const u=t.enabled===!1;return{text:t.name||t.slug,key:`project.${t.slug}`,icon:t.icon||"folder",items:i,...u&&{colour:"neutral",badge:{text:"off"}}}})}function Fe(e,n){for(const o of e)if(!o.hidden&&(o.key==="projects"||!o.key&&(o.text||"").toLowerCase()==="projects")){o.items=Array.isArray(o.items)?o.items.slice():[],o.items.push(...n);return}}function Y(e,n){const o=Q.includes(n.tone)?n.tone:"info",t=n.label||String(n.count);n.details?F.set(e,n.details):F.delete(e);const s=!n.inventory&&k.some(d=>d.url===e&&!d.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${o}" data-count-for="${g(e)}"${s} data-badge-label="${g(t)}" aria-label="${g(t)}">${g(String(n.count))}</span>`}function J(e){for(const n of e.querySelectorAll("details")){const o=n.querySelector(":scope > summary");if(!o)continue;let t=0,s=!1;const d=[];for(const c of n.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const m=Number(c.textContent)||0;m&&(t+=m,s||(s=c.classList.contains("sidebar-badge--danger")),d.push({text:c.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:c.dataset.badgeLabel,tone:c.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const a=`folder:${n.dataset.stateKey||o.textContent.trim()}`;let i=o.querySelector(":scope > .sidebar-badge--sum");if(!t){i?.remove(),F.delete(a);continue}i||(i=document.createElement("span"),o.querySelector(".sidebar-text")?.after(i));const u=o.querySelector(".sidebar-text")?.textContent.trim()||"This folder",l=d.map(c=>`${c.text}: ${c.meta}`).join(" \xB7 ");i.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${s?"danger":"info"}`,i.dataset.countFor=a,i.dataset.badgeLabel=l,i.setAttribute("aria-label",l),i.textContent=String(t),F.set(a,{title:u,items:d})}}const F=new Map;function Ee(e,n){if(!e.url||!n)return"";const o=n[e.url];return!o||!(o.count>0)&&!o.showZero?"":Y(e.url,o)}const Q=["info","attention","danger","live"],Me={"#/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 z(e,{fallbackLabel:n="",tone:o="info"}={}){const t=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(t)||t<0)return null;const s=typeof e?.label=="string"&&e.label||(n?`${t} - ${n}`:String(t)),d={count:Math.round(t),label:s,tone:Q.includes(e?.tone)?e.tone:o};return e?.details&&typeof e.details=="object"&&(d.details=e.details),e?.inventory===!0&&(d.inventory=!0),e?.showZero===!0&&(d.showZero=!0),d}function ee(e,n,o=null){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="spacer"){const b=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${g(b)}" aria-hidden="true"></div>`}const t=String(e.text||"?").trim().charAt(0).toUpperCase()||"?",s=e.icon?`<span data-icon="${g(e.icon)}"></span>`:`<span class="sidebar-initial" aria-hidden="true">${g(t)}</span>`,d=g(e.text||""),a=e.url?g(e.url):"",i=Array.isArray(e.items)&&e.items.length>0,u=e.colour?K(e.colour):"",l=le(e),c=[l.hover&&` data-menu-item-hover="${g(l.hover)}"`,l.active&&` data-menu-item-active="${g(l.active)}"`].filter(Boolean).join(""),m=[u&&`color:${g(u)}`,l.accent&&`--dm-menu-highlight:${g(l.accent)}`].filter(Boolean).join(";"),y=m?` style="${m}"`:"";let v="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const b=e.badge.variant?K(e.badge.variant):"";v=`<span class="dm-menu-badge"${b?` style="background:${g(b)};color:var(--dm-text-inverse,#fff)"`:""}>${g(String(e.badge.text))}</span>`}if(i){const b=ge(e,o),C=S.get(b)!==!1,A=e.items.map(T=>ee(T,n,b)).join(""),L=e.frequent?' data-frequent title="Your Frequent Tools - right-click any Tool to add or remove it"':"";return`<details data-state-key="${g(b)}"${C?" open":""}${L}>
2
+ <summary${y}>${s} <span class="sidebar-text">${d}</span>${v}</summary>
3
+ <div class="sidebar-children">${A}</div>
4
+ </details>`}return`<a href="${a}" class="sidebar-link" data-url="${a}"${y}${c}>${s} <span class="sidebar-text">${d}</span>${Ee(e,n)}${v}</a>`}async function Te(e=[],n=[],o=null){const t=f=>f.then(x=>Array.isArray(x)?x.length:Array.isArray(x?.entries)?x.entries.length:0).catch(()=>0);k=Be(e,o);const s=new Set(k.map(f=>f.url)),d={pages:"#/pages",media:"#/media",menus:"#/menus","context-menus":"#/context-menus",theme:"#/theme",collections:"#/collections",forms:"#/forms",views:"#/views",actions:"#/actions",blocks:"#/blocks",components:"#/components",users:"#/users",plugins:"#/plugins",notifications:"#/system/notifications"},a=(f,x)=>_(n,f)&&!s.has(d[f])?x():Promise.resolve(0),[i,u,l,c,m,y,v,b,C,A,L,T,R,I]=await Promise.all([a("pages",()=>t(h.pages.list())),a("media",()=>t(h.media.list())),a("menus",()=>t(h.menus.list())),a("context-menus",()=>t(h.contextMenus.list())),a("theme",()=>h.theme.get().then(f=>(f?.overrides||[]).length).catch(()=>0)),a("collections",()=>t(h.collections.list())),a("forms",()=>t(h.forms.list())),a("views",()=>t(h.views.list())),a("actions",()=>t(h.actions.list())),a("blocks",()=>t(h.blocks.list())),a("components",()=>t(h.components.list())),a("users",()=>t(h.users.list())),a("plugins",()=>t(h.plugins.list())),a("notifications",()=>h.system?.notifications?.unreadCount?.().then(f=>f?.count??0).catch(()=>0)??Promise.resolve(0))]),j={};await Promise.all(k.map(async f=>{const x=await te(f);x&&(j[f.url]=x)}));const B={"#/pages":i,"#/media":u,"#/menus":l,"#/context-menus":c,"#/theme":m,"#/collections":y,"#/forms":v,"#/views":b,"#/actions":C,"#/blocks":A,"#/components":L,"#/users":T,"#/plugins":R,"#/system/notifications":I},P={};for(const[f,x]of Object.entries(B)){if(s.has(f))continue;const[r,w]=Me[f]??["item","items"],p=z(x);p&&(P[f]={...p,label:`${p.count} ${p.count===1?r:w}`})}return{...P,...j}}let k=[];const Ne="dm:sidebar-count",je=6e4;function Be(e=[],n=null){const o=[],t=s=>{if(s&&!(n&&typeof s.url=="string"&&s.url.startsWith("#/")&&!ie(n,s.url.slice(1)))){s.url&&typeof s.countUrl=="string"&&s.countUrl&&o.push({url:s.url,countUrl:s.countUrl,text:s.text||"",countLabel:s.countLabel||"",inventory:s.inventory===!0});for(const d of[...s.children||[],...s.items||[]])t(d)}};for(const s of e??[])t(s?.item??s);return o}async function te(e){try{const n=new Date,o=`${n.getFullYear()}-${String(n.getMonth()+1).padStart(2,"0")}-${String(n.getDate()).padStart(2,"0")}`,t=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${o}`;return z(await H.get(t),{fallbackLabel:e.countLabel||e.text})}catch{return null}}function ne(e,n,o){const t=[...e.querySelectorAll("a.sidebar-link")].filter(s=>s.dataset.url===n);if(t.length){for(const s of t){const d=s.querySelector(".sidebar-badge[data-count-for]");if(!o||!(o.count>0)&&!o.showZero)d?.remove();else{const a=document.createRange();a.selectNodeContents(s);const i=a.createContextualFragment(Y(n,o)).firstElementChild;d?d.replaceWith(i):s.querySelector(".sidebar-text")?.after(i)}}W.set(n,o),J(e)}}const W=new Map;async function D(e,n=null){const o=n?k.filter(t=>t.url===n):k;await Promise.all(o.map(async t=>{const s=await te(t),d=W.get(t.url);JSON.stringify(d??null)!==JSON.stringify(s)&&ne(e,t.url,s)}))}function Pe(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener(Ne,n=>{const o=n.detail||{};if(o.url)if(o.count!==void 0){const t=k.find(s=>s.url===o.url);ne(e,o.url,z(o,{fallbackLabel:t?.countLabel||t?.text||""}))}else D(e,o.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&D(e)},je),document.addEventListener("visibilitychange",()=>{document.hidden||D(e)}),_e(e))}function se(e,n,o){const t=(a,i,u)=>{const l=document.createElement(a);return i&&(l.className=i),u!=null&&(l.textContent=String(u)),l};if(!o){e.textContent=n;return}e.appendChild(t("strong","dm-badge-pop-title",o.title||n)),(o.rows?.length||!o.items?.length)&&e.appendChild(t("p","dm-badge-pop-sub",n));const s=Array.isArray(o.rows)?o.rows.slice(0,6):[];if(s.length){const a=t("dl","dm-badge-pop-rows");for(const i of s)a.appendChild(t("dt",null,i.label)),a.appendChild(t("dd",i.tone==="danger"?"is-danger":null,i.value));e.appendChild(a)}const d=Array.isArray(o.items)?o.items.slice(0,6):[];if(d.length){const a=t("ul","dm-badge-pop-items");for(const i of d){const u=t("li",i.tone==="danger"?"is-danger":null);u.appendChild(t("span","dm-badge-pop-text",i.text)),i.meta&&u.appendChild(t("span","dm-badge-pop-meta",i.meta)),a.appendChild(u)}e.appendChild(a)}else o.empty&&e.appendChild(t("p","dm-badge-pop-empty",o.empty))}function _e(e){let n=null,o=null;const t=()=>e.classList.contains("is-collapsed"),s=()=>{clearTimeout(o),n?.remove(),n=null},d=(l,c)=>{s(),o=setTimeout(()=>{n=document.createElement("div"),n.className="dm-help-pop dm-badge-pop",n.setAttribute("role","tooltip"),c(n),document.body.appendChild(n);const m=l.getBoundingClientRect(),y=n.getBoundingClientRect(),v=Math.min(m.right+10,innerWidth-y.width-8);n.style.left=`${Math.max(8,v)}px`,n.style.top=`${Math.max(8,Math.min(m.top+m.height/2-y.height/2,innerHeight-y.height-8))}px`},150)},a=l=>{const c=l?.dataset.badgeLabel;if(!c)return s();d(l,m=>se(m,c,F.get(l.dataset.countFor)))},i=l=>{const c=l.querySelector(":scope > .sidebar-text")?.textContent.trim()||"",m=l.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");d(l,y=>{const v=m&&F.get(m.dataset.countFor);if(v)return se(y,m.dataset.badgeLabel,{...v,title:v.title||c});if(!m){y.textContent=c;return}const b=document.createElement("strong");b.className="dm-badge-pop-title",b.textContent=c;const C=document.createElement("p");C.className="dm-badge-pop-sub",C.textContent=m.dataset.badgeLabel,y.append(b,C)})},u=l=>{const c=l.closest?.("a.sidebar-link, summary");return c&&e.contains(c)?c:null};e.addEventListener("mouseover",l=>{if(t()){const m=u(l.target);m&&!m.contains(l.relatedTarget)&&i(m);return}const c=l.target.closest?.(".sidebar-badge[data-badge-label]");c&&a(c)}),e.addEventListener("mouseout",l=>{if(t()){const c=u(l.target);c&&!c.contains(l.relatedTarget)&&s();return}l.target.closest?.(".sidebar-badge[data-badge-label]")&&s()}),e.addEventListener("focusin",l=>{const c=l.target.closest?.("a, summary");if(t()&&c&&e.contains(c)&&c.querySelector(":scope > .sidebar-text"))return i(c);const m=c?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");m?a(m):s()}),e.addEventListener("focusout",s),e.addEventListener("click",s),window.addEventListener("scroll",s,!0),window.addEventListener("hashchange",s)}async function He(e=[]){if(!_(e,"settings"))return"Admin";try{const n=await h.settings.get();return n?.adminBrand?.title||n?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:n,adminScope:o=null}){const t=$(e).get(0);if(!t)return;let s,d=null,a=null,i=null,u=null,l=null;try{const r=await h.get("/sidebar/menu");s=Array.isArray(r?.items)?r.items:null,d=r?.variant||null,a=r?.position||null,i=r?.style||null,u=r?.float||null,l=r?.highlight||null}catch{s=null}(!s||!s.some(r=>r&&(!r.hidden||r.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),s=Le.slice());let c=[];try{c=await H.get("/api/sidebar/registered-items")||[]}catch{}s=me(s,V),s=ce(s,c.filter(r=>(r?.item??r)?.url!==V)),s=de(s),s=fe(s,[ue()]);const m=await qe(n);m.length&&Fe(s,m),s=he(s,n,o),s=pe(s);const y=Ce(s,U(O()));y&&(s=[y,...s]);const[v,b]=await Promise.all([Te(c.filter(r=>_(n,r?.item?.permission)),n,o).catch(()=>({})),He(n)]),C=d?` dm-admin-sidebar--${g(d)}`:"";function A(r,w="px"){if(r==null)return r;const p=String(r).trim();return/^\d+(\.\d+)?$/.test(p)?`${p}${w}`:p}let L="";if(i){const r=[];i.fontFamily&&r.push(`font-family: ${i.fontFamily}, sans-serif`),i.fontSize&&r.push(`font-size: ${A(i.fontSize)}`),i.fontWeight&&r.push(`font-weight: ${i.fontWeight}`),i.letterSpacing&&r.push(`letter-spacing: ${A(i.letterSpacing,"em")}`);const w=[];if(r.length&&w.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 { ${r.join("; ")} }`),i.iconSize){const p=A(i.iconSize);w.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${p} !important; height: ${p} !important; }`)}w.length&&(L=`<style data-admin-sidebar-style>${w.join(" ")}</style>`)}const T=ye(n)?be():"",R=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span><span class="dm-admin-sidebar-title">${g(b)}</span>${T}${xe()}</div>`,I=s.some(r=>r&&r.type==="spacer"&&r.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",j=`${L}<nav class="dm-admin-sidebar${C}${I}">${R}${s.map(r=>ee(r,v)).join("")}</nav>`,B=document.createRange();B.selectNodeContents(t);const P=B.createContextualFragment(j);t.replaceChildren(P),Domma.icons.scan(t),t.querySelector(".dm-admin-sidebar-arrange")?.addEventListener("click",()=>ve(t));for(const r of k)W.set(r.url,v[r.url]??null);J(t),Pe(t),t.removeAttribute("data-menu-hover"),t.removeAttribute("data-menu-active"),t.style.removeProperty("--dm-menu-highlight"),re(t,{highlight:l});const f=document.querySelector(".dashboard-wrapper");["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(r=>{t.style[r]=""}),t.classList.remove("dm-menu-floating"),f&&(f.style.paddingLeft="",f.style.paddingRight=""),t.__dmGutter=()=>{if(a!=="floating"||!f)return;const r=u&&u.anchor||"TL",w=Number.isFinite(Number(u?.offsetX))?Number(u.offsetX):16,p=t.classList.contains("is-collapsed")?we:parseInt(t.style.width,10)||t.getBoundingClientRect().width||260,q=Math.round(p+w*2)+"px";r==="R"||r==="TR"||r==="BR"?f.style.paddingRight=q:f.style.paddingLeft=q},a==="floating"&&ae(t,{position:"floating",float:u||{}}),t.querySelectorAll("details").forEach(r=>{r.addEventListener("toggle",function(){const w=this.getAttribute("data-state-key");w&&S.set(w,this.open);const p=this.querySelector(":scope > .sidebar-children");if(p)if(this.open){const q=p.scrollHeight;p.style.maxHeight="0",requestAnimationFrame(()=>{p.style.maxHeight=q+"px",p.addEventListener("transitionend",function oe(){p.style.maxHeight="none",p.removeEventListener("transitionend",oe)})})}else{const q=p.scrollHeight;p.style.maxHeight=q+"px",requestAnimationFrame(()=>{p.style.maxHeight="0"})}})}),$e(t),Ue(t),t.dataset.routeWired||(t.dataset.routeWired="1",M.subscribe("router:afterChange",()=>t.__dmSetActive?.()));function x(){const r=location.hash||"#/";$(t).find(".sidebar-link").removeClass("active"),$(t).find(`.sidebar-link[data-url="${r}"]`).addClass("active")}x(),t.__dmSetActive=x}function Re(){try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}function G(e){return`${location.origin}${location.pathname}${e}`}function N(e,n){const o=O();if(!Ae(o,e(U(o)))){E.toast("Frequent could not be saved - this browser is blocking storage.",{type:"error"});return}n&&E.toast(n,{type:"success",duration:2500}),Re()}function Ie(e){const n=e.dataset.url;if(!n)return[];const o=e.querySelector(".sidebar-text")?.textContent.trim()||n,t=U(O()),s=t.includes(n),d=!!e.closest("details[data-frequent]"),a=t.indexOf(n),i=!s&&t.length>=X;return[{header:o},{label:"Open",icon:"arrow-right",action:()=>{location.hash=n}},{label:"Open in a new tab",icon:"external-link",action:()=>window.open(G(n),"_blank","noopener")},{type:"divider"},s?{label:"Remove from Frequent",icon:"star",action:()=>N(u=>ke(u,n),`${o} removed from Frequent.`)}:{label:i?`Frequent is full (${X})`:"Add to Frequent",icon:"star",disabled:i,action:()=>N(u=>Se(u,n),`${o} added to Frequent.`)},...d?[{label:"Move up",icon:"arrow-up",disabled:a<=0,action:()=>N(u=>Z(u,n,-1))},{label:"Move down",icon:"arrow-down",disabled:a<0||a>=t.length-1,action:()=>N(u=>Z(u,n,1))}]:[],{type:"divider"},{label:"Copy link",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(G(n)),E.toast("Link copied.",{type:"success",duration:2e3})}catch{E.toast(G(n),{type:"info",duration:8e3})}}}]}function Oe(e,n){const o=e.closest("details"),t=e.querySelector(".sidebar-text")?.textContent.trim()||"Folder",s=d=>n.querySelectorAll("details").forEach(a=>{a.open!==d&&(a.open=d)});return[{header:t},{label:o?.open?"Close":"Open",icon:o?.open?"chevron-up":"chevron-down",action:()=>{o&&(o.open=!o.open)}},{label:"Open every folder",icon:"chevrons-down",action:()=>s(!0)},{label:"Close every folder",icon:"chevrons-up",action:()=>s(!1)},...o?.hasAttribute("data-frequent")?[{type:"divider"},{label:"Clear Frequent",icon:"x",action:()=>N(()=>[],"Frequent cleared.")}]:[]]}function Ue(e){e.dataset.ctxWired||typeof E>"u"||!E.contextMenu||(e.dataset.ctxWired="1",E.contextMenu(e,{match:"a.sidebar-link, details > summary",inherit:!1,density:"compact",items:n=>n.matches("summary")?Oe(n,e):Ie(n)}))}
@@ -1 +1 @@
1
- export const TAB_FIELDS={general:["title","tagline","siteUrl","adminHome","spacerSize"],seo:["seoTitle","seoSeparator","seoDescription"],footer:["footerCopyright","footerMotionToggle","socialTwitter","socialFacebook","socialInstagram","socialLinkedin","socialGithub","socialYoutube"],email:["smtpHost","smtpPort","smtpUser","smtpPass","smtpSecure","smtpFromAddress","smtpFromName"],backToTop:["bttEnabled","bttSmooth","bttLabel","bttThreshold","bttPosition","bttOffset","bttBottomOffset"],cookies:["ccEnabled","ccMessage","ccAcceptAll","ccRejectAll","ccCustomize","ccSavePrefs","ccPrivacyText","ccPrivacyUrl","ccCookieText","ccCookieUrl","ccPosition","ccLayout","ccTheme","ccShowFunctional","ccShowAnalytics","ccShowMarketing","ccVersion"],breadcrumbs:["bcEnabled","bcHomeLabel","bcPosition","bcOffsetX","bcOffsetY"],css:["css"]},SECRET_FIELDS=["smtpPass"],BREADCRUMB_POSITIONS=["TL","TR","BL","BR"];const e=(t,c="")=>t==null?c:String(t),i=(t,c)=>{const s=Number.parseInt(t,10);return Number.isFinite(s)?s:c};export function homeValue(t){return/^\/[A-Za-z0-9\-_/]*$/.test(t||"")&&String(t).replace(/\/+$/,"")||"/"}export function formFromSettings(t={},c=""){const s=t.backToTop||{},o=t.cookieConsent||{},r=t.breadcrumbs||{};return{title:e(t.title),tagline:e(t.tagline),siteUrl:e(t.baseUrl),adminHome:homeValue(t.adminHome),spacerSize:e(t.layoutOptions?.spacerSize??40),seoTitle:e(t.seo?.defaultTitle),seoSeparator:e(t.seo?.titleSeparator," | "),seoDescription:e(t.seo?.defaultDescription),footerCopyright:e(t.footer?.copyright),footerMotionToggle:t.footer?.showMotionToggle!==!1,socialTwitter:e(t.social?.twitter),socialFacebook:e(t.social?.facebook),socialInstagram:e(t.social?.instagram),socialLinkedin:e(t.social?.linkedin),socialGithub:e(t.social?.github),socialYoutube:e(t.social?.youtube),smtpHost:e(t.smtp?.host),smtpPort:e(t.smtp?.port||587),smtpUser:e(t.smtp?.user),smtpPass:"",smtpSecure:t.smtp?.secure===!0,smtpFromAddress:e(t.smtp?.fromAddress),smtpFromName:e(t.smtp?.fromName),bttEnabled:s.enabled!==!1,bttSmooth:s.smooth!==!1,bttLabel:e(s.label),bttThreshold:e(s.scrollThreshold??300),bttPosition:e(s.position,"bottom-right"),bttOffset:e(s.offset??32),bttBottomOffset:e(s.bottomOffset??s.offset??32),ccEnabled:o.enabled!==!1,ccMessage:e(o.message),ccAcceptAll:e(o.acceptAllText,"Accept All"),ccRejectAll:e(o.rejectAllText,"Reject All"),ccCustomize:e(o.customizeText,"Customize"),ccSavePrefs:e(o.savePreferencesText,"Save Preferences"),ccPrivacyText:e(o.privacyPolicyText,"Privacy Policy"),ccPrivacyUrl:e(o.privacyPolicyUrl),ccCookieText:e(o.cookiePolicyText,"Cookie Policy"),ccCookieUrl:e(o.cookiePolicyUrl),ccPosition:e(o.position,"bottom"),ccLayout:e(o.layout,"bar"),ccTheme:e(o.theme,"dark"),ccShowFunctional:o.showFunctional!==!1,ccShowAnalytics:o.showAnalytics!==!1,ccShowMarketing:o.showMarketing!==!1,ccVersion:e(o.consentVersion,"1.0"),bcEnabled:r.enabled===!0,bcHomeLabel:e(r.homeLabel,"Home"),bcPosition:BREADCRUMB_POSITIONS.includes(r.position)?r.position:"TL",bcOffsetX:e(r.offsetX??8),bcOffsetY:e(r.offsetY??8),css:e(c)}}export function payloadFromForm(t,c={},{adminHomeReady:s=!0}={}){const o=r=>e(r).trim();return{title:o(t.title),tagline:o(t.tagline),baseUrl:o(t.siteUrl).replace(/\/+$/,""),...s&&{adminHome:o(t.adminHome)||"/"},layoutOptions:{spacerSize:i(t.spacerSize,40)||40},seo:{defaultTitle:o(t.seoTitle),titleSeparator:e(t.seoSeparator)||" | ",defaultDescription:o(t.seoDescription)},footer:{copyright:o(t.footerCopyright),showMotionToggle:t.footerMotionToggle===!0,links:c.footer?.links||[]},social:{twitter:o(t.socialTwitter),facebook:o(t.socialFacebook),instagram:o(t.socialInstagram),linkedin:o(t.socialLinkedin),github:o(t.socialGithub),youtube:o(t.socialYoutube)},smtp:{host:o(t.smtpHost),port:i(t.smtpPort,587)||587,user:o(t.smtpUser),pass:e(t.smtpPass),secure:t.smtpSecure===!0,fromAddress:o(t.smtpFromAddress),fromName:o(t.smtpFromName)},backToTop:{enabled:t.bttEnabled===!0,scrollThreshold:i(t.bttThreshold,300),position:e(t.bttPosition)||"bottom-right",offset:i(t.bttOffset,32),bottomOffset:i(t.bttBottomOffset,32),label:o(t.bttLabel),smooth:t.bttSmooth===!0},cookieConsent:{enabled:t.ccEnabled===!0,message:o(t.ccMessage),acceptAllText:o(t.ccAcceptAll),rejectAllText:o(t.ccRejectAll),customizeText:o(t.ccCustomize),savePreferencesText:o(t.ccSavePrefs),privacyPolicyText:o(t.ccPrivacyText),privacyPolicyUrl:o(t.ccPrivacyUrl),cookiePolicyText:o(t.ccCookieText),cookiePolicyUrl:o(t.ccCookieUrl),position:e(t.ccPosition)||"bottom",layout:e(t.ccLayout)||"bar",theme:e(t.ccTheme)||"dark",showFunctional:t.ccShowFunctional===!0,showAnalytics:t.ccShowAnalytics===!0,showMarketing:t.ccShowMarketing===!0,consentVersion:o(t.ccVersion)||"1.0"},breadcrumbs:{enabled:t.bcEnabled===!0,homeLabel:o(t.bcHomeLabel)||"Home",position:BREADCRUMB_POSITIONS.includes(t.bcPosition)?t.bcPosition:"TL",offsetX:i(t.bcOffsetX,8),offsetY:i(t.bcOffsetY,8)}}}export function siteUrlError(t){const c=e(t).trim().replace(/\/+$/,"");return c?/^https?:\/\//i.test(c)?/^https?:\/\/([a-z0-9.\-]+|\[[0-9a-f:.]+\])(:\d{1,5})?$/i.test(c)?"":"Just the address, like https://example.com - no page path after it.":"Start with https:// (or http://).":""}const a=t=>typeof t=="boolean"?t:e(t);export function snapshot(t){return JSON.stringify(Object.keys(t).sort().map(c=>[c,a(t[c])]))}export function changedTabs(t,c){const s=[];for(const[o,r]of Object.entries(TAB_FIELDS))r.some(n=>a(t[n])!==a(c[n]))&&s.push(o);return s}export function draftOf(t){return Object.fromEntries(Object.entries(t).filter(([c])=>!SECRET_FIELDS.includes(c)))}export function settingsProblems(t){const c=[];return e(t.smtpHost).trim()?e(t.smtpFromAddress).trim()||c.push({tone:"warn",tab:"email",text:"No From address - mail has no sender and many servers refuse it."}):c.push({tone:"danger",tab:"email",text:"No SMTP host - mail goes to a throwaway test inbox and reaches no one (password resets, form notifications)."}),t.ccEnabled===!0&&!e(t.ccPrivacyUrl).trim()&&c.push({tone:"warn",tab:"cookies",text:"The cookie banner is on but links to no privacy policy."}),c}export function describeCacheKey(t){const c=/^page:(.*?):m\d+:role([^:]*):/.exec(String(t||""));return c?{path:c[1]||"/",role:c[2]||"anon",page:!0}:{path:String(t||""),role:"",page:!1}}export function expiresIn(t,c=Date.now()){if(t==null)return"no expiry";const s=t-c;return s<=0?"expired":s<6e4?`${Math.round(s/1e3)}s`:s<36e5?`${Math.round(s/6e4)}m`:`${Math.round(s/36e5)}h`}
1
+ export const TAB_FIELDS={general:["title","tagline","siteUrl","adminHome","spacerSize","bodyClass"],seo:["seoTitle","seoSeparator","seoDescription"],footer:["footerCopyright","footerMotionToggle","footerClass","footerSocialClass","socialTwitter","socialFacebook","socialInstagram","socialLinkedin","socialGithub","socialYoutube"],email:["smtpHost","smtpPort","smtpUser","smtpPass","smtpSecure","smtpFromAddress","smtpFromName"],backToTop:["bttEnabled","bttSmooth","bttLabel","bttThreshold","bttPosition","bttOffset","bttBottomOffset"],cookies:["ccEnabled","ccMessage","ccAcceptAll","ccRejectAll","ccCustomize","ccSavePrefs","ccPrivacyText","ccPrivacyUrl","ccCookieText","ccCookieUrl","ccPosition","ccLayout","ccTheme","ccShowFunctional","ccShowAnalytics","ccShowMarketing","ccVersion"],breadcrumbs:["bcEnabled","bcHomeLabel","bcPosition","bcOffsetX","bcOffsetY"],css:["css"]},SECRET_FIELDS=["smtpPass"],BREADCRUMB_POSITIONS=["TL","TR","BL","BR"];const e=(t,s="")=>t==null?s:String(t),i=(t,s)=>{const c=Number.parseInt(t,10);return Number.isFinite(c)?c:s};export function homeValue(t){return/^\/[A-Za-z0-9\-_/]*$/.test(t||"")&&String(t).replace(/\/+$/,"")||"/"}export function formFromSettings(t={},s=""){const c=t.backToTop||{},o=t.cookieConsent||{},r=t.breadcrumbs||{};return{title:e(t.title),tagline:e(t.tagline),siteUrl:e(t.baseUrl),adminHome:homeValue(t.adminHome),spacerSize:e(t.layoutOptions?.spacerSize??40),bodyClass:e(t.bodyClass),seoTitle:e(t.seo?.defaultTitle),seoSeparator:e(t.seo?.titleSeparator," | "),seoDescription:e(t.seo?.defaultDescription),footerCopyright:e(t.footer?.copyright),footerMotionToggle:t.footer?.showMotionToggle!==!1,footerClass:e(t.footer?.class),footerSocialClass:e(t.footer?.socialClass),socialTwitter:e(t.social?.twitter),socialFacebook:e(t.social?.facebook),socialInstagram:e(t.social?.instagram),socialLinkedin:e(t.social?.linkedin),socialGithub:e(t.social?.github),socialYoutube:e(t.social?.youtube),smtpHost:e(t.smtp?.host),smtpPort:e(t.smtp?.port||587),smtpUser:e(t.smtp?.user),smtpPass:"",smtpSecure:t.smtp?.secure===!0,smtpFromAddress:e(t.smtp?.fromAddress),smtpFromName:e(t.smtp?.fromName),bttEnabled:c.enabled!==!1,bttSmooth:c.smooth!==!1,bttLabel:e(c.label),bttThreshold:e(c.scrollThreshold??300),bttPosition:e(c.position,"bottom-right"),bttOffset:e(c.offset??32),bttBottomOffset:e(c.bottomOffset??c.offset??32),ccEnabled:o.enabled!==!1,ccMessage:e(o.message),ccAcceptAll:e(o.acceptAllText,"Accept All"),ccRejectAll:e(o.rejectAllText,"Reject All"),ccCustomize:e(o.customizeText,"Customize"),ccSavePrefs:e(o.savePreferencesText,"Save Preferences"),ccPrivacyText:e(o.privacyPolicyText,"Privacy Policy"),ccPrivacyUrl:e(o.privacyPolicyUrl),ccCookieText:e(o.cookiePolicyText,"Cookie Policy"),ccCookieUrl:e(o.cookiePolicyUrl),ccPosition:e(o.position,"bottom"),ccLayout:e(o.layout,"bar"),ccTheme:e(o.theme,"dark"),ccShowFunctional:o.showFunctional!==!1,ccShowAnalytics:o.showAnalytics!==!1,ccShowMarketing:o.showMarketing!==!1,ccVersion:e(o.consentVersion,"1.0"),bcEnabled:r.enabled===!0,bcHomeLabel:e(r.homeLabel,"Home"),bcPosition:BREADCRUMB_POSITIONS.includes(r.position)?r.position:"TL",bcOffsetX:e(r.offsetX??8),bcOffsetY:e(r.offsetY??8),css:e(s)}}export function payloadFromForm(t,s={},{adminHomeReady:c=!0}={}){const o=r=>e(r).trim();return{title:o(t.title),tagline:o(t.tagline),baseUrl:o(t.siteUrl).replace(/\/+$/,""),...c&&{adminHome:o(t.adminHome)||"/"},layoutOptions:{spacerSize:i(t.spacerSize,40)||40},bodyClass:o(t.bodyClass),seo:{defaultTitle:o(t.seoTitle),titleSeparator:e(t.seoSeparator)||" | ",defaultDescription:o(t.seoDescription)},footer:{copyright:o(t.footerCopyright),showMotionToggle:t.footerMotionToggle===!0,links:s.footer?.links||[],class:o(t.footerClass),socialClass:o(t.footerSocialClass)},social:{twitter:o(t.socialTwitter),facebook:o(t.socialFacebook),instagram:o(t.socialInstagram),linkedin:o(t.socialLinkedin),github:o(t.socialGithub),youtube:o(t.socialYoutube)},smtp:{host:o(t.smtpHost),port:i(t.smtpPort,587)||587,user:o(t.smtpUser),pass:e(t.smtpPass),secure:t.smtpSecure===!0,fromAddress:o(t.smtpFromAddress),fromName:o(t.smtpFromName)},backToTop:{enabled:t.bttEnabled===!0,scrollThreshold:i(t.bttThreshold,300),position:e(t.bttPosition)||"bottom-right",offset:i(t.bttOffset,32),bottomOffset:i(t.bttBottomOffset,32),label:o(t.bttLabel),smooth:t.bttSmooth===!0},cookieConsent:{enabled:t.ccEnabled===!0,message:o(t.ccMessage),acceptAllText:o(t.ccAcceptAll),rejectAllText:o(t.ccRejectAll),customizeText:o(t.ccCustomize),savePreferencesText:o(t.ccSavePrefs),privacyPolicyText:o(t.ccPrivacyText),privacyPolicyUrl:o(t.ccPrivacyUrl),cookiePolicyText:o(t.ccCookieText),cookiePolicyUrl:o(t.ccCookieUrl),position:e(t.ccPosition)||"bottom",layout:e(t.ccLayout)||"bar",theme:e(t.ccTheme)||"dark",showFunctional:t.ccShowFunctional===!0,showAnalytics:t.ccShowAnalytics===!0,showMarketing:t.ccShowMarketing===!0,consentVersion:o(t.ccVersion)||"1.0"},breadcrumbs:{enabled:t.bcEnabled===!0,homeLabel:o(t.bcHomeLabel)||"Home",position:BREADCRUMB_POSITIONS.includes(t.bcPosition)?t.bcPosition:"TL",offsetX:i(t.bcOffsetX,8),offsetY:i(t.bcOffsetY,8)}}}export function siteUrlError(t){const s=e(t).trim().replace(/\/+$/,"");return s?/^https?:\/\//i.test(s)?/^https?:\/\/([a-z0-9.\-]+|\[[0-9a-f:.]+\])(:\d{1,5})?$/i.test(s)?"":"Just the address, like https://example.com - no page path after it.":"Start with https:// (or http://).":""}const a=t=>typeof t=="boolean"?t:e(t);export function snapshot(t){return JSON.stringify(Object.keys(t).sort().map(s=>[s,a(t[s])]))}export function changedTabs(t,s){const c=[];for(const[o,r]of Object.entries(TAB_FIELDS))r.some(n=>a(t[n])!==a(s[n]))&&c.push(o);return c}export function draftOf(t){return Object.fromEntries(Object.entries(t).filter(([s])=>!SECRET_FIELDS.includes(s)))}export function settingsProblems(t){const s=[];return e(t.smtpHost).trim()?e(t.smtpFromAddress).trim()||s.push({tone:"warn",tab:"email",text:"No From address - mail has no sender and many servers refuse it."}):s.push({tone:"danger",tab:"email",text:"No SMTP host - mail goes to a throwaway test inbox and reaches no one (password resets, form notifications)."}),t.ccEnabled===!0&&!e(t.ccPrivacyUrl).trim()&&s.push({tone:"warn",tab:"cookies",text:"The cookie banner is on but links to no privacy policy."}),s}export function describeCacheKey(t){const s=/^page:(.*?):m\d+:role([^:]*):/.exec(String(t||""));return s?{path:s[1]||"/",role:s[2]||"anon",page:!0}:{path:String(t||""),role:"",page:!1}}export function expiresIn(t,s=Date.now()){if(t==null)return"no expiry";const c=t-s;return c<=0?"expired":c<6e4?`${Math.round(c/1e3)}s`:c<36e5?`${Math.round(c/6e4)}m`:`${Math.round(c/36e5)}h`}
@@ -0,0 +1 @@
1
+ const h=["ok","warn","danger","info","attention","live"],w=t=>Number.isFinite(Number(t))?Number(t).toLocaleString("en-GB"):String(t??"");function i(t,e,r){const n=document.createElement(t);return e&&(n.className=e),r!=null&&r!==""&&(n.textContent=String(r)),n}function l(t){const e=t==="warn"?"attention":t;return h.includes(e)?` is-${e}`:""}function u(t,e){const r=i("div","dbp-rows");if(!t.length)return r.append(i("p","dbp-empty",e||"Nothing to show.")),r;for(const n of t){const a=i(n.href?"a":"div",`dbp-row${l(n.tone)}`);n.href&&/^#\//.test(n.href)&&(a.href=n.href);const o=i("span","dbp-row-main");o.append(i("span","dbp-row-text",n.text)),n.meta&&o.append(i("small","dbp-row-meta",n.meta)),a.append(o),n.value!==void 0&&n.value!==""&&a.append(i("span","dbp-row-value",n.value)),r.append(a)}return r}function g(t,e){const r=i("div",`dbp-figure${l(e.tone)}`);r.append(i("span","dbp-figure-value",w(e.count??0))),e.label&&r.append(i("span","dbp-figure-label",e.label)),t.append(r);const n=e.details||{};if(Array.isArray(n.rows)&&n.rows.length){const a=i("dl","dbp-facts");for(const o of n.rows.slice(0,4))a.append(i("dt","",o.label),i("dd",l(o.tone).trim(),o.value));t.append(a)}Array.isArray(n.items)&&n.items.length&&t.append(u(n.items.slice(0,5),""))}function v(t,e){const r=i("div",`dbp-figure${l(e.tone)}`);r.append(i("span","dbp-figure-value",e.value??"-")),e.label&&r.append(i("span","dbp-figure-label",e.label)),t.append(r),e.sub&&t.append(i("p","dbp-sub",e.sub))}function C(t,e){t.append(u(Array.isArray(e.items)?e.items.slice(0,12):[],e.empty))}let d=null;const y="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js";export function loadChart(){return window.Chart?Promise.resolve(!0):(d||(d=new Promise(t=>{const e=document.createElement("script");e.src=y,e.async=!0,e.onload=()=>t(!0),e.onerror=()=>{d=null,t(!1)},document.head.appendChild(e)})),d)}async function A(t,e){const r=Array.isArray(e.labels)?e.labels:[],n=Array.isArray(e.values)?e.values.map(p=>Number(p)||0):[];if(!n.some(p=>p>0)){t.append(i("p","dbp-empty","Nothing recorded yet."));return}const a=i("div","dbp-chart"),o=document.createElement("canvas");if(a.append(o),t.append(a),!await loadChart()||!o.isConnected){a.replaceWith(i("p","dbp-empty","The chart could not load."));return}const f=getComputedStyle(t),s=(p,b)=>f.getPropertyValue(p).trim()||b,c=s("--dm-primary","#5b8cff"),m=new window.Chart(o,{type:"line",data:{labels:r,datasets:[{label:e.label||"",data:n,borderColor:c,backgroundColor:/^#[0-9a-f]{6}$/i.test(c)?`${c}26`:"transparent",fill:!0,tension:.3,pointRadius:0,borderWidth:2}]},options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{display:!1}},scales:{x:{ticks:{maxTicksLimit:8,color:s("--dm-text-muted","#94a3b8")},grid:{color:s("--dm-border","rgba(127,127,127,.2)")}},y:{beginAtZero:!0,ticks:{precision:0,color:s("--dm-text-muted","#94a3b8")},grid:{color:s("--dm-border","rgba(127,127,127,.2)")}}}}});t.__dbpChart=m}const N={count:g,stat:v,list:C,chart:A};export async function drawWidget(t,e,r){destroyWidget(t),t.replaceChildren();const n=N[e];if(!n||!r||typeof r!="object"){t.append(i("p","dbp-empty","This widget has nothing to show."));return}await n(t,r)}export function drawError(t,e){destroyWidget(t),t.replaceChildren(i("p","dbp-empty is-error",e||"Could not load."))}export function destroyWidget(t){try{t.__dbpChart?.destroy()}catch{}t.__dbpChart=null}
@@ -171,6 +171,11 @@
171
171
  <input class="form-input" id="ctx-opacity" type="number" min="20" max="100" placeholder="100" data-model="opacity.value">
172
172
  <small class="cmx-err" data-bind-hidden="!err.value.opacity" data-bind-text="err.value.opacity"></small>
173
173
  </div>
174
+ <div>
175
+ <label class="form-label" for="ctx-class">CSS classes
176
+ <span data-help-title="CSS classes" data-help="Your own class names, separated by spaces, added to the menu panel - style them in Custom CSS. Each item can carry classes of its own too, in its editor."></span></label>
177
+ <input class="form-input" id="ctx-class" type="text" placeholder="brand-menu" autocomplete="off" spellcheck="false" data-model="className.value">
178
+ </div>
174
179
  </div>
175
180
  </div>
176
181
  </div>
@@ -0,0 +1,3 @@
1
+ <!-- My Dashboard - the Dashboard Pro board at its own address (admin/js/views/my-dashboard.js), for a site
2
+ whose `#/` belongs to another Tool (the fleet manager's Dashboard, a chosen admin home). -->
3
+ <div class="dbx"></div>
@@ -66,6 +66,11 @@
66
66
  <span data-help="The height a [spacer] shortcode gets when it is inserted from the page editor's toolbar."></span></label>
67
67
  <input id="stx-spacer" type="number" class="form-input" min="0" max="500" placeholder="40" data-model="spacerSize.value">
68
68
  </div>
69
+ <div class="stx-wide">
70
+ <label class="form-label" for="stx-body-class">Body CSS classes
71
+ <span data-help-title="Body CSS classes" data-help="Your own class names, separated by spaces, added to the body of every public page - a hook for site-wide styling in Custom CSS. Letters, digits, hyphens and underscores; no dots."></span></label>
72
+ <input id="stx-body-class" type="text" class="form-input" placeholder="brand-acme rounded-ui" autocomplete="off" spellcheck="false" data-model="bodyClass.value">
73
+ </div>
69
74
  </div>
70
75
  </div>
71
76
  <div class="stx-card stx-moved">
@@ -116,6 +121,16 @@
116
121
  <span data-help="An accessibility switch in the footer that lets visitors turn animations off. Turning this off only hides the switch - visitors who already chose it, and anyone whose browser asks for reduced motion, still get a calmer site."></span></label>
117
122
  <input id="stx-motion" type="checkbox" class="form-switch" data-model="footerMotionToggle.value">
118
123
  </div>
124
+ <div>
125
+ <label class="form-label" for="stx-footer-class">Footer CSS classes
126
+ <span data-help-title="Footer CSS classes" data-help="Your own class names, separated by spaces, added to the site footer. Style them in Custom CSS. The footer's menus take classes of their own in Menus."></span></label>
127
+ <input id="stx-footer-class" type="text" class="form-input" placeholder="footer-dark" autocomplete="off" spellcheck="false" data-model="footerClass.value">
128
+ </div>
129
+ <div>
130
+ <label class="form-label" for="stx-social-class">Social icon CSS classes
131
+ <span data-help-title="Social icon CSS classes" data-help="Your own class names, separated by spaces, added to every social media icon link in the footer."></span></label>
132
+ <input id="stx-social-class" type="text" class="form-input" placeholder="social-round" autocomplete="off" spellcheck="false" data-model="footerSocialClass.value">
133
+ </div>
119
134
  </div>
120
135
  </div>
121
136
  <div class="stx-card">
@@ -1,4 +1,4 @@
1
- import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin-chrome.js";import{helpPopovers as qe}from"../lib/help-popover.js";import{onceClosed as it,openMenuAt as Ne}from"../lib/tool-kit.js";import{makeIconInput as rt}from"/admin/js/lib/shortcode-modal.js";import{makePageInput as ct}from"/admin/js/lib/page-picker.js";import{ACCENTS as lt,ACTION_ARG as H,ACTION_GROUPS as dt,EDITABLE_TYPES as ut,NEEDS_TARGET as mt,SELF_DISABLING as pt,checkMenu as vt,duplicateItem as ft,formFromMenu as Re,insertItem as ht,itemForm as bt,itemFromForm as Fe,itemRow as yt,moveItem as Pe,newItem as gt,previewItems as xt,previewable as Ke,recordFromForm as kt,removeItem as Y,slugify as wt,snapshot as ue,splitList as Q,themeFromForm as Et}from"../lib/context-menu-editor-arrange.js";import{fmtDateTime as St}from"../lib/dates.js";import{rowPlaceKeeper as Mt}from"../lib/menu-drag.js";const At=/^\/context-menus\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,F=l=>`context-menu-editor.draft.${l||"new"}`,Tt=`
1
+ import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin-chrome.js";import{helpPopovers as qe}from"../lib/help-popover.js";import{onceClosed as it,openMenuAt as Ne}from"../lib/tool-kit.js";import{makeIconInput as rt}from"/admin/js/lib/shortcode-modal.js";import{makePageInput as ct}from"/admin/js/lib/page-picker.js";import{ACCENTS as lt,ACTION_ARG as B,ACTION_GROUPS as dt,EDITABLE_TYPES as ut,NEEDS_TARGET as mt,SELF_DISABLING as pt,checkMenu as vt,duplicateItem as ft,formFromMenu as Re,insertItem as ht,itemForm as bt,itemFromForm as Pe,itemRow as yt,moveItem as Fe,newItem as gt,previewItems as xt,previewable as Ke,recordFromForm as kt,removeItem as W,slugify as wt,snapshot as ue,splitList as Q,themeFromForm as Et}from"../lib/context-menu-editor-arrange.js";import{fmtDateTime as St}from"../lib/dates.js";import{rowPlaceKeeper as Mt}from"../lib/menu-drag.js";import{cssClassProblems as At}from"/public/js/css-classes.mjs";const Ct=/^\/context-menus\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,F=l=>`context-menu-editor.draft.${l||"new"}`,It=`
2
2
  <div class="cmx-row" data-index="{{index}}" tabindex="0" data-bind-class="cls">
3
3
  <span class="cmx-grip" title="Drag to reorder" aria-hidden="true"></span>
4
4
  <span class="cmx-icon" aria-hidden="true" data-name="{{icon}}"></span>
@@ -10,7 +10,7 @@ import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin
10
10
  <button type="button" class="cmx-down" data-bind-disabled="last" aria-label="Move down" title="Move down (Alt+\u2193)">\u2193</button>
11
11
  </span>
12
12
  <button type="button" class="cmx-kebab" aria-label="Actions for {{text}}" title="Actions">\u22EE</button>
13
- </div>`,It='<span class="cmx-chip" data-bind-class="cls">{{text}}</span>',Lt="<small>{{text}}</small>";function C(l){return String(l??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function Ct(l){const u=document.createElement("template");return u.innerHTML=l.trim(),u.content.firstElementChild}let X=null;function me(){if(X){try{X.destroy()}catch{}X=null}const l=document.getElementById("ctx-preview");if(l&&E.contextMenu&&E.contextMenu.registry)for(const u of E.contextMenu.registry(l))try{u.destroy()}catch{}}function pe(l,u){const c=l.querySelector(".dm-plugin-banner-name");if(!c)return;const L=c.querySelector(".dm-plugin-banner-sep");if(L){for(;L.nextSibling;)L.nextSibling.remove();c.appendChild(document.createTextNode(u))}}let w=null,He=!1;const Z=l=>String(l||"").split("?")[0].replace(/\/$/,"");function Dt(){He||!window.R?.use||(He=!0,R.use((l,u,c)=>!(At.test(u?.path||"")&&Z(l?.path)!==Z(u?.path))||!w?.dirty()?c():E.confirm("This context menu has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(A=>{A?c():history.replaceState(null,"",`#${u.path}`)})),M.subscribe("router:afterChange",({to:l}={})=>{if(w&&Z(l?.path)!==w.path){const u=w;w=null,u.dispose()}}))}export const contextMenuEditorView={templateUrl:"/admin/js/templates/context-menu-editor.html",async onMount(l){if(Dt(),w){const e=w;w=null,e.dispose()}me();const u=[],c=e=>(u.push(e),e);let L=!1;const A=l.get(0),m=A.querySelector(".cmx")||A,B=()=>!L&&m.isConnected,ve=location.hash.match(/^#\/context-menus\/edit\/([^?#]+)/),p=ve?decodeURIComponent(ve[1]):null,h=!p,Be=Z(location.hash.replace(/^#/,""));let x=null;if(!h){const e=E.loader(A,{type:"dots"});try{x=await de.contextMenus.get(p)}catch(n){e.destroy(),E.toast(`Could not load "${p}": ${n.message||n}`,{type:"error"});return}if(e.destroy(),!m.isConnected)return}const D=Re(x),r={};for(const[e,n]of Object.entries(D))r[e]=M.observable(n);const _=()=>Object.fromEntries(Object.keys(r).map(e=>[e,r[e].peek()])),fe=()=>Object.fromEntries(Object.keys(r).map(e=>[e,r[e].value])),s=M.observable(Array.isArray(x?.items)?x.items:[]),U=M.observable(ue(D,s.peek())),z=M.computed(()=>ue(fe(),s.value)!==U.value),G=M.observable(!1),V=M.observable(!1),P=M.observable([]),O=M.computed(()=>vt(fe(),s.value,{isNew:h})),_e=M.computed(()=>{const e=O.value.errors;if(V.value)return e;const n={};for(const t of Object.keys(e))t!=="items"&&String(r[t]?.value??"").trim()&&(n[t]=e[t]);return n}),he=M.computed(()=>{const e=V.value?O.value.errors:{};return{binding:!!(e.name||e.slug||e.selector),items:!!e.items||O.value.itemErrors.some(Boolean),appearance:!!(e.opacity||e.accent)}});let ee=!h;c(M.effect(()=>{const e=r.name.value;ee||(r.slug.value=wt(e))}));const te=M.observable([]);c(M.effect(()=>{const e=s.value,n=O.value.itemErrors;te.value=e.map((t,i)=>({...yt(t,i,e.length,n[i]||""),key:`${i}:${JSON.stringify(t)}`}))}));const b=m.querySelector("#ctx-items");c(M.effect(()=>{te.value,requestAnimationFrame(()=>{b?.isConnected&&(b.querySelectorAll(".cmx-icon[data-name]").forEach(e=>{const n=e.dataset.name;if(!n||e.dataset.drawn===n)return;e.dataset.drawn=n;const t=document.createElement("span");t.setAttribute("data-icon",n),t.setAttribute("data-icon-size","14"),e.replaceChildren(t)}),I.scan(b))})}));const Ue=M.computed(()=>{const e=[];e.push(r.enabled.value?{text:"On",cls:"is-ok"}:{text:"Off",cls:"is-warn"});const n=Q(r.pages.value).length,t=Q(r.projects.value).length;e.push({text:n||t?`${n+t} scope rule${n+t===1?"":"s"}`:"Site-wide",cls:""});const i=s.value.filter(a=>!["divider","header"].includes(a?.type||"item")).length;return e.push({text:`${i} item${i===1?"":"s"} to click`,cls:i?"":"is-danger"}),r.selector.value.trim()&&e.push({text:r.selector.value.trim(),cls:""}),e});let ne=null;function ze(){if(!B())return;me();const e=s.peek();if(!Ke(e))return;const n=Et(_());X=E.contextMenu("#ctx-preview",{cursor:"glyph",items:xt(e),inherit:!1,accent:n.accent,transition:n.transition,density:n.density,radius:n.radius,shadow:n.shadow,opacity:n.opacity})}c(M.effect(()=>{s.value,r.accent.value,r.transition.value,r.density.value,r.radius.value,r.shadow.value,r.opacity.value,clearTimeout(ne),ne=setTimeout(ze,120)})),c(()=>{clearTimeout(ne),me()});const Ge=M.computed(()=>Ke(s.value)?"Right-click here to try the menu":"Add an item to try the menu here"),ae=e=>be(e),k=e=>{s.value=e};function $(e){requestAnimationFrame(()=>b?.querySelector(`.cmx-row[data-index="${e}"]`)?.focus())}function j(e,n){n<0||n>=s.peek().length||(k(Pe(s.peek(),e,n)),$(n))}function q(e,n=s.peek().length){const t=gt(e);k(ht(s.peek(),n,t)),e!=="divider"&&be(n,{isAdd:!0})}function Ve(e){const n=s.peek(),t=n[e];if(!t)return[];const i=t.type||"item";return[{header:i==="divider"?"Divider":t.text||"(no text)"},{label:i==="divider"?"Change\u2026":"Edit\u2026",icon:"edit",action:()=>ae(e)},{type:"divider"},{label:"Move up",icon:"arrow-up",shortcut:"Alt+\u2191",disabled:e===0,action:()=>j(e,e-1)},{label:"Move down",icon:"arrow-down",shortcut:"Alt+\u2193",disabled:e===n.length-1,action:()=>j(e,e+1)},{label:"Duplicate",icon:"copy",action:()=>{k(ft(s.peek(),e)),$(e+1)}},{label:"Add below",icon:"plus",submenu:[{label:"Item",action:()=>q("item",e+1)},{label:"Heading",action:()=>q("header",e+1)},{label:"Divider",action:()=>q("divider",e+1)}]},{type:"divider"},{label:"Remove",icon:"trash",danger:!0,action:()=>{k(Y(s.peek(),e)),$(Math.max(0,e-1))}}]}let v=null;function be(e,{isAdd:n=!1}={}){const t=s.peek()[e];if(!t)return;if(v)try{v.so.close(!0)}catch{}const i=bt(t),a={};for(const[o,d]of Object.entries(i))a[o]=M.observable(d);const J=()=>Object.fromEntries(Object.keys(a).map(o=>[o,a[o].peek()])),Ze=JSON.stringify(i),se=()=>JSON.stringify(J())!==Ze,Ae=!ut.includes(i.type),ie=Array.isArray(t.submenu)&&t.submenu.length,Te={...a,locked:Ae,notLocked:!Ae,hasSub:ie,subText:ie?`Opens a submenu of ${t.submenu.length} - kept as it is (edit it in the file).`:"",isItem:M.computed(()=>a.type.value!=="divider"&&a.type.value!=="header"),hasText:M.computed(()=>a.type.value!=="divider"),showAction:M.computed(()=>a.type.value!=="divider"&&a.type.value!=="header"&&!ie),argLabel:M.computed(()=>H[a.action.value]?.label||""),note:M.computed(()=>pt[a.action.value]||mt[a.action.value]||""),problem:M.computed(()=>{const o=Fe(Object.fromEntries(Object.keys(a).map(d=>[d,a[d].value])),t);return o.type==="divider"?"":String(o.text||"").trim()?H[o.action?.type]?.required&&!String(o.action?.[H[o.action.type].key]||"").trim()?`Needs a ${H[o.action.type].label.toLowerCase()}.`:"":"Needs text."})},g=E.slideover({title:n?"New item":i.type==="divider"?"Divider":`Edit ${C(i.text||"item")}`,size:"sm",position:"right"}),et=dt.map(o=>`<optgroup label="${C(o.label)}">${Object.entries(o.actions).map(([d,W])=>`<option value="${C(d)}">${C(W)}</option>`).join("")}</optgroup>`).join(""),T=Ct(`
13
+ </div>`,Tt='<span class="cmx-chip" data-bind-class="cls">{{text}}</span>',Lt="<small>{{text}}</small>";function $(l){return String(l??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function $t(l){const u=document.createElement("template");return u.innerHTML=l.trim(),u.content.firstElementChild}let X=null;function me(){if(X){try{X.destroy()}catch{}X=null}const l=document.getElementById("ctx-preview");if(l&&E.contextMenu&&E.contextMenu.registry)for(const u of E.contextMenu.registry(l))try{u.destroy()}catch{}}function pe(l,u){const c=l.querySelector(".dm-plugin-banner-name");if(!c)return;const T=c.querySelector(".dm-plugin-banner-sep");if(T){for(;T.nextSibling;)T.nextSibling.remove();c.appendChild(document.createTextNode(u))}}let w=null,He=!1;const Z=l=>String(l||"").split("?")[0].replace(/\/$/,"");function Dt(){He||!window.R?.use||(He=!0,R.use((l,u,c)=>!(Ct.test(u?.path||"")&&Z(l?.path)!==Z(u?.path))||!w?.dirty()?c():E.confirm("This context menu has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(A=>{A?c():history.replaceState(null,"",`#${u.path}`)})),M.subscribe("router:afterChange",({to:l}={})=>{if(w&&Z(l?.path)!==w.path){const u=w;w=null,u.dispose()}}))}export const contextMenuEditorView={templateUrl:"/admin/js/templates/context-menu-editor.html",async onMount(l){if(Dt(),w){const e=w;w=null,e.dispose()}me();const u=[],c=e=>(u.push(e),e);let T=!1;const A=l.get(0),m=A.querySelector(".cmx")||A,_=()=>!T&&m.isConnected,ve=location.hash.match(/^#\/context-menus\/edit\/([^?#]+)/),p=ve?decodeURIComponent(ve[1]):null,h=!p,Be=Z(location.hash.replace(/^#/,""));let x=null;if(!h){const e=E.loader(A,{type:"dots"});try{x=await de.contextMenus.get(p)}catch(n){e.destroy(),E.toast(`Could not load "${p}": ${n.message||n}`,{type:"error"});return}if(e.destroy(),!m.isConnected)return}const D=Re(x),r={};for(const[e,n]of Object.entries(D))r[e]=M.observable(n);const U=()=>Object.fromEntries(Object.keys(r).map(e=>[e,r[e].peek()])),fe=()=>Object.fromEntries(Object.keys(r).map(e=>[e,r[e].value])),s=M.observable(Array.isArray(x?.items)?x.items:[]),z=M.observable(ue(D,s.peek())),G=M.computed(()=>ue(fe(),s.value)!==z.value),V=M.observable(!1),Y=M.observable(!1),K=M.observable([]),O=M.computed(()=>vt(fe(),s.value,{isNew:h})),_e=M.computed(()=>{const e=O.value.errors;if(Y.value)return e;const n={};for(const t of Object.keys(e))t!=="items"&&String(r[t]?.value??"").trim()&&(n[t]=e[t]);return n}),he=M.computed(()=>{const e=Y.value?O.value.errors:{};return{binding:!!(e.name||e.slug||e.selector),items:!!e.items||O.value.itemErrors.some(Boolean),appearance:!!(e.opacity||e.accent)}});let ee=!h;c(M.effect(()=>{const e=r.name.value;ee||(r.slug.value=wt(e))}));const te=M.observable([]);c(M.effect(()=>{const e=s.value,n=O.value.itemErrors;te.value=e.map((t,i)=>({...yt(t,i,e.length,n[i]||""),key:`${i}:${JSON.stringify(t)}`}))}));const b=m.querySelector("#ctx-items");c(M.effect(()=>{te.value,requestAnimationFrame(()=>{b?.isConnected&&(b.querySelectorAll(".cmx-icon[data-name]").forEach(e=>{const n=e.dataset.name;if(!n||e.dataset.drawn===n)return;e.dataset.drawn=n;const t=document.createElement("span");t.setAttribute("data-icon",n),t.setAttribute("data-icon-size","14"),e.replaceChildren(t)}),I.scan(b))})}));const Ue=M.computed(()=>{const e=[];e.push(r.enabled.value?{text:"On",cls:"is-ok"}:{text:"Off",cls:"is-warn"});const n=Q(r.pages.value).length,t=Q(r.projects.value).length;e.push({text:n||t?`${n+t} scope rule${n+t===1?"":"s"}`:"Site-wide",cls:""});const i=s.value.filter(a=>!["divider","header"].includes(a?.type||"item")).length;return e.push({text:`${i} item${i===1?"":"s"} to click`,cls:i?"":"is-danger"}),r.selector.value.trim()&&e.push({text:r.selector.value.trim(),cls:""}),e});let ne=null;function ze(){if(!_())return;me();const e=s.peek();if(!Ke(e))return;const n=Et(U());X=E.contextMenu("#ctx-preview",{cursor:"glyph",items:xt(e),inherit:!1,accent:n.accent,transition:n.transition,density:n.density,radius:n.radius,shadow:n.shadow,opacity:n.opacity})}c(M.effect(()=>{s.value,r.accent.value,r.transition.value,r.density.value,r.radius.value,r.shadow.value,r.opacity.value,clearTimeout(ne),ne=setTimeout(ze,120)})),c(()=>{clearTimeout(ne),me()});const Ge=M.computed(()=>Ke(s.value)?"Right-click here to try the menu":"Add an item to try the menu here"),ae=e=>be(e),k=e=>{s.value=e};function j(e){requestAnimationFrame(()=>b?.querySelector(`.cmx-row[data-index="${e}"]`)?.focus())}function q(e,n){n<0||n>=s.peek().length||(k(Fe(s.peek(),e,n)),j(n))}function N(e,n=s.peek().length){const t=gt(e);k(ht(s.peek(),n,t)),e!=="divider"&&be(n,{isAdd:!0})}function Ve(e){const n=s.peek(),t=n[e];if(!t)return[];const i=t.type||"item";return[{header:i==="divider"?"Divider":t.text||"(no text)"},{label:i==="divider"?"Change\u2026":"Edit\u2026",icon:"edit",action:()=>ae(e)},{type:"divider"},{label:"Move up",icon:"arrow-up",shortcut:"Alt+\u2191",disabled:e===0,action:()=>q(e,e-1)},{label:"Move down",icon:"arrow-down",shortcut:"Alt+\u2193",disabled:e===n.length-1,action:()=>q(e,e+1)},{label:"Duplicate",icon:"copy",action:()=>{k(ft(s.peek(),e)),j(e+1)}},{label:"Add below",icon:"plus",submenu:[{label:"Item",action:()=>N("item",e+1)},{label:"Heading",action:()=>N("header",e+1)},{label:"Divider",action:()=>N("divider",e+1)}]},{type:"divider"},{label:"Remove",icon:"trash",danger:!0,action:()=>{k(W(s.peek(),e)),j(Math.max(0,e-1))}}]}let v=null;function be(e,{isAdd:n=!1}={}){const t=s.peek()[e];if(!t)return;if(v)try{v.so.close(!0)}catch{}const i=bt(t),a={};for(const[o,d]of Object.entries(i))a[o]=M.observable(d);const J=()=>Object.fromEntries(Object.keys(a).map(o=>[o,a[o].peek()])),Ze=JSON.stringify(i),se=()=>JSON.stringify(J())!==Ze,Ae=!ut.includes(i.type),ie=Array.isArray(t.submenu)&&t.submenu.length,Ce={...a,locked:Ae,notLocked:!Ae,hasSub:ie,subText:ie?`Opens a submenu of ${t.submenu.length} - kept as it is (edit it in the file).`:"",isItem:M.computed(()=>a.type.value!=="divider"&&a.type.value!=="header"),hasText:M.computed(()=>a.type.value!=="divider"),showAction:M.computed(()=>a.type.value!=="divider"&&a.type.value!=="header"&&!ie),argLabel:M.computed(()=>B[a.action.value]?.label||""),note:M.computed(()=>pt[a.action.value]||mt[a.action.value]||""),problem:M.computed(()=>{const o=Pe(Object.fromEntries(Object.keys(a).map(L=>[L,a[L].value])),t),d=At(o.class)[0];return o.type==="divider"?"":String(o.text||"").trim()?d?`CSS classes: ${d}.`:B[o.action?.type]?.required&&!String(o.action?.[B[o.action.type].key]||"").trim()?`Needs a ${B[o.action.type].label.toLowerCase()}.`:"":"Needs text."})},g=E.slideover({title:n?"New item":i.type==="divider"?"Divider":`Edit ${$(i.text||"item")}`,size:"sm",position:"right"}),et=dt.map(o=>`<optgroup label="${$(o.label)}">${Object.entries(o.actions).map(([d,L])=>`<option value="${$(d)}">${$(L)}</option>`).join("")}</optgroup>`).join(""),C=$t(`
14
14
  <div class="cmx-form">
15
15
  <div data-bind-hidden="locked">
16
16
  <label class="form-label" for="cmx-i-type">Type
@@ -19,7 +19,7 @@ import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin
19
19
  <option value="item">Item</option><option value="header">Heading</option><option value="divider">Divider</option>
20
20
  </select>
21
21
  </div>
22
- <p class="cmx-note" data-bind-hidden="notLocked">A ${C(i.type)} item - its text, icon and action can be changed here; its type is kept.</p>
22
+ <p class="cmx-note" data-bind-hidden="notLocked">A ${$(i.type)} item - its text, icon and action can be changed here; its type is kept.</p>
23
23
  <div data-bind-hidden="!hasText.value">
24
24
  <label class="form-label" for="cmx-i-text">Text</label>
25
25
  <input id="cmx-i-text" type="text" class="form-input" maxlength="80" data-model="text.value">
@@ -28,7 +28,7 @@ import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin
28
28
  <label class="form-label" for="cmx-i-icon">Icon <small class="cmx-hint-inline">(optional)</small></label>
29
29
  <div class="cmx-icon-mount"></div>
30
30
  </div>
31
- <p class="cmx-note" data-bind-hidden="!hasSub">${C(Te.subText)}</p>
31
+ <p class="cmx-note" data-bind-hidden="!hasSub">${$(Ce.subText)}</p>
32
32
  <div data-bind-hidden="!showAction.value">
33
33
  <label class="form-label" for="cmx-i-action">Does
34
34
  <span data-help-title="What it does" data-help="A fixed set of actions, on purpose: nothing typed here runs as script on your pages. Some grey themselves out when they do not apply to the click - the note under the choice says when."></span></label>
@@ -39,6 +39,11 @@ import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin
39
39
  <label class="form-label" for="cmx-i-arg" data-bind-text="argLabel.value">Argument</label>
40
40
  <div class="cmx-arg-mount"></div>
41
41
  </div>
42
+ <div data-bind-hidden="!isItem.value">
43
+ <label class="form-label" for="cmx-i-class">CSS classes <small class="cmx-hint-inline">(optional)</small>
44
+ <span data-help="Your own class names, separated by spaces, added to this item - style them in Custom CSS."></span></label>
45
+ <input id="cmx-i-class" type="text" class="form-input" placeholder="menu-cta" autocomplete="off" spellcheck="false" data-model="className.value">
46
+ </div>
42
47
  <div class="cmx-setting" data-bind-hidden="!isItem.value">
43
48
  <label class="form-label" for="cmx-i-danger">Destructive (red)
44
49
  <span data-help="Drawn in red, for things like Delete - a warning, not a guard."></span></label>
@@ -52,4 +57,4 @@ import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin
52
57
  <button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
53
58
  <button type="button" class="btn btn-sm btn-primary" data-on-click="done"><span data-icon="check" data-icon-size="13"></span> Done</button>
54
59
  </div>
55
- </div>`),N=rt("edit",i.icon);N.input.id="cmx-i-icon",N.input.classList.add("form-input");const Ie=()=>{a.icon.value=N.input.value};N.input.addEventListener("input",Ie),T.querySelector(".cmx-icon-mount").appendChild(N.el);const re=T.querySelector(".cmx-arg-mount");let f=null;const ce=()=>{a.arg.value=f.value};function Le(){f?.removeEventListener("input",ce),re.replaceChildren();const o=H[a.action.peek()];if(!o){f=null;return}if(o.key==="url"){const d=ct(o.placeholder,a.arg.peek());f=d.input,re.appendChild(d.el)}else f=document.createElement("input"),f.type="text",f.className="form-input",f.placeholder=o.placeholder,f.value=a.arg.peek(),re.appendChild(f);f.id="cmx-i-arg",f.addEventListener("input",ce)}let Ce=a.action.peek();const tt=M.effect(()=>{const o=a.action.value;o!==Ce&&(Ce=o,a.arg.value="",Le())});Le();const De=()=>{const o=Fe(J(),t),d=[...s.peek()],W=d.indexOf(t);W!==-1&&(d[W]=o,k(d))};let K=!1,le=!1;const Oe={done:()=>{De(),K=!0,g.close(),$(e)},cancel:()=>g.close(),remove:()=>{const o=s.peek().indexOf(t);o!==-1&&k(Y(s.peek(),o)),K=!0,g.close()}};g.element.appendChild(T);const nt=qe(T),at=M.applyBindings(Te,T,{methods:Oe}),ot=o=>{(o.ctrlKey||o.metaKey)&&o.key==="Enter"&&(o.preventDefault(),Oe.done())};T.addEventListener("keydown",ot);const $e={so:g,dirty:se,apply:De};v=$e,it(g,()=>{tt(),at.dispose(),nt(),N.input.removeEventListener("input",Ie),f?.removeEventListener("input",ce),v===$e&&(v=null)});const st=g.close.bind(g);g.close=async o=>{if(!K&&o!==!0&&se()){if(le)return;le=!0;const d=await E.confirm("Discard the changes to this item?");if(le=!1,!d)return}if(!K&&n&&o!==!0&&!se()){const d=s.peek().indexOf(t);d!==-1&&k(Y(s.peek(),d))}return K=!0,st()},I.scan(T),g.open(),requestAnimationFrame(()=>T.querySelector(i.type==="divider"?"#cmx-i-type":"#cmx-i-text")?.focus())}c(()=>{if(v){const e=v;e.dirty()&&e.apply();try{e.so.close(!0)}catch{}}});async function oe(){if(G.peek())return;V.value=!0,P.value=[];const e=O.peek();if(!e.ok){const t=Object.keys(e.errors).length;E.toast(`Not saved yet - ${t===1?"one thing needs":`${t} things need`} fixing (marked in red).`,{type:"warning"});const i=he.peek(),a=i.binding?0:i.items?1:i.appearance?2:-1;a>=0&&Ye?.activate?.(a);return}G.value=!0;const n=kt(_(),s.peek());try{if(h)await de.contextMenus.create(n);else{const t=await de.contextMenus.update(p,n);t?.items&&(s.value=t.items)}if(!B())return;U.value=ue(_(),s.peek());try{S.remove?S.remove(F(p)):S.set(F(p),null)}catch{}window.dispatchEvent(new CustomEvent("dm:sidebar-count",{detail:{url:"#/context-menus"}})),h?(E.toast("Context menu created.",{type:"success"}),location.hash="#/context-menus"):(E.toast("Context menu saved.",{type:"success"}),pe(A,r.name.peek()||p))}catch(t){P.value=String(t.message||t).split("; ").map((i,a)=>({key:String(a),text:i})),m.querySelector(".cmx-alert")?.scrollIntoView({behavior:"smooth",block:"nearest"})}finally{G.value=!1}}function Je(){E.confirm("Throw away your unsaved changes to this menu?").then(e=>{if(!e||!B())return;const n=Re(x);for(const[t,i]of Object.entries(n))r[t].value=i;s.value=Array.isArray(x?.items)?x.items:[],P.value=[]})}m.querySelector("#ctx-items").innerHTML=Tt,m.querySelector(".cmx-chips").innerHTML=It,m.querySelector(".cmx-server-errors").innerHTML=Lt;const ye=m.querySelector("#ctx-accent");if(D.accent&&!lt.includes(D.accent)&&ye){const e=document.createElement("option");e.value=D.accent,e.textContent=`Custom (${D.accent})`,ye.appendChild(e)}const We={...r,items:s,rows:te,chips:Ue,dirty:z,saving:G,err:_e,tabBad:he,serverErrors:P,isNew:h,notNew:!h,previewText:Ge,serverError:M.computed(()=>P.value.length>0),saveText:M.computed(()=>h?"Create menu":"Save"),problemText:M.computed(()=>V.value&&!O.value.ok?" - some need fixing before they can be saved":""),scopeText:M.computed(()=>{const e=Q(r.pages.value),n=Q(r.projects.value);return!e.length&&!n.length?"Site-wide: every page carries this menu.":`Only ${[e.length&&`pages matching ${e.join(", ")}`,n.length&&`pages in ${n.join(", ")}`].filter(Boolean).join(", or ")}.`})},Ye=E.tabs(m.querySelector("#ctx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Qe=M.applyBindings(We,m,{methods:{save:()=>oe(),discard:()=>Je(),addItem:()=>q("item"),addHeader:()=>q("header"),addDivider:()=>q("divider")}});c(()=>Qe.dispose()),c(qe(m,{position:"bottom"}));const ge=m.querySelector("#ctx-slug"),xe=()=>{ee=!0};ge?.addEventListener("input",xe),c(()=>ge?.removeEventListener("input",xe));const Xe=E.contextMenu(b,{cursor:"glyph",match:".cmx-row",inherit:!1,density:"compact",items:e=>Ve(Number(e.dataset.index))});c(()=>Xe?.destroy?.());const ke=e=>{const n=e.target.closest(".cmx-row");if(!n)return;const t=Number(n.dataset.index);if(e.target.closest(".cmx-kebab")){Ne(e.target.closest(".cmx-kebab"));return}if(e.target.closest(".cmx-up")){j(t,t-1);return}if(e.target.closest(".cmx-down")){j(t,t+1);return}ae(t)},we=e=>{const n=e.target.closest(".cmx-row");if(!n||e.target!==n)return;const t=Number(n.dataset.index);e.key==="F10"&&e.shiftKey?(e.preventDefault(),Ne(n)):e.key==="Enter"?(e.preventDefault(),ae(t)):e.altKey&&e.key==="ArrowUp"?(e.preventDefault(),j(t,t-1)):e.altKey&&e.key==="ArrowDown"?(e.preventDefault(),j(t,t+1)):e.key==="Delete"&&(e.preventDefault(),k(Y(s.peek(),t)),$(Math.max(0,t-1)))};if(b.addEventListener("click",ke),b.addEventListener("keydown",we),c(()=>{b.removeEventListener("click",ke),b.removeEventListener("keydown",we)}),typeof E.sortable=="function"){const e=Mt(),n=E.sortable(b,{items:".cmx-row",key:null,keyboard:!1,onStart:e.start,onEnd:({item:t})=>e.restore(t),onSort:({item:t,from:i,to:a})=>{e.restore(t),k(Pe(s.peek(),i,a)),$(a)}});c(()=>n.destroy())}h?pe(A,"New menu"):pe(A,x?.name||p),je(l,{id:"ctx-editor-back",icon:"arrow-left",title:"All context menus",onClick:()=>{location.hash="#/context-menus"}}),je(l,{id:"ctx-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>oe()});const Ee=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||v||(e.preventDefault(),e.stopImmediatePropagation(),oe())};document.addEventListener("keydown",Ee,!0),c(()=>document.removeEventListener("keydown",Ee,!0));const Se=()=>{if(z.peek())try{S.set(F(p),{form:_(),items:s.peek(),base:U.peek(),at:new Date().toISOString()})}catch{}},Me=e=>{v?.dirty()&&v.apply(),z.peek()&&(Se(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Me),c(()=>window.removeEventListener("beforeunload",Me)),w={path:Be,dirty:()=>z.peek()||!!v?.dirty(),dispose(){if(L=!0,v){const e=v;e.dirty()&&e.apply();try{e.so.close(!0)}catch{}}Se(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(m);let y=null;try{y=S.get(F(p))}catch{}if(y?.form){try{S.remove?S.remove(F(p)):S.set(F(p),null)}catch{}const e=y.base&&y.base!==U.peek(),n=y.at?` (${St(y.at)})`:"",t=h?"the new context menu you started":`"${C(x?.name||p)}"`,i=e?`You left unsaved changes to ${t}${n}, but it has been saved since. Restore your version anyway?`:`You left unsaved changes to ${t}${n}. Restore them?`;if(await E.confirm(i)&&B()){for(const[a,J]of Object.entries(y.form))r[a]&&a!==(h?"":"slug")&&(r[a].value=J);h&&y.form.slug&&(ee=!0),Array.isArray(y.items)&&(s.value=y.items)}}}};
60
+ </div>`),P=rt("edit",i.icon);P.input.id="cmx-i-icon",P.input.classList.add("form-input");const Ie=()=>{a.icon.value=P.input.value};P.input.addEventListener("input",Ie),C.querySelector(".cmx-icon-mount").appendChild(P.el);const re=C.querySelector(".cmx-arg-mount");let f=null;const ce=()=>{a.arg.value=f.value};function Te(){f?.removeEventListener("input",ce),re.replaceChildren();const o=B[a.action.peek()];if(!o){f=null;return}if(o.key==="url"){const d=ct(o.placeholder,a.arg.peek());f=d.input,re.appendChild(d.el)}else f=document.createElement("input"),f.type="text",f.className="form-input",f.placeholder=o.placeholder,f.value=a.arg.peek(),re.appendChild(f);f.id="cmx-i-arg",f.addEventListener("input",ce)}let Le=a.action.peek();const tt=M.effect(()=>{const o=a.action.value;o!==Le&&(Le=o,a.arg.value="",Te())});Te();const $e=()=>{const o=Pe(J(),t),d=[...s.peek()],L=d.indexOf(t);L!==-1&&(d[L]=o,k(d))};let H=!1,le=!1;const De={done:()=>{$e(),H=!0,g.close(),j(e)},cancel:()=>g.close(),remove:()=>{const o=s.peek().indexOf(t);o!==-1&&k(W(s.peek(),o)),H=!0,g.close()}};g.element.appendChild(C);const nt=qe(C),at=M.applyBindings(Ce,C,{methods:De}),ot=o=>{(o.ctrlKey||o.metaKey)&&o.key==="Enter"&&(o.preventDefault(),De.done())};C.addEventListener("keydown",ot);const Oe={so:g,dirty:se,apply:$e};v=Oe,it(g,()=>{tt(),at.dispose(),nt(),P.input.removeEventListener("input",Ie),f?.removeEventListener("input",ce),v===Oe&&(v=null)});const st=g.close.bind(g);g.close=async o=>{if(!H&&o!==!0&&se()){if(le)return;le=!0;const d=await E.confirm("Discard the changes to this item?");if(le=!1,!d)return}if(!H&&n&&o!==!0&&!se()){const d=s.peek().indexOf(t);d!==-1&&k(W(s.peek(),d))}return H=!0,st()},I.scan(C),g.open(),requestAnimationFrame(()=>C.querySelector(i.type==="divider"?"#cmx-i-type":"#cmx-i-text")?.focus())}c(()=>{if(v){const e=v;e.dirty()&&e.apply();try{e.so.close(!0)}catch{}}});async function oe(){if(V.peek())return;Y.value=!0,K.value=[];const e=O.peek();if(!e.ok){const t=Object.keys(e.errors).length;E.toast(`Not saved yet - ${t===1?"one thing needs":`${t} things need`} fixing (marked in red).`,{type:"warning"});const i=he.peek(),a=i.binding?0:i.items?1:i.appearance?2:-1;a>=0&&We?.activate?.(a);return}V.value=!0;const n=kt(U(),s.peek());try{if(h)await de.contextMenus.create(n);else{const t=await de.contextMenus.update(p,n);t?.items&&(s.value=t.items)}if(!_())return;z.value=ue(U(),s.peek());try{S.remove?S.remove(F(p)):S.set(F(p),null)}catch{}window.dispatchEvent(new CustomEvent("dm:sidebar-count",{detail:{url:"#/context-menus"}})),h?(E.toast("Context menu created.",{type:"success"}),location.hash="#/context-menus"):(E.toast("Context menu saved.",{type:"success"}),pe(A,r.name.peek()||p))}catch(t){K.value=String(t.message||t).split("; ").map((i,a)=>({key:String(a),text:i})),m.querySelector(".cmx-alert")?.scrollIntoView({behavior:"smooth",block:"nearest"})}finally{V.value=!1}}function Ye(){E.confirm("Throw away your unsaved changes to this menu?").then(e=>{if(!e||!_())return;const n=Re(x);for(const[t,i]of Object.entries(n))r[t].value=i;s.value=Array.isArray(x?.items)?x.items:[],K.value=[]})}m.querySelector("#ctx-items").innerHTML=It,m.querySelector(".cmx-chips").innerHTML=Tt,m.querySelector(".cmx-server-errors").innerHTML=Lt;const ye=m.querySelector("#ctx-accent");if(D.accent&&!lt.includes(D.accent)&&ye){const e=document.createElement("option");e.value=D.accent,e.textContent=`Custom (${D.accent})`,ye.appendChild(e)}const Je={...r,items:s,rows:te,chips:Ue,dirty:G,saving:V,err:_e,tabBad:he,serverErrors:K,isNew:h,notNew:!h,previewText:Ge,serverError:M.computed(()=>K.value.length>0),saveText:M.computed(()=>h?"Create menu":"Save"),problemText:M.computed(()=>Y.value&&!O.value.ok?" - some need fixing before they can be saved":""),scopeText:M.computed(()=>{const e=Q(r.pages.value),n=Q(r.projects.value);return!e.length&&!n.length?"Site-wide: every page carries this menu.":`Only ${[e.length&&`pages matching ${e.join(", ")}`,n.length&&`pages in ${n.join(", ")}`].filter(Boolean).join(", or ")}.`})},We=E.tabs(m.querySelector("#ctx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Qe=M.applyBindings(Je,m,{methods:{save:()=>oe(),discard:()=>Ye(),addItem:()=>N("item"),addHeader:()=>N("header"),addDivider:()=>N("divider")}});c(()=>Qe.dispose()),c(qe(m,{position:"bottom"}));const ge=m.querySelector("#ctx-slug"),xe=()=>{ee=!0};ge?.addEventListener("input",xe),c(()=>ge?.removeEventListener("input",xe));const Xe=E.contextMenu(b,{cursor:"glyph",match:".cmx-row",inherit:!1,density:"compact",items:e=>Ve(Number(e.dataset.index))});c(()=>Xe?.destroy?.());const ke=e=>{const n=e.target.closest(".cmx-row");if(!n)return;const t=Number(n.dataset.index);if(e.target.closest(".cmx-kebab")){Ne(e.target.closest(".cmx-kebab"));return}if(e.target.closest(".cmx-up")){q(t,t-1);return}if(e.target.closest(".cmx-down")){q(t,t+1);return}ae(t)},we=e=>{const n=e.target.closest(".cmx-row");if(!n||e.target!==n)return;const t=Number(n.dataset.index);e.key==="F10"&&e.shiftKey?(e.preventDefault(),Ne(n)):e.key==="Enter"?(e.preventDefault(),ae(t)):e.altKey&&e.key==="ArrowUp"?(e.preventDefault(),q(t,t-1)):e.altKey&&e.key==="ArrowDown"?(e.preventDefault(),q(t,t+1)):e.key==="Delete"&&(e.preventDefault(),k(W(s.peek(),t)),j(Math.max(0,t-1)))};if(b.addEventListener("click",ke),b.addEventListener("keydown",we),c(()=>{b.removeEventListener("click",ke),b.removeEventListener("keydown",we)}),typeof E.sortable=="function"){const e=Mt(),n=E.sortable(b,{items:".cmx-row",key:null,keyboard:!1,onStart:e.start,onEnd:({item:t})=>e.restore(t),onSort:({item:t,from:i,to:a})=>{e.restore(t),k(Fe(s.peek(),i,a)),j(a)}});c(()=>n.destroy())}h?pe(A,"New menu"):pe(A,x?.name||p),je(l,{id:"ctx-editor-back",icon:"arrow-left",title:"All context menus",onClick:()=>{location.hash="#/context-menus"}}),je(l,{id:"ctx-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>oe()});const Ee=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||v||(e.preventDefault(),e.stopImmediatePropagation(),oe())};document.addEventListener("keydown",Ee,!0),c(()=>document.removeEventListener("keydown",Ee,!0));const Se=()=>{if(G.peek())try{S.set(F(p),{form:U(),items:s.peek(),base:z.peek(),at:new Date().toISOString()})}catch{}},Me=e=>{v?.dirty()&&v.apply(),G.peek()&&(Se(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Me),c(()=>window.removeEventListener("beforeunload",Me)),w={path:Be,dirty:()=>G.peek()||!!v?.dirty(),dispose(){if(T=!0,v){const e=v;e.dirty()&&e.apply();try{e.so.close(!0)}catch{}}Se(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(m);let y=null;try{y=S.get(F(p))}catch{}if(y?.form){try{S.remove?S.remove(F(p)):S.set(F(p),null)}catch{}const e=y.base&&y.base!==z.peek(),n=y.at?` (${St(y.at)})`:"",t=h?"the new context menu you started":`"${$(x?.name||p)}"`,i=e?`You left unsaved changes to ${t}${n}, but it has been saved since. Restore your version anyway?`:`You left unsaved changes to ${t}${n}. Restore them?`;if(await E.confirm(i)&&_()){for(const[a,J]of Object.entries(y.form))r[a]&&a!==(h?"":"slug")&&(r[a].value=J);h&&y.form.slug&&(ee=!0),Array.isArray(y.items)&&(s.value=y.items)}}}};
@@ -0,0 +1,30 @@
1
+ import{apiRequest as y}from"../api.js";import{addBannerAction as v}from"../lib/plugin-chrome.js";import{helpPopovers as Q}from"../lib/help-popover.js";import{openMenuAt as G}from"../lib/tool-kit.js";import{SIZE_LABELS as ee,SIZES as te,addWidget as ae,anyOpen as oe,boardCards as se,dropWidget as de,galleryGroups as re,removeWidget as Y,resizeWidget as ie,sameLayout as T,setAllCollapsed as ne,setCollapsed as le,shiftWidget as S,withToday as ce}from"../lib/dashboard-board-arrange.js";import{destroyWidget as pe,drawError as ue,drawWidget as be}from"../lib/widget-render.js";const fe=400,he=l=>String(l||"").replace(/^\/api(?=\/)/,""),n=l=>String(l??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");export async function mountBoard(l,b,{isAdmin:N=!1}={}){const q=l.get(0),c=q.querySelector(".dbx")||q;c.classList.add("dbp"),c.replaceChildren(),c.insertAdjacentHTML("beforeend",`
2
+ <div class="dbp-head">
3
+ <p class="dbp-hint">Your own Dashboard. Drag a card by its grip to move it; the chevron folds it; \u22EE or right-click to resize or remove it.
4
+ <span data-help-title="Dashboard Pro" data-help="Add widgets from the Tools you use with Add widget. Each card keeps itself up to date. This layout is yours - other people arrange their own. Reset goes back to the site's default; an administrator can make their layout that default for everyone who has not arranged their own."></span></p>
5
+ <span class="dbp-source" aria-live="polite"></span>
6
+ </div>
7
+ <div class="dbp-board" role="list" aria-label="Dashboard widgets"></div>
8
+ <div class="dbp-empty-board" hidden>
9
+ <p>Nothing on your Dashboard yet.</p>
10
+ <button type="button" class="btn btn-primary dbp-add-first"><span data-icon="plus" data-icon-size="14"></span> Add a widget</button>
11
+ </div>`);const i=c.querySelector(".dbp-board"),U=c.querySelector(".dbp-source"),D=()=>c.isConnected;let f=[],a={version:1,widgets:[]},w=a,k="built-in";const g=new Map;b.add(()=>{for(const e of g.values())clearInterval(e);g.clear()});async function _(){const[e,t]=await Promise.all([y("/widgets",{method:"GET"}).catch(()=>[]),y("/dashboard/layout",{method:"GET"}).catch(()=>null)]);f=Array.isArray(e)?e:[],a=t?.layout||{version:1,widgets:[]},w=a,k=t?.source||"built-in"}let A=null;function $(){clearTimeout(A),A=setTimeout(async()=>{if(!T(a,w))try{w=(await y("/dashboard/layout",{method:"PUT",body:JSON.stringify(a)}))?.layout||a,k="mine",M()}catch(e){E.toast(`Your layout was not saved: ${e.message||e}`,{type:"error"})}},fe)}b.add(()=>clearTimeout(A));function M(){U.textContent={mine:"Your layout",site:"The site's default layout","built-in":"The starting layout"}[k]||""}function p(e){T(e,a)||(a=e,C(),$())}function O(e,t){a=le(a,e,t);const s=i.querySelector(`.dbp-card[data-id="${CSS.escape(e)}"]`);s&&x(s,t),$()}function x(e,t){const s=e.classList.contains("is-collapsed");e.classList.toggle("is-collapsed",t);const o=e.querySelector(".dbp-body");o&&(o.hidden=t);const d=e.querySelector(".dbp-fold"),r=e.getAttribute("aria-label")||"";d&&(d.setAttribute("aria-expanded",t?"false":"true"),d.title=t?"Open":"Fold",d.setAttribute("aria-label",`${t?"Open":"Fold"} ${r}`)),s&&!t&&h(e)}function j(e){a=ne(a,e),i.querySelectorAll(".dbp-card").forEach(t=>x(t,e)),$()}const H=4,K=16;function z(e){const t=e.getBoundingClientRect().height;t&&(e.style.gridRowEnd=`span ${Math.max(1,Math.ceil((t+K)/H))}`)}const L=typeof ResizeObserver=="function"?new ResizeObserver(e=>{for(const t of e)z(t.target)}):null;b.add(()=>L?.disconnect());async function h(e){const t=f.find(o=>o.id===e.dataset.id),s=e.querySelector(".dbp-body");if(!(!t||!s)){e.classList.add("is-loading");try{const o=t.kind==="count"?ce(t.dataUrl):t.dataUrl,d=await y(he(o),{method:"GET"});if(!D()||!e.isConnected)return;await be(s,t.kind,d)}catch(o){e.isConnected&&ue(s,o.status===403?"You cannot see this any more.":"Could not load.")}finally{e.classList.remove("is-loading")}}}function J(e){const t=e.href?`<a class="dbp-title" href="${n(e.href)}" title="Open ${n(e.title)}">${n(e.title)}</a>`:`<span class="dbp-title">${n(e.title)}</span>`;return`<section class="dbp-card dbp-size-${n(e.size)}${e.collapsed?" is-collapsed":""}" data-id="${n(e.id)}" role="listitem" tabindex="0" aria-label="${n(e.title)}">
12
+ <header class="dbp-card-head">
13
+ <span class="dbp-grip" title="Drag to move" aria-hidden="true"><span data-icon="menu" data-icon-size="14"></span></span>
14
+ <span class="dbp-icon" aria-hidden="true"><span data-icon="${n(e.icon||"grid")}" data-icon-size="16"></span></span>
15
+ ${t}
16
+ <small class="dbp-from">${e.source&&e.source!=="Built in"?n(e.source):""}</small>
17
+ <button type="button" class="dbp-fold" aria-expanded="${e.collapsed?"false":"true"}" aria-label="${e.collapsed?"Open":"Fold"} ${n(e.title)}" title="${e.collapsed?"Open":"Fold"}"><span data-icon="chevron-down" data-icon-size="14"></span></button>
18
+ <button type="button" class="dbp-kebab" aria-label="Actions for ${n(e.title)}" title="Actions">\u22EE</button>
19
+ </header>
20
+ <div class="dbp-body"${e.collapsed?" hidden":""}><div class="dbp-skeleton"></div></div>
21
+ </section>`}function C(){for(const t of g.values())clearInterval(t);g.clear(),i.querySelectorAll(".dbp-body").forEach(pe);const e=se(a,f);i.innerHTML=e.map(J).join(""),c.querySelector(".dbp-empty-board").hidden=e.length>0,L?.disconnect();for(const t of i.querySelectorAll(".dbp-card"))z(t),L?.observe(t);I.scan(i);for(const t of i.querySelectorAll(".dbp-card")){t.classList.contains("is-collapsed")||h(t);const s=f.find(o=>o.id===t.dataset.id);g.set(t.dataset.id,setInterval(()=>{!document.hidden&&t.isConnected&&!t.classList.contains("is-collapsed")&&h(t)},(s?.refresh||60)*1e3))}M()}const R=e=>{const t=e.detail?.url;if(t)for(const s of i.querySelectorAll(".dbp-card"))f.find(d=>d.id===s.dataset.id)?.href===t&&h(s)};window.addEventListener("dm:sidebar-count",R),b.add(()=>window.removeEventListener("dm:sidebar-count",R));function X(e){const t=e.dataset.id,s=f.find(r=>r.id===t),o=a.widgets.find(r=>r.id===t);if(!s||!o)return[];const d=a.widgets.findIndex(r=>r.id===t);return[{header:s.title},...s.href?[{label:`Open ${s.title}`,icon:"arrow-right",action:()=>{location.hash=s.href}}]:[],{label:o.collapsed?"Open":"Fold",icon:o.collapsed?"chevron-down":"chevron-up",action:()=>O(t,!o.collapsed)},{label:"Refresh",icon:"refresh-cw",disabled:o.collapsed===!0,action:()=>h(e)},{type:"divider"},...te.map(r=>({label:ee[r],icon:o.size===r?"check":"maximize-2",disabled:o.size===r,action:()=>p(ie(a,t,r))})),{type:"divider"},{label:"Move earlier",icon:"arrow-left",disabled:d<=0,action:()=>p(S(a,t,-1))},{label:"Move later",icon:"arrow-right",disabled:d>=a.widgets.length-1,action:()=>p(S(a,t,1))},{type:"divider"},{label:"Remove from the Dashboard",icon:"x",action:()=>p(Y(a,t))}]}const Z=E.contextMenu(i,{cursor:"glyph",match:".dbp-card",inherit:!1,density:"compact",items:e=>X(e)});b.add(()=>Z?.destroy?.());const W=e=>{const t=e.target.closest(".dbp-kebab");if(t){G(t);return}const s=e.target.closest(".dbp-fold");if(s){const o=s.closest(".dbp-card");o&&O(o.dataset.id,!o.classList.contains("is-collapsed"))}},F=e=>{const t=e.target.closest(".dbp-card");if(t){if(e.key==="F10"&&e.shiftKey){e.preventDefault(),G(t);return}e.target===t&&(e.altKey&&(e.key==="ArrowLeft"||e.key==="ArrowUp")?(e.preventDefault(),p(S(a,t.dataset.id,-1)),P(t.dataset.id)):e.altKey&&(e.key==="ArrowRight"||e.key==="ArrowDown")?(e.preventDefault(),p(S(a,t.dataset.id,1)),P(t.dataset.id)):e.key==="Delete"&&(e.preventDefault(),p(Y(a,t.dataset.id))))}},P=e=>requestAnimationFrame(()=>i.querySelector(`.dbp-card[data-id="${CSS.escape(e)}"]`)?.focus());if(i.addEventListener("click",W),i.addEventListener("keydown",F),b.add(()=>{i.removeEventListener("click",W),i.removeEventListener("keydown",F)}),typeof E.sortable=="function"){const e=E.sortable(i,{items:".dbp-card",handle:".dbp-grip",live:!1,axis:"x",onDrop({key:t,targetKey:s,zone:o}){if(o!=="before"&&o!=="after")return!1;const d=de(a,t,s,o);return T(d,a)?!1:(p(d),!0)}});b.add(()=>e?.destroy?.())}function B(){const e=E.slideover({title:"Add a widget",size:"md",position:"right"}),t=document.createElement("div");t.className="dbp-gallery",t.innerHTML=`<input type="search" class="form-input dbp-gallery-search" placeholder="Search widgets" aria-label="Search widgets">
22
+ <p class="dbp-gallery-hint">Widgets from the Tools you can open. Each one is added at its usual size - resize it from its \u22EE menu.</p>
23
+ <div class="dbp-gallery-list"></div>`,e.element.appendChild(t);const s=t.querySelector(".dbp-gallery-list"),o=t.querySelector(".dbp-gallery-search");function d(){const r=re(f,a,o.value);s.innerHTML=r.length?r.map(m=>`
24
+ <h4 class="dbp-gallery-group">${n(m.source)}</h4>
25
+ ${m.widgets.map(u=>`
26
+ <div class="dbp-gallery-item">
27
+ <span class="dbp-icon"><span data-icon="${n(u.icon||"grid")}" data-icon-size="16"></span></span>
28
+ <span class="dbp-gallery-main"><strong>${n(u.title)}</strong><small>${n(u.description||"")}</small></span>
29
+ ${u.added?'<span class="dbp-gallery-added">On the board</span>':`<button type="button" class="btn btn-sm btn-primary" data-add="${n(u.id)}">Add</button>`}
30
+ </div>`).join("")}`).join(""):'<p class="dbp-empty">No widgets match.</p>',I.scan(s)}s.addEventListener("click",r=>{const m=r.target.closest("[data-add]")?.dataset.add,u=m&&f.find(V=>V.id===m);u&&(p(ae(a,u)),E.toast(`${u.title} added.`,{type:"success",duration:2e3}),d())}),o.addEventListener("input",d),e.open(),d(),setTimeout(()=>o.focus(),50)}c.querySelector(".dbp-add-first").addEventListener("click",B),v(l,{id:"dashboard-add-widget",icon:"plus",title:"Add widget",onClick:B}),v(l,{id:"dashboard-fold-all",icon:"chevrons-up",title:"Fold or open every widget",onClick:()=>j(oe(a))}),v(l,{id:"dashboard-refresh",icon:"refresh-cw",title:"Refresh every widget",onClick:()=>i.querySelectorAll(".dbp-card").forEach(h)}),v(l,{id:"dashboard-reset",icon:"rotate-ccw",title:"Reset to the site's default layout",onClick:async()=>{if(await E.confirm("Go back to the site's default layout? Your own arrangement is forgotten."))try{const e=await y("/dashboard/layout",{method:"DELETE"});a=e?.layout||a,w=a,k=e?.source||"built-in",C(),E.toast("Back to the default layout.",{type:"success"})}catch(e){E.toast(`Could not reset: ${e.message||e}`,{type:"error"})}}}),N&&v(l,{id:"dashboard-make-default",icon:"users",title:"Make this the default for everyone",onClick:async()=>{if(await E.confirm("Make this layout the default? Everyone who has not arranged their own Dashboard sees it. (Widgets they may not use are left out for them.)"))try{await y("/dashboard/layout/default",{method:"PUT",body:JSON.stringify(a)}),E.toast("This is now the default layout.",{type:"success"})}catch(e){E.toast(`Could not set the default: ${e.message||e}`,{type:"error"})}}}),b.add(Q(c,{position:"bottom"})),I.scan(c),await _(),D()&&C()}
@@ -1,7 +1,7 @@
1
- import{api as L,apiRequest as m}from"../api.js";import{addBannerAction as ne}from"../lib/plugin-chrome.js";import{helpPopovers as $e}from"../lib/help-popover.js";import{openMenuAt as re,viewLifetime as Ee}from"../lib/tool-kit.js";import{SHAPES as ie,showLoading as j}from"../lib/loading.js";import{BADGE_SOURCES as Se,activityRows as Ae,attentionFrom as Pe,canAny as _,chartSeries as Re,isSitePath as O,kpisFrom as Te}from"../lib/dashboard-arrange.js";const le=/^(#\/?)?(\?.*)?$/,Le=3e4,je="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js",_e={type:"custom",template:'<div class="skeleton skeleton-rect" style="flex:1;aspect-ratio:auto;height:240px"></div>'},Oe=`
1
+ import{api as j,apiRequest as f}from"../api.js";import{addBannerAction as ie}from"../lib/plugin-chrome.js";import{helpPopovers as $e}from"../lib/help-popover.js";import{openMenuAt as le,viewLifetime as Se}from"../lib/tool-kit.js";import{SHAPES as ce,showLoading as _}from"../lib/loading.js";import{BADGE_SOURCES as Ae,activityRows as Pe,attentionFrom as Re,canAny as B,chartSeries as Te,isSitePath as O,kpisFrom as Le}from"../lib/dashboard-arrange.js";const q=/^(#\/?)?(\?.*)?$/,je=3e4,_e="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js",Be={type:"custom",template:'<div class="skeleton skeleton-rect" style="flex:1;aspect-ratio:auto;height:240px"></div>'},Oe=`
2
2
  <div class="dbx-row" data-id="{{id}}" data-href="{{href}}" data-url="{{url}}" tabindex="0" data-bind-class="rowCls">
3
3
  <span class="dbx-main"><strong>{{text}}</strong><small data-bind-hidden="!meta">{{meta}}</small></span>
4
4
  <span class="dbx-value" data-bind-hidden="!value">{{value}}</span>
5
5
  <span class="dbx-chip" data-bind-hidden="!chip" data-bind-class="chipCls">{{chip}}</span>
6
6
  <button type="button" class="dbx-kebab" aria-label="Actions" title="Actions" data-bind-hidden="!menu">\u22EE</button>
7
- </div>`;let b=null;function Be(){return window.Chart?Promise.resolve(!0):(b||(b=new Promise(i=>{const o=document.createElement("script");o.src=je,o.async=!0,o.onload=()=>i(!0),o.onerror=()=>{b=null,i(!1)},document.head.appendChild(o)})),b)}const c=i=>Number(i||0).toLocaleString("en-GB"),qe=i=>`${Number(i)>=0?"+":""}${Number(i).toFixed(0)}%`,v=(i,o,d,l={})=>({id:`${i}${o}`,text:d,meta:"",href:"",url:d,value:"",chip:"",chipCls:"",rowCls:"",menu:!0,...l}),u=Ee(le);export const dashboardView={templateUrl:"/admin/js/templates/dashboard.html",async onMount(i){u.reset();const o=i.get(0).querySelector(".dbx")||i.get(0),d=()=>le.test(location.hash)&&o.isConnected,l=M.observable(null),g=M.observable({}),y=M.observable(!1),w=M.observable(""),x=M.observable(null),k=M.observable(Date.now()),p=M.observable(null),C=M.observable(!!window.Chart);let f=[],B=new Set;const ce=M.computed(()=>Te(l.value,g.value)),de=M.computed(()=>{const e=x.value;if(!e)return"";const a=Math.floor((k.value-e)/6e4);return a<1?"Updated just now":`Updated ${a} min ago`}),q=M.observable([]),F=M.observable([]),N=M.observable([]),D=M.observable([]),U=M.observable([]),H=M.observable([]),G=M.observable([]),K=M.observable([]);u.add(M.effect(()=>{const e=l.value,a=t=>t==="danger"?"is-danger":"is-warn";q.value=Pe(g.value,e?.health?.checks||[]).map(t=>({id:t.id,text:`${t.title}: ${t.text}`,meta:t.meta,href:t.href,url:"",value:"",chip:t.tone==="danger"?"Urgent":"Check",chipCls:a(t.tone),rowCls:a(t.tone)+(t.href?"":" is-static"),menu:!!t.href})),F.value=Ae(e?.activity||[],k.value).map(t=>({id:t.id,text:t.text,meta:t.meta,href:t.href,url:t.url,value:"",chip:"",chipCls:"",rowCls:"",menu:!0})),N.value=(e?.topPages||[]).map((t,r)=>v("t",r,t.url,{value:c(t.hits),chip:t.deltaPct==null?"":qe(t.deltaPct),chipCls:Number(t.deltaPct)>=0?"is-ok":"is-danger"})),D.value=(e?.spikes||[]).map((t,r)=>v("s",r,t.url,{value:c(t.hits),chip:`\xD7${Number(t.ratio)||0}`,chipCls:"is-warn"}));const s=e?.journeys;U.value=(s?.entry||[]).map((t,r)=>v("e",r,t.url,{value:c(t.count)})),H.value=(s?.exit||[]).map((t,r)=>v("x",r,t.url,{value:c(t.count)})),G.value=(s?.paths||[]).map((t,r)=>({id:`p${r}`,text:`${t.from} \u2192 ${t.to}`,meta:"",href:"",url:"",value:c(t.count),chip:"",chipCls:"",rowCls:"is-static",menu:!1}));const n={ok:["OK","is-ok"],warn:["Look","is-warn"],fail:["Failing","is-danger"]};K.value=(e?.health?.checks||[]).map((t,r)=>({id:`c${r}`,text:String(t.label||t.id),meta:Array.isArray(t.detail)?t.detail.slice(0,5).join(", "):"",href:"",url:"",value:String(t.value??""),chip:(n[t.level]||n.warn)[0],chipCls:(n[t.level]||n.warn)[1],rowCls:"is-static",menu:!1}))}));const ue=M.computed(()=>!!l.value?.journeys?.totalSessions),he=M.computed(()=>{const e=l.value?.journeys;return e?.totalSessions?`Bounce rate ${(Number(e.bounceRate)*100).toFixed(1)}%`:""}),$=M.computed(()=>Re(l.value?.traffic?.series)),S=M.computed(()=>C.value&&!$.value.empty),pe=M.computed(()=>y.value?l.value?C.value?"No page views recorded yet.":"The chart could not load (Chart.js).":"Traffic needs an administrator.":"Loading\u2026"),me=M.computed(()=>!!p.value?.enabled),fe=M.computed(()=>{const e=p.value||{},a=(e.hits||0)+(e.misses||0),s=n=>{if(!n)return"never";const t=Math.max(0,Math.floor((Date.now()-new Date(n).getTime())/1e3));return t<60?`${t}s ago`:t<3600?`${Math.floor(t/60)} min ago`:t<86400?`${Math.floor(t/3600)} h ago`:`${Math.floor(t/86400)} days ago`};return{hitRate:e.hitRate==null?"-":`${(e.hitRate*100).toFixed(1)}%`,hitsMisses:a?`${c(e.hits)} hits \xB7 ${c(e.misses)} misses`:"no visits yet",size:e.maxItems?`${c(e.size)} / ${c(e.maxItems)}`:c(e.size),driver:e.driver?`driver: ${e.driver}`:"",writes:c(e.writes),invalidations:`${c(e.invalidations)} invalidations`,cleared:s(e.lastClearedAt)}});let A=0;async function V(){const e=Se.filter(s=>_(f,s.permission)),a=await Promise.all(e.map(s=>m(s.countUrl,{method:"GET"}).catch(()=>null)));d()&&(g.value=Object.fromEntries(e.map((s,n)=>[s.key,a[n]]).filter(([,s])=>s)))}async function z({first:e=!1}={}){const a=++A,s=e?[j(o.querySelectorAll(".dbx-kpi-value"),ie.figure),j(o.querySelectorAll(".dbx-loading"),{...ie.rows,count:3}),j(o.querySelector(".dbx-chart-note"),_e)]:[],n=()=>Promise.all(s.map(t=>t.done()));try{f=(await m("/auth/permissions",{method:"GET"}).catch(()=>({})))?.permissions||[];const[r,Me,oe]=await Promise.all([L.dashboard.summary().catch(T=>({__error:T})),m("/cache/status",{method:"GET"}).catch(()=>null),_(f,"pages")?L.pages.list().catch(()=>[]):Promise.resolve([]),V()]);if(await n(),a!==A||!d())return;B=new Set((Array.isArray(oe)?oe:[]).map(T=>T.urlPath).filter(Boolean)),r?.__error?(l.value=null,w.value=/403|forbidden/i.test(String(r.__error?.message))?"":`The dashboard could not load: ${r.__error.message}`):(l.value=r,w.value=r?.warnings?.length?`Some parts could not load: ${r.warnings.join(" \xB7 ")}`:""),p.value=Me,x.value=Date.now()}finally{await n(),a===A&&(y.value=!0)}}let be=0;async function ve(){if(!(!d()||!l.peek())){k.value=Date.now();try{const e=await L.dashboard.summaryLite();if(!d())return;const a={...l.peek()};e.traffic&&(a.traffic={...a.traffic,...e.traffic}),e.realtime&&(a.realtime=e.realtime),e.spikes&&(a.spikes=e.spikes),e.health&&(a.health={...a.health,status:e.health.status}),l.value=a,x.value=Date.now()}catch{}++be%2===0&&V(),m("/cache/status",{method:"GET"}).then(e=>{d()&&(p.value=e)}).catch(()=>{})}}const ge=setInterval(ve,Le);u.add(()=>clearInterval(ge));let h=null;const P=o.querySelector(".dbx-canvas"),ye=()=>{const e=getComputedStyle(o),a=(s,n)=>e.getPropertyValue(s).trim()||n;return{line:a("--dm-primary","#5b8cff"),text:a("--dm-text-muted","#94a3b8"),grid:a("--dm-border","rgba(127,127,127,.2)")}},we=e=>/^#[0-9a-f]{6}$/i.test(e)?`${e}26`:"transparent";function J(){if(!S.peek()||!P?.isConnected||!P.offsetParent)return;const{labels:e,values:a}=$.peek(),{line:s,text:n,grid:t}=ye();if(h){h.data.labels=e,h.data.datasets[0].data=a,h.update("none");return}h=new window.Chart(P,{type:"line",data:{labels:e,datasets:[{data:a,borderColor:s,backgroundColor:we(s),fill:!0,tension:.3,pointRadius:0,borderWidth:2}]},options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{display:!1}},scales:{x:{ticks:{maxTicksLimit:8,color:n},grid:{color:t}},y:{beginAtZero:!0,ticks:{precision:0,color:n},grid:{color:t}}}}})}u.add(M.effect(()=>{$.value,S.value,requestAnimationFrame(J)})),u.add(()=>{try{h?.destroy()}catch{}h=null});const W=()=>[...document.body.classList].filter(e=>/^dm-(theme|custom)-/.test(e)).sort().join(" ");let Z=W();const Q=new MutationObserver(()=>{const e=W();if(e!==Z){Z=e;try{h?.destroy()}catch{}h=null,requestAnimationFrame(J)}});Q.observe(document.body,{attributes:!0,attributeFilter:["class"]}),u.add(()=>Q.disconnect());const R=e=>new URL(e,location.origin).href,X=()=>_(f,"pages"),Y=e=>O(e)&&B.has(e.split(/[?#]/)[0].replace(/\/$/,"")||"/");async function ee(e,a){try{await navigator.clipboard.writeText(e),E.toast(`${a} copied.`,{type:"success"})}catch{E.toast("Could not copy - select it and copy it by hand.",{type:"error"})}}function te(e){const{href:a,url:s}=e.dataset;if(a){location.hash=a;return}if(s&&X()&&Y(s)){location.hash=`#/pages/edit${s.split(/[?#]/)[0]}`;return}s&&O(s)&&window.open(R(s),"_blank","noopener")}function xe(e){const{href:a,url:s}=e.dataset,n=[{header:(e.querySelector(".dbx-main strong")?.textContent||"").slice(0,60)}];return a&&n.push({label:"Open",icon:"arrow-right",action:()=>{location.hash=a}}),s&&O(s)?(n.push({label:"Open on the site",icon:"external-link",action:()=>window.open(R(s),"_blank","noopener")}),n.push({label:"Copy address",icon:"copy",action:()=>ee(R(s),"Address")}),X()&&n.push({label:"Edit the page",icon:"edit",disabled:!Y(s),action:()=>{location.hash=`#/pages/edit${s.split(/[?#]/)[0]}`}})):s&&n.push({label:"Copy address",icon:"copy",action:()=>ee(s,"Address")}),n.length>1?n:[]}for(const e of[".dbx-attention",".dbx-activity",".dbx-top-pages",".dbx-spikes",".dbx-entry",".dbx-exit",".dbx-paths",".dbx-checks"]){const a=o.querySelector(e);a&&(a.innerHTML=Oe)}const ke=M.applyBindings({summary:l,loaded:y,warning:w,asOf:de,kpi:ce,attention:q,activity:F,topPages:N,spikes:D,entry:U,exit:H,paths:G,checks:K,hasJourneys:ue,bounce:he,hasChart:S,chartNote:pe,cache:p,cacheOn:me,cacheView:fe},o,{methods:{async toggleCache(e){const a=!!e?.target?.checked;try{p.value=await m("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:a})}),E.toast(`The cache is ${a?"on":"off"}.`,{type:"success"})}catch(s){e?.target&&(e.target.checked=!a),E.toast(s.message||"Could not change the cache.",{type:"error"})}},async clearCache(){if(await E.confirm("Clear the whole response cache? Pages render afresh until it fills again."))try{p.value=await m("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}}}});u.add(()=>ke.dispose()),u.add($e(o,{position:"bottom"}));const Ce=E.contextMenu(o,{cursor:"glyph",match:".dbx-row",inherit:!1,density:"compact",items:e=>xe(e)});u.add(()=>Ce?.destroy?.());const ae=e=>{const a=e.target.closest(".dbx-row");if(!a||!o.contains(a))return;const s=e.target.closest(".dbx-kebab");if(s){re(s);return}te(a)},se=e=>{const a=e.target.closest(".dbx-row");!a||!o.contains(a)||(e.key==="F10"&&e.shiftKey?(e.preventDefault(),re(a)):e.key==="Enter"&&e.target===a&&te(a))};o.addEventListener("click",ae),o.addEventListener("keydown",se),u.add(()=>{o.removeEventListener("click",ae),o.removeEventListener("keydown",se)}),ne(i,{id:"dashboard-refresh",icon:"refresh-cw",title:"Refresh",onClick:()=>z()}),ne(i,{id:"dashboard-new-page",icon:"plus",title:"New page",onClick:()=>{location.hash="#/pages/new"}}),I.scan(o),Be().then(e=>{d()&&(C.value=e)}),await z({first:!0}),d()&&I.scan(o)}};
7
+ </div>`;let v=null;function qe(){return window.Chart?Promise.resolve(!0):(v||(v=new Promise(i=>{const u=document.createElement("script");u.src=_e,u.async=!0,u.onload=()=>i(!0),u.onerror=()=>{v=null,i(!1)},document.head.appendChild(u)})),v)}const c=i=>Number(i||0).toLocaleString("en-GB"),Fe=i=>`${Number(i)>=0?"+":""}${Number(i).toFixed(0)}%`,g=(i,u,n,h={})=>({id:`${i}${u}`,text:n,meta:"",href:"",url:n,value:"",chip:"",chipCls:"",rowCls:"",menu:!0,...h}),d=Se(q);export const dashboardView={templateUrl:"/admin/js/templates/dashboard.html",async onMount(i){d.reset();const u=await f("/dashboard/pro",{method:"GET"}).catch(()=>null);if(!q.test(location.hash))return;if(u?.enabled){const{mountBoard:e}=await import("./dashboard-board.js");await e(i,d,{isAdmin:u.isAdmin===!0});return}const n=i.get(0).querySelector(".dbx")||i.get(0),h=()=>q.test(location.hash)&&n.isConnected,l=M.observable(null),y=M.observable({}),w=M.observable(!1),x=M.observable(""),k=M.observable(null),C=M.observable(Date.now()),m=M.observable(null),$=M.observable(!!window.Chart);let b=[],F=new Set;const de=M.computed(()=>Le(l.value,y.value)),ue=M.computed(()=>{const e=k.value;if(!e)return"";const a=Math.floor((C.value-e)/6e4);return a<1?"Updated just now":`Updated ${a} min ago`}),N=M.observable([]),D=M.observable([]),U=M.observable([]),G=M.observable([]),H=M.observable([]),K=M.observable([]),V=M.observable([]),z=M.observable([]);d.add(M.effect(()=>{const e=l.value,a=t=>t==="danger"?"is-danger":"is-warn";N.value=Re(y.value,e?.health?.checks||[]).map(t=>({id:t.id,text:`${t.title}: ${t.text}`,meta:t.meta,href:t.href,url:"",value:"",chip:t.tone==="danger"?"Urgent":"Check",chipCls:a(t.tone),rowCls:a(t.tone)+(t.href?"":" is-static"),menu:!!t.href})),D.value=Pe(e?.activity||[],C.value).map(t=>({id:t.id,text:t.text,meta:t.meta,href:t.href,url:t.url,value:"",chip:"",chipCls:"",rowCls:"",menu:!0})),U.value=(e?.topPages||[]).map((t,r)=>g("t",r,t.url,{value:c(t.hits),chip:t.deltaPct==null?"":Fe(t.deltaPct),chipCls:Number(t.deltaPct)>=0?"is-ok":"is-danger"})),G.value=(e?.spikes||[]).map((t,r)=>g("s",r,t.url,{value:c(t.hits),chip:`\xD7${Number(t.ratio)||0}`,chipCls:"is-warn"}));const s=e?.journeys;H.value=(s?.entry||[]).map((t,r)=>g("e",r,t.url,{value:c(t.count)})),K.value=(s?.exit||[]).map((t,r)=>g("x",r,t.url,{value:c(t.count)})),V.value=(s?.paths||[]).map((t,r)=>({id:`p${r}`,text:`${t.from} \u2192 ${t.to}`,meta:"",href:"",url:"",value:c(t.count),chip:"",chipCls:"",rowCls:"is-static",menu:!1}));const o={ok:["OK","is-ok"],warn:["Look","is-warn"],fail:["Failing","is-danger"]};z.value=(e?.health?.checks||[]).map((t,r)=>({id:`c${r}`,text:String(t.label||t.id),meta:Array.isArray(t.detail)?t.detail.slice(0,5).join(", "):"",href:"",url:"",value:String(t.value??""),chip:(o[t.level]||o.warn)[0],chipCls:(o[t.level]||o.warn)[1],rowCls:"is-static",menu:!1}))}));const he=M.computed(()=>!!l.value?.journeys?.totalSessions),pe=M.computed(()=>{const e=l.value?.journeys;return e?.totalSessions?`Bounce rate ${(Number(e.bounceRate)*100).toFixed(1)}%`:""}),S=M.computed(()=>Te(l.value?.traffic?.series)),A=M.computed(()=>$.value&&!S.value.empty),me=M.computed(()=>w.value?l.value?$.value?"No page views recorded yet.":"The chart could not load (Chart.js).":"Traffic needs an administrator.":"Loading\u2026"),fe=M.computed(()=>!!m.value?.enabled),be=M.computed(()=>{const e=m.value||{},a=(e.hits||0)+(e.misses||0),s=o=>{if(!o)return"never";const t=Math.max(0,Math.floor((Date.now()-new Date(o).getTime())/1e3));return t<60?`${t}s ago`:t<3600?`${Math.floor(t/60)} min ago`:t<86400?`${Math.floor(t/3600)} h ago`:`${Math.floor(t/86400)} days ago`};return{hitRate:e.hitRate==null?"-":`${(e.hitRate*100).toFixed(1)}%`,hitsMisses:a?`${c(e.hits)} hits \xB7 ${c(e.misses)} misses`:"no visits yet",size:e.maxItems?`${c(e.size)} / ${c(e.maxItems)}`:c(e.size),driver:e.driver?`driver: ${e.driver}`:"",writes:c(e.writes),invalidations:`${c(e.invalidations)} invalidations`,cleared:s(e.lastClearedAt)}});let P=0;async function J(){const e=Ae.filter(s=>B(b,s.permission)),a=await Promise.all(e.map(s=>f(s.countUrl,{method:"GET"}).catch(()=>null)));h()&&(y.value=Object.fromEntries(e.map((s,o)=>[s.key,a[o]]).filter(([,s])=>s)))}async function W({first:e=!1}={}){const a=++P,s=e?[_(n.querySelectorAll(".dbx-kpi-value"),ce.figure),_(n.querySelectorAll(".dbx-loading"),{...ce.rows,count:3}),_(n.querySelector(".dbx-chart-note"),Be)]:[],o=()=>Promise.all(s.map(t=>t.done()));try{b=(await f("/auth/permissions",{method:"GET"}).catch(()=>({})))?.permissions||[];const[r,Ee,re]=await Promise.all([j.dashboard.summary().catch(L=>({__error:L})),f("/cache/status",{method:"GET"}).catch(()=>null),B(b,"pages")?j.pages.list().catch(()=>[]):Promise.resolve([]),J()]);if(await o(),a!==P||!h())return;F=new Set((Array.isArray(re)?re:[]).map(L=>L.urlPath).filter(Boolean)),r?.__error?(l.value=null,x.value=/403|forbidden/i.test(String(r.__error?.message))?"":`The dashboard could not load: ${r.__error.message}`):(l.value=r,x.value=r?.warnings?.length?`Some parts could not load: ${r.warnings.join(" \xB7 ")}`:""),m.value=Ee,k.value=Date.now()}finally{await o(),a===P&&(w.value=!0)}}let ve=0;async function ge(){if(!(!h()||!l.peek())){C.value=Date.now();try{const e=await j.dashboard.summaryLite();if(!h())return;const a={...l.peek()};e.traffic&&(a.traffic={...a.traffic,...e.traffic}),e.realtime&&(a.realtime=e.realtime),e.spikes&&(a.spikes=e.spikes),e.health&&(a.health={...a.health,status:e.health.status}),l.value=a,k.value=Date.now()}catch{}++ve%2===0&&J(),f("/cache/status",{method:"GET"}).then(e=>{h()&&(m.value=e)}).catch(()=>{})}}const ye=setInterval(ge,je);d.add(()=>clearInterval(ye));let p=null;const R=n.querySelector(".dbx-canvas"),we=()=>{const e=getComputedStyle(n),a=(s,o)=>e.getPropertyValue(s).trim()||o;return{line:a("--dm-primary","#5b8cff"),text:a("--dm-text-muted","#94a3b8"),grid:a("--dm-border","rgba(127,127,127,.2)")}},xe=e=>/^#[0-9a-f]{6}$/i.test(e)?`${e}26`:"transparent";function Z(){if(!A.peek()||!R?.isConnected||!R.offsetParent)return;const{labels:e,values:a}=S.peek(),{line:s,text:o,grid:t}=we();if(p){p.data.labels=e,p.data.datasets[0].data=a,p.update("none");return}p=new window.Chart(R,{type:"line",data:{labels:e,datasets:[{data:a,borderColor:s,backgroundColor:xe(s),fill:!0,tension:.3,pointRadius:0,borderWidth:2}]},options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{display:!1}},scales:{x:{ticks:{maxTicksLimit:8,color:o},grid:{color:t}},y:{beginAtZero:!0,ticks:{precision:0,color:o},grid:{color:t}}}}})}d.add(M.effect(()=>{S.value,A.value,requestAnimationFrame(Z)})),d.add(()=>{try{p?.destroy()}catch{}p=null});const Q=()=>[...document.body.classList].filter(e=>/^dm-(theme|custom)-/.test(e)).sort().join(" ");let X=Q();const Y=new MutationObserver(()=>{const e=Q();if(e!==X){X=e;try{p?.destroy()}catch{}p=null,requestAnimationFrame(Z)}});Y.observe(document.body,{attributes:!0,attributeFilter:["class"]}),d.add(()=>Y.disconnect());const T=e=>new URL(e,location.origin).href,ee=()=>B(b,"pages"),te=e=>O(e)&&F.has(e.split(/[?#]/)[0].replace(/\/$/,"")||"/");async function ae(e,a){try{await navigator.clipboard.writeText(e),E.toast(`${a} copied.`,{type:"success"})}catch{E.toast("Could not copy - select it and copy it by hand.",{type:"error"})}}function se(e){const{href:a,url:s}=e.dataset;if(a){location.hash=a;return}if(s&&ee()&&te(s)){location.hash=`#/pages/edit${s.split(/[?#]/)[0]}`;return}s&&O(s)&&window.open(T(s),"_blank","noopener")}function ke(e){const{href:a,url:s}=e.dataset,o=[{header:(e.querySelector(".dbx-main strong")?.textContent||"").slice(0,60)}];return a&&o.push({label:"Open",icon:"arrow-right",action:()=>{location.hash=a}}),s&&O(s)?(o.push({label:"Open on the site",icon:"external-link",action:()=>window.open(T(s),"_blank","noopener")}),o.push({label:"Copy address",icon:"copy",action:()=>ae(T(s),"Address")}),ee()&&o.push({label:"Edit the page",icon:"edit",disabled:!te(s),action:()=>{location.hash=`#/pages/edit${s.split(/[?#]/)[0]}`}})):s&&o.push({label:"Copy address",icon:"copy",action:()=>ae(s,"Address")}),o.length>1?o:[]}for(const e of[".dbx-attention",".dbx-activity",".dbx-top-pages",".dbx-spikes",".dbx-entry",".dbx-exit",".dbx-paths",".dbx-checks"]){const a=n.querySelector(e);a&&(a.innerHTML=Oe)}const Ce=M.applyBindings({summary:l,loaded:w,warning:x,asOf:ue,kpi:de,attention:N,activity:D,topPages:U,spikes:G,entry:H,exit:K,paths:V,checks:z,hasJourneys:he,bounce:pe,hasChart:A,chartNote:me,cache:m,cacheOn:fe,cacheView:be},n,{methods:{async toggleCache(e){const a=!!e?.target?.checked;try{m.value=await f("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:a})}),E.toast(`The cache is ${a?"on":"off"}.`,{type:"success"})}catch(s){e?.target&&(e.target.checked=!a),E.toast(s.message||"Could not change the cache.",{type:"error"})}},async clearCache(){if(await E.confirm("Clear the whole response cache? Pages render afresh until it fills again."))try{m.value=await f("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}}}});d.add(()=>Ce.dispose()),d.add($e(n,{position:"bottom"}));const Me=E.contextMenu(n,{cursor:"glyph",match:".dbx-row",inherit:!1,density:"compact",items:e=>ke(e)});d.add(()=>Me?.destroy?.());const oe=e=>{const a=e.target.closest(".dbx-row");if(!a||!n.contains(a))return;const s=e.target.closest(".dbx-kebab");if(s){le(s);return}se(a)},ne=e=>{const a=e.target.closest(".dbx-row");!a||!n.contains(a)||(e.key==="F10"&&e.shiftKey?(e.preventDefault(),le(a)):e.key==="Enter"&&e.target===a&&se(a))};n.addEventListener("click",oe),n.addEventListener("keydown",ne),d.add(()=>{n.removeEventListener("click",oe),n.removeEventListener("keydown",ne)}),ie(i,{id:"dashboard-refresh",icon:"refresh-cw",title:"Refresh",onClick:()=>W()}),ie(i,{id:"dashboard-new-page",icon:"plus",title:"New page",onClick:()=>{location.hash="#/pages/new"}}),I.scan(n),qe().then(e=>{h()&&($.value=e)}),await W({first:!0}),h()&&I.scan(n)}};
@@ -1 +1 @@
1
- import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{themeView as e}from"./theme.js";import{notificationsView as m}from"./notifications.js";import{layoutsView as p}from"./layouts.js";import{mediaView as n}from"./media.js";import{loginView as s}from"./login.js";import{usersView as f}from"./users.js";import{userEditorView as w}from"./user-editor.js";import{pluginsView as c}from"./plugins.js";import{pluginCodeView as V}from"./plugin-code.js";import{documentationView as a}from"./documentation.js";import{tutorialsView as l}from"./tutorials.js";import{docViews as d}from"./doc-pages.js";import{pluginGuidesView as u,pluginGuideView as E}from"./plugin-guides.js";import{apiReferenceView as g}from"./api-reference.js";import{collectionsView as v}from"./collections.js";import{collectionEditorView as b}from"./collection-editor.js";import{collectionEntriesView as h}from"./collection-entries.js";import{formsView as y}from"./forms.js";import{formEditorView as j}from"./form-editor.js";import{formSubmissionsView as k}from"./form-submissions.js";import{viewsListView as L}from"./views-list.js";import{viewEditorView as x}from"./view-editor.js";import{viewPreviewView as M}from"./view-preview.js";import{actionsListView as D}from"./actions-list.js";import{actionEditorView as G}from"./action-editor.js";import{proDocsView as P}from"./pro-docs.js";import{blocksView as R}from"./blocks.js";import{blockEditorView as S}from"./block-editor.js";import{componentsView as A}from"./components.js";import{componentEditorView as C}from"./component-editor.js";import{myProfileView as N}from"./my-profile.js";import{rolesView as T}from"./roles.js";import{roleEditorView as U}from"./role-editor.js";import{effectsView as q}from"./effects.js";import{searchView as z}from"./search.js";import{contentApiView as B}from"./content-api.js";import{notesView as F}from"./notes.js";import{todoView as H}from"./todo.js";import{calculatorView as I}from"../lib/calculator.js";import{analyticsView as J}from"./analytics.js";import{seoView as K}from"./seo.js";import{contactsView as O}from"./contacts.js";import{callNewView as Q}from"./call-new.js";import{menusView as W}from"./menus.js";import{contextMenusView as X}from"./context-menus.js";import{contextMenuEditorView as Y}from"./context-menu-editor.js";import{menuEditorView as Z}from"./menu-editor.js";import{menuLocationsView as _}from"./menu-locations.js";import{projectsView as $}from"./projects.js";import{projectDetailView as oo}from"./project-detail.js";import{projectSettingsView as io}from"./project-settings.js";import{apiTokensView as ro}from"./api-tokens.js";import{apiEndpointsView as to}from"./api-endpoints.js";import{apiEndpointEditorView as eo}from"./api-endpoint-editor.js";const mo={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:$,projectDetail:oo,projectSettings:io,apiTokens:ro,apiEndpoints:to,apiEndpointEditor:eo,dashboard:o,pages:i,pageEditor:r,settings:t,theme:e,layouts:p,media:n,login:s,users:f,userEditor:w,plugins:c,pluginCode:V,documentation:a,tutorials:l,apiReference:g,collections:v,collectionEditor:b,collectionEntries:h,forms:y,formEditor:j,formSubmissions:k,viewsList:L,viewEditor:x,viewPreview:M,actionsList:D,actionEditor:G,proDocs:P,blocks:R,blockEditor:S,components:A,componentEditor:C,myProfile:N,roles:T,roleEditor:U,effects:q,search:z,contentApi:B,notes:F,todo:H,calculator:I,analytics:J,seo:K,contacts:O,callNew:Q,notifications:m,menus:W,contextMenus:X,contextMenuEditor:Y,menuEditor:Z,menuLocations:_,menusRedirect:mo,pluginGuides:u,pluginGuide:E,...d};
1
+ import{dashboardView as o}from"./dashboard.js";import{myDashboardView as i}from"./my-dashboard.js";import{pagesView as r}from"./pages.js";import{pageEditorView as t}from"./page-editor.js";import{settingsView as e}from"./settings.js";import{themeView as m}from"./theme.js";import{notificationsView as p}from"./notifications.js";import{layoutsView as n}from"./layouts.js";import{mediaView as s}from"./media.js";import{loginView as f}from"./login.js";import{usersView as w}from"./users.js";import{userEditorView as c}from"./user-editor.js";import{pluginsView as V}from"./plugins.js";import{pluginCodeView as a}from"./plugin-code.js";import{documentationView as l}from"./documentation.js";import{tutorialsView as d}from"./tutorials.js";import{docViews as u}from"./doc-pages.js";import{pluginGuidesView as E,pluginGuideView as g}from"./plugin-guides.js";import{apiReferenceView as b}from"./api-reference.js";import{collectionsView as h}from"./collections.js";import{collectionEditorView as v}from"./collection-editor.js";import{collectionEntriesView as y}from"./collection-entries.js";import{formsView as j}from"./forms.js";import{formEditorView as k}from"./form-editor.js";import{formSubmissionsView as D}from"./form-submissions.js";import{viewsListView as L}from"./views-list.js";import{viewEditorView as x}from"./view-editor.js";import{viewPreviewView as M}from"./view-preview.js";import{actionsListView as G}from"./actions-list.js";import{actionEditorView as P}from"./action-editor.js";import{proDocsView as R}from"./pro-docs.js";import{blocksView as S}from"./blocks.js";import{blockEditorView as A}from"./block-editor.js";import{componentsView as C}from"./components.js";import{componentEditorView as N}from"./component-editor.js";import{myProfileView as T}from"./my-profile.js";import{rolesView as U}from"./roles.js";import{roleEditorView as q}from"./role-editor.js";import{effectsView as z}from"./effects.js";import{searchView as B}from"./search.js";import{contentApiView as F}from"./content-api.js";import{notesView as H}from"./notes.js";import{todoView as I}from"./todo.js";import{calculatorView as J}from"../lib/calculator.js";import{analyticsView as K}from"./analytics.js";import{seoView as O}from"./seo.js";import{contactsView as Q}from"./contacts.js";import{callNewView as W}from"./call-new.js";import{menusView as X}from"./menus.js";import{contextMenusView as Y}from"./context-menus.js";import{contextMenuEditorView as Z}from"./context-menu-editor.js";import{menuEditorView as _}from"./menu-editor.js";import{menuLocationsView as $}from"./menu-locations.js";import{projectsView as oo}from"./projects.js";import{projectDetailView as io}from"./project-detail.js";import{projectSettingsView as ro}from"./project-settings.js";import{apiTokensView as to}from"./api-tokens.js";import{apiEndpointsView as eo}from"./api-endpoints.js";import{apiEndpointEditorView as mo}from"./api-endpoint-editor.js";const po={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:oo,projectDetail:io,projectSettings:ro,apiTokens:to,apiEndpoints:eo,apiEndpointEditor:mo,dashboard:o,myDashboard:i,pages:r,pageEditor:t,settings:e,theme:m,layouts:n,media:s,login:f,users:w,userEditor:c,plugins:V,pluginCode:a,documentation:l,tutorials:d,apiReference:b,collections:h,collectionEditor:v,collectionEntries:y,forms:j,formEditor:k,formSubmissions:D,viewsList:L,viewEditor:x,viewPreview:M,actionsList:G,actionEditor:P,proDocs:R,blocks:S,blockEditor:A,components:C,componentEditor:N,myProfile:T,roles:U,roleEditor:q,effects:z,search:B,contentApi:F,notes:H,todo:I,calculator:J,analytics:K,seo:O,contacts:Q,callNew:W,notifications:p,menus:X,contextMenus:Y,contextMenuEditor:Z,menuEditor:_,menuLocations:$,menusRedirect:po,pluginGuides:E,pluginGuide:g,...u};
@@ -0,0 +1 @@
1
+ import{apiRequest as r}from"../api.js";import{viewLifetime as m}from"../lib/tool-kit.js";const o=/^#\/my-dashboard\/?(\?.*)?$/,a=m(o);export const myDashboardView={templateUrl:"/admin/js/templates/my-dashboard.html",async onMount(t){a.reset();const e=await r("/dashboard/pro",{method:"GET"}).catch(()=>null);if(!o.test(location.hash))return;const i=t.get(0).querySelector(".dbx")||t.get(0);if(!e?.enabled){i.innerHTML='<p class="dbp-empty">My Dashboard comes with Dashboard Pro, which is not switched on for this site. Find it in the Marketplace.</p>';return}const{mountBoard:s}=await import("./dashboard-board.js");await s(t,a,{isAdmin:e.isAdmin===!0})}};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.0.0",
3
+ "version": "1.2.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",
@@ -84,7 +84,7 @@
84
84
  "@fastify/rate-limit": "^10.3.0",
85
85
  "@fastify/static": "9.1.1",
86
86
  "bcryptjs": "^3.0.3",
87
- "domma-js": "0.51.6",
87
+ "domma-js": "0.51.7",
88
88
  "dotenv": "^17.2.3",
89
89
  "fastify": "5.8.5",
90
90
  "gray-matter": "^4.0.3",