domma-cms 1.10.38 → 1.11.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,30 +1,32 @@
1
- import{api as O}from"../api.js";import{colourToCss as St,HIGHLIGHT_HOVER as lt,HIGHLIGHT_ACTIVE as it,HIGHLIGHT_HOVER_LABELS as ct,HIGHLIGHT_ACTIVE_LABELS as rt,cssClassProblems as dt,normaliseCssClasses as At,ENTRANCE_ANIMATIONS as qe,ENTRANCE_ANIMATION_LABELS as Et}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{openIconPicker as $t}from"../lib/card-builder.js";import{sortableMenuRows as xt}from"../lib/menu-drag.js";import{placeTools as Ct,ensureLockedTools as Lt,isLockedUrl as _t,groupKeyFor as It,slugify as Ht,GROUP_ICON as Tt,TOOLS_FOLDER_KEY as Rt}from"../lib/sidebar-grouping.js";import{flatten as ut,nest as Nt,uid as _e}from"../lib/menu-rows.js";import{addBannerAction as Ue}from"../lib/plugin-chrome.js";import{helpPopovers as Ot}from"../lib/help-popover.js";import{onceClosed as Dt,openMenuAt as Ge,sidebarCount as jt}from"../lib/tool-kit.js";import{addChild as Pt,bindingFrom as Bt,duplicateRow as Ft,indentRow as Mt,insertBelow as zt,isStructural as ae,menuFromForm as Wt,menuProblems as Kt,moveRow as mt,outdentRow as qt,siblingsOf as Ut,snapshot as Ie}from"../lib/menu-editor-arrange.js";import{fmtDateTime as Gt}from"../lib/dates.js";const Vt=760,Yt="admin-sidebar",Xt=/^\/menus\/(new|edit\/[^?#]+)(\?.*)?$/;function y(o){return String(o||"").replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;")}const He=o=>String(o??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function Jt(o,r){let i=0,c=r.find(s=>s.id===o);for(;c&&c.parentId;)i++,c=r.find(s=>s.id===c.parentId);return i}function pt(o){const r=new Set,i=new Set,c=s=>{for(const v of o.filter(k=>k.parentId===s))!v.type&&_t(v.url)&&!i.has(v.url)&&(i.add(v.url),r.add(v.id)),c(v.id)};return c(null),r}const Te='<button type="button" class="btn btn-sm btn-ghost mex-kebab" aria-label="Actions" title="Actions (right-click, or Shift+F10)">\u22EE</button>';function Qt(o,r,i=!1){const c=i?pt(r):new Set,s=[];function v(k){for(const p of r.filter(F=>F.parentId===k)){const x=`style="margin-left:${Jt(p.id,r)*28}px"`;if(p.type==="separator"){s.push(`
2
- <div class="menu-tree-row menu-tree-row--separator" data-id="${p.id}" tabindex="0" ${x}>
1
+ import{api as P}from"../api.js";import{colourToCss as xt,HIGHLIGHT_HOVER as dt,HIGHLIGHT_ACTIVE as ut,HIGHLIGHT_HOVER_LABELS as mt,HIGHLIGHT_ACTIVE_LABELS as pt,cssClassProblems as ht,normaliseCssClasses as St,ENTRANCE_ANIMATIONS as nt,ENTRANCE_ANIMATION_LABELS as At}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{openIconPicker as Et}from"../lib/card-builder.js";import{normalisePanel as $t,DESCRIPTION_MAX as Ct}from"/public/js/menu-look.mjs?v=20261029-menu-cards";import{sortableMenuRows as Lt}from"../lib/menu-drag.js";import{placeTools as Tt,ensureLockedTools as _t,isLockedUrl as It,groupKeyFor as Ht,slugify as Nt,GROUP_ICON as Rt,TOOLS_FOLDER_KEY as Bt}from"../lib/sidebar-grouping.js";import{flatten as ft,nest as Ot,uid as Ge}from"../lib/menu-rows.js";import{addBannerAction as ot}from"../lib/plugin-chrome.js";import{helpPopovers as Dt}from"../lib/help-popover.js";import{onceClosed as Pt,openMenuAt as at,sidebarCount as jt}from"../lib/tool-kit.js";import{addChild as Ft,bindingFrom as Mt,duplicateRow as zt,indentRow as Ut,insertBelow as Wt,isStructural as ge,menuFromForm as Kt,menuProblems as qt,moveRow as gt,outdentRow as Gt,siblingsOf as Vt,snapshot as Ve}from"../lib/menu-editor-arrange.js";import{fmtDateTime as Yt}from"../lib/dates.js";const Xt=760,Jt="admin-sidebar",Qt=/^\/menus\/(new|edit\/[^?#]+)(\?.*)?$/;function A(n){return String(n||"").replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;")}const Ye=n=>String(n??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function Zt(n,d){let i=0,r=d.find(l=>l.id===n);for(;r&&r.parentId;)i++,r=d.find(l=>l.id===r.parentId);return i}function vt(n){const d=new Set,i=new Set,r=l=>{for(const k of n.filter(L=>L.parentId===l))!k.type&&It(k.url)&&!i.has(k.url)&&(i.add(k.url),d.add(k.id)),r(k.id)};return r(null),d}const Xe='<button type="button" class="btn btn-sm btn-ghost mex-kebab" aria-label="Actions" title="Actions (right-click, or Shift+F10)">\u22EE</button>';function en(n,d,i=!1){const r=i?vt(d):new Set,l=[];function k(L){for(const u of d.filter(q=>q.parentId===L)){const C=`style="margin-left:${Zt(u.id,d)*28}px"`;if(u.type==="separator"){l.push(`
2
+ <div class="menu-tree-row menu-tree-row--separator" data-id="${u.id}" tabindex="0" ${C}>
3
3
  <span class="menu-tree-grip" data-icon="grip-vertical" data-icon-size="18" title="Drag to move"></span>
4
4
  <span class="me-sep-line"><span class="me-sep-label">Separator</span></span>
5
- ${Te}
6
- </div>`);continue}if(p.type==="spacer"){const ne=["xs","sm","md","lg","xl","flex"].map(z=>`<option value="${z}"${(p.size||"md")===z?" selected":""}>${z==="flex"?"flexible":z}</option>`).join("");s.push(`
7
- <div class="menu-tree-row menu-tree-row--spacer" data-id="${p.id}" tabindex="0" ${x}>
5
+ ${Xe}
6
+ </div>`);continue}if(u.type==="spacer"){const ve=["xs","sm","md","lg","xl","flex"].map(te=>`<option value="${te}"${(u.size||"md")===te?" selected":""}>${te==="flex"?"flexible":te}</option>`).join("");l.push(`
7
+ <div class="menu-tree-row menu-tree-row--spacer" data-id="${u.id}" tabindex="0" ${C}>
8
8
  <span class="menu-tree-grip" data-icon="grip-vertical" data-icon-size="18" title="Drag to move"></span>
9
9
  <span class="me-spacer-line">
10
10
  <span class="me-spacer-label">Spacer</span>
11
- <select class="form-input me-spacer-size" title="Spacer size - flexible fills the leftover room" aria-label="Spacer size">${ne}</select>
11
+ <select class="form-input me-spacer-size" title="Spacer size - flexible fills the leftover room" aria-label="Spacer size">${ve}</select>
12
12
  </span>
13
- ${Te}
14
- </div>`);continue}if(p.type==="theme-switcher"){s.push(`
15
- <div class="menu-tree-row menu-tree-row--switcher" data-id="${p.id}" tabindex="0" ${x}>
13
+ ${Xe}
14
+ </div>`);continue}if(u.type==="theme-switcher"){l.push(`
15
+ <div class="menu-tree-row menu-tree-row--switcher" data-id="${u.id}" tabindex="0" ${C}>
16
16
  <span class="menu-tree-grip" data-icon="grip-vertical" data-icon-size="18" title="Drag to move"></span>
17
17
  <span class="me-sep-line"><span class="me-sep-label"><span data-icon="palette"></span> Theme switcher - the themes chosen under Theme &gt; Theme switcher</span></span>
18
- ${Te}
19
- </div>`);continue}const J=p.badge&&(p.badge.text||p.badge.countFrom)?`<span class="dm-menu-badge">${y(String(p.badge.text||"#"))}</span>`:"",C=p.tool,g=C&&(C.persisted||p.touched),q=C?C.orphan?' <span class="badge badge-secondary me-tool-badge" title="A placeholder for a Tool that is not enabled - it returns here when it is">Not enabled</span>':` <span class="badge badge-secondary me-tool-badge" title="${g?"A Tool, placed here by this menu":"A Tool in its default place - move or edit it to place it here"}">${y(C.source||"Tool")}${g?"":" \xB7 default"}</span>`:"",se=c.has(p.id),te=St(p.colour),U=te?` style="color:${y(te)}"`:"",le=se?'<button type="button" class="btn btn-sm btn-ghost" disabled title="Always shown - it can be moved, not hidden" aria-label="Always shown"><span data-icon="lock"></span></button>':`<button type="button" class="btn btn-sm btn-ghost me-hidden ${p.hidden?"active":""}" title="${p.hidden?"Hidden - click to show":"Hide"}" aria-label="${p.hidden?"Show":"Hide"}" aria-pressed="${p.hidden?"true":"false"}"><span data-icon="${p.hidden?"eye-off":"eye"}"></span></button>`;s.push(`
20
- <div class="menu-tree-row menu-tree-row--slim${p.hidden?" menu-tree-row--hidden":""}${C?" menu-tree-row--tool":""}${C&&!g?" menu-tree-row--default":""}" data-id="${p.id}" tabindex="0" ${x}>
18
+ ${Xe}
19
+ </div>`);continue}const Q=u.badge&&(u.badge.text||u.badge.countFrom)?`<span class="dm-menu-badge">${A(String(u.badge.text||"#"))}</span>`:"",N=u.tool,v=N&&(N.persisted||u.touched),le=N?N.orphan?' <span class="badge badge-secondary me-tool-badge" title="A placeholder for a Tool that is not enabled - it returns here when it is">Not enabled</span>':` <span class="badge badge-secondary me-tool-badge" title="${v?"A Tool, placed here by this menu":"A Tool in its default place - move or edit it to place it here"}">${A(N.source||"Tool")}${v?"":" \xB7 default"}</span>`:"",Z=r.has(u.id),se=xt(u.colour),ee=se?` style="color:${A(se)}"`:"",ie=Z?'<button type="button" class="btn btn-sm btn-ghost" disabled title="Always shown - it can be moved, not hidden" aria-label="Always shown"><span data-icon="lock"></span></button>':`<button type="button" class="btn btn-sm btn-ghost me-hidden ${u.hidden?"active":""}" title="${u.hidden?"Hidden - click to show":"Hide"}" aria-label="${u.hidden?"Show":"Hide"}" aria-pressed="${u.hidden?"true":"false"}"><span data-icon="${u.hidden?"eye-off":"eye"}"></span></button>`;l.push(`
20
+ <div class="menu-tree-row menu-tree-row--slim${u.hidden?" menu-tree-row--hidden":""}${N?" menu-tree-row--tool":""}${N&&!v?" menu-tree-row--default":""}" data-id="${u.id}" tabindex="0" ${C}>
21
21
  <span class="menu-tree-grip" data-icon="grip-vertical" data-icon-size="18" title="Drag to move"></span>
22
- <span class="me-row-label${!p.text&&!C?" mex-untitled":""}"${U}>${y(p.text||C?.label||"(no label - click to add one)")}${J}${q}</span>
23
- <span class="me-row-url">${y(p.url||"")}</span>
24
- ${le}
22
+ <span class="me-row-label${!u.text&&!N?" mex-untitled":""}"${ee}>${A(u.text||N?.label||"(no label - click to add one)")}${Q}${le}</span>
23
+ <span class="me-row-url">${A(u.url||"")}</span>
24
+ ${ie}
25
25
  <button type="button" class="btn btn-sm btn-ghost me-edit" title="Edit" aria-label="Edit"><span data-icon="edit"></span></button>
26
- ${Te}
27
- </div>`),v(p.id)}}v(null),o.innerHTML=s.join("")||'<p class="mex-note">No items yet - Add item, or right-click an item for more.</p>',I.scan(o)}function w(o,r={},...i){const c=document.createElement(o);Object.assign(c,r);for(const s of i)s!=null&&c.append(s);return c}function T(o,r,i){const c=w("label",{className:"form-field"},w("span",{className:"form-label",textContent:o}),r);return i&&c.append(w("small",{className:"form-hint",textContent:i,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),c}function Zt(o,r){const i=[["","- Everyone -"],["public","Public - anyone, incl. logged-out"],...(r||[]).map(s=>[s.name,`${s.label||s.name} and above`]),...(r||[]).filter(s=>s.level!==0).map(s=>[`=${s.name}`,`${s.label||s.name} only`]),["private","Private - super-admin only"]];o&&!i.some(([s])=>s===o)&&i.push([o,`${o} (custom)`]);const c=ee(i,o||"");return c.name="visibility",c}function ee(o,r){const i=w("select",{className:"form-select"});return o.forEach(([c,s])=>i.add(new Option(s,c))),i.value=r??"",i}function Re(o,r){const i=typeof r=="string"&&r.startsWith("#"),c=ee([["","- none -"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],i?"__custom__":r||""),s=w("input",{type:"color",className:"form-input me-colour-hex",value:i?r:"#000000"});s.style.display=i?"":"none",c.addEventListener("change",()=>{s.style.display=c.value==="__custom__"?"":"none"});const v=T(o,c);return v.append(s),v._read=()=>c.value==="__custom__"?s.value:c.value,v}function en(o,r){const i=w("input",{className:"form-input me-icon",name:"icon",value:r||""}),c=w("span",{className:"me-icon-preview"});r&&c.setAttribute("data-icon",r);const s=w("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),v=k=>{k?c.setAttribute("data-icon",k):c.removeAttribute("data-icon"),Domma.icons.scan(c.parentNode||c)};return s.addEventListener("click",()=>$t(k=>{i.value=k,v(k)})),i.addEventListener("input",()=>v(i.value.trim())),T(o,w("div",{className:"me-icon-field"},i,s,c))}function Ne(...o){return w("div",{className:"me-field-grid"},...o)}async function tn(o,r,i,c,s){const v=o.text||o.tool?.label||"",k=u=>w("h4",{className:"me-form-section",textContent:u}),p=T("Label",w("input",{className:"form-input",name:"text",value:o.text||""}),"The text shown for this item."),F=w("input",{className:"form-input",name:"url",value:o.url||""}),x=T("URL",F,o.tool||o.locked?"A Tool's address is its identity, so it cannot be changed here.":"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items.");(o.tool||o.locked)&&(F.readOnly=!0);const J=en("Icon",o.icon||""),C=Zt(o.visibility||"",c),g=T("Visibility",C,o.locked?"Always in the sidebar for everyone who may use it, so it cannot be restricted here.":'Who can see this item. "Role and above" shows it to that role and anyone more senior; "Role only" shows it to holders of that role alone (and the super-admin). Blank = everyone.');o.locked&&(C.disabled=!0);const q=ee([["","- no permission gate -"],...(r||[]).map(u=>[u.key,u.label||u.key])],o.permission||"");q.name="permission";const se=T("Permission",q,o.tool||o.locked?"A Tool is guarded by its own permission, which always applies.":"Hide unless the viewer holds this specific permission. Independent of Visibility - both must pass.");(o.tool||o.locked)&&(q.disabled=!0);const te=T("Badge text",w("input",{className:"form-input",name:"badgeText",value:o.badge?.text||""})),U=ee([["","- none -"],...(i||[]).map(u=>[u.slug,u.title||u.slug])],o.badge?.countFrom||"");U.name="badgeCount";const le=T("Badge count from",U,"Show a live count of this collection\u2019s entries as the badge."),ne=Re("Badge colour",o.badge?.variant||""),z=ee([["link","Link"],["pill","Pill"]],o.pill?"pill":"link");z.name="renderAs";const Oe=T("Render as",z,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),D=T("Pill style",ee([["filled","Filled"],["outline","Outline"]],o.pill?.style||"filled"));D.querySelector("select").name="pillStyle";const pe=Re("Pill colour",o.pill?.variant||""),ie=w("div",{className:"me-pill-wrap"},Ne(D,pe));ie.style.display=o.pill?"":"none",z.addEventListener("change",()=>{ie.style.display=z.value==="pill"?"":"none"});const h=Re("Text colour",o.colour||""),j=ee([["","- use the menu default -"],...lt.map(u=>[u,ct[u]||u])],o.highlight?.hover||"");j.name="hlHover";const ce=T("Hover effect",j,"Overrides the menu-wide hover effect for this item only."),he=ee([["","- use the menu default -"],...it.map(u=>[u,rt[u]||u])],o.highlight?.active||"");he.name="hlActive";const Se=T("When selected",he,"How this item is marked when it points at the page being viewed."),W=Re("Highlight colour",o.highlight?.accent||""),L=w("input",{className:"form-input",name:"class",value:o.class||"",placeholder:"nav-cta push-right",autocomplete:"off",spellcheck:!1}),re=T("CSS classes",L,"Your own class names, separated by spaces, added to this item's <li> - target the link with .your-class > a. Style them in Custom CSS."),_=w("small",{className:"text-danger",style:"display:none;margin-top:.25rem"});re.append(_);const G=w("button",{className:"btn btn-primary",textContent:"Apply",title:"Apply to the item (Ctrl+Enter) - Save the menu to keep it"}),P=w("div",{className:"me-item-form"},k("Link"),p,x,J,k("Access"),Ne(g,se),k("Badge"),Ne(te,ne),le,k("Appearance"),Oe,ie,h,k("Highlighting"),Ne(ce,Se),W,k("Styling"),re,G),$=E.slideover({title:v?`Edit ${He(v)}`:"Edit menu item",size:"lg",position:"right"});$.element.appendChild(P);const fe=()=>JSON.stringify([...P.querySelectorAll("input, select, textarea")].map(u=>u.type==="checkbox"?u.checked:u.value)),Ae=fe();let de=!1,ue=!1;Dt($);const V=$.close.bind($);$.close=async u=>{if(!de&&u!==!0&&fe()!==Ae){if(ue)return;ue=!0;const Q=await E.confirm("Discard the changes to this item?");if(ue=!1,!Q)return}return de=!0,V()},P.addEventListener("keydown",u=>{(u.ctrlKey||u.metaKey)&&u.key==="Enter"&&(u.preventDefault(),G.click())}),$.open();const oe=$.element.closest(".dm-slideover")||$.element.querySelector(".dm-slideover")||$.element;oe.style.setProperty("width",Vt+"px","important"),oe.style.setProperty("max-width","95vw","important"),Domma.icons.scan(P);const A=u=>P.querySelector(`[name="${u}"]`)?.value||"";G.addEventListener("click",()=>{const u=dt(A("class"));if(u.length){_.textContent=u.join("; "),_.style.display="block",L.focus();return}o.class=At(A("class")),o.text=A("text"),o.icon=A("icon"),o.locked||(o.visibility=A("visibility")),!o.tool&&!o.locked?(o.url=A("url"),o.permission=A("permission")):o.touched=!0;const Q=A("badgeText"),me=A("badgeCount"),ge=ne._read();if(o.badge=Q||me?{...Q&&{text:Q},...me&&{countFrom:me},...ge&&{variant:ge}}:null,A("renderAs")==="pill"){const we=pe._read();o.pill={style:A("pillStyle")||"filled",...we&&{variant:we}}}else o.pill=null;o.colour=h._read();const ve=A("hlHover"),be=A("hlActive"),ye=W._read();o.highlight=ve||be||ye?{...ve&&{hover:ve},...be&&{active:be},...ye&&{accent:ye}}:null,de=!0,$.close(!0),s()})}let K=null,ht=!1;function nn(){ht||!window.R?.use||(ht=!0,R.use((o,r,i)=>{const c=r?.path||"";return!(Xt.test(c)&&(o?.path||"")!==c)||!K?.dirty()?i():E.confirm(`You have unsaved changes to the menu "${He(K.name())}". Leave anyway? They are kept as a draft for your next visit.`).then(v=>{v?i():history.replaceState(null,"",`#${c}`)})}),M.subscribe("router:afterChange",({to:o}={})=>{if(K&&(o?.path||"")!==K.path){const r=K;K=null,r.dispose()}}))}function ft(o,r){const i=o.querySelector(".dm-plugin-banner-name"),c=i?.querySelector(".dm-plugin-banner-sep");if(!(!i||!c)){for(;c.nextSibling;)c.nextSibling.remove();i.appendChild(document.createTextNode(r))}}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(o){if(nn(),K){const e=K;K=null,e.dispose()}const r=[],i=e=>(r.push(e),e);let c=!1;const s=o.get(0).querySelector(".mex")||o.get(0),v=()=>!c&&s.isConnected,k=location.hash.replace(/^#/,""),p=location.hash.match(/^#\/menus\/edit\/([^?#]+)/),F=p?decodeURIComponent(p[1]):null,x=!F,J=`menu-editor.draft.${F||"new"}`,C=E.loader(o.get(0),{type:"dots"});let g,q,se,te,U,le,ne;try{if(x)g={slug:"",name:"",description:"",items:[]};else try{g=await O.menus.get(F)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}[q,se,te,U,le,ne]=await Promise.all([O.projects.list().catch(()=>[]),H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),O.collections.list().catch(()=>[]),O.get("/collections/roles/entries?limit=100").catch(()=>null),O.menuLocations.registry().catch(()=>[]),O.pages.list().catch(()=>[])])}finally{C.destroy()}if(!s.isConnected)return;const Oe=(Array.isArray(U)?U:U?.entries||[]).map(e=>({name:e.data?.name,label:e.data?.label,level:e.data?.level??99})).filter(e=>e.name).sort((e,t)=>t.level-e.level),D=!x&&F===Yt;let pe=[];if(D&&(pe=await H.get("/api/sidebar/registered-items").catch(()=>(E.toast("Could not load the Tools - they are not shown, and saving keeps their places as they are.",{type:"warning",duration:8e3}),[])),!s.isConnected))return;const ie=e=>ut(D?Lt(Ct(e||[],pe,{forEditor:!0}),{leaveInHiddenFolders:!0}):e);let h=ie(g.items);const j=M.observable(!1),ce=M.observable(!1),he=M.observable(""),Se=M.observable(0);let W="";const L=s.querySelector("#me-items-list");function re(){Qt(L,h,D),Se.value=h.filter(e=>!ae(e)).length}const _=()=>{re(),Y()},G=()=>D?pt(h):new Set,P=e=>h.find(t=>t.id===e);function $(e){requestAnimationFrame(()=>L.querySelector(`.menu-tree-row[data-id="${CSS.escape(e)}"]`)?.focus())}function fe(e){const t=f=>h.find(l=>!l.type&&!l.url&&!l.tool&&(l.key||"").toLowerCase()===f)||h.find(l=>!l.type&&!l.url&&!l.tool&&!l.key&&(l.text||"").toLowerCase()===f);let a=t(String(e.tool.folder||"").toLowerCase())||t(Rt);const m=String(e.tool.group||"").trim();if(a&&m){const f=It(a.key||Ht(a.text),m),l=d=>d!==e&&!d.type&&!d.url&&!d.tool;let b=h.find(d=>l(d)&&(d.key||"").toLowerCase()===f)||h.find(d=>l(d)&&d.parentId===a.id&&(!d.key||d.key.toLowerCase()===f)&&(d.text||"").toLowerCase()===m.toLowerCase());b||([b]=ut([{text:m,key:f,icon:Tt,synthesised:!0}],a.id),h.push(b)),a=b}e.parentId=a?a.id:null,h.splice(h.indexOf(e),1),h.push(e),Object.assign(e,{text:e.tool.defaults.text,icon:e.tool.defaults.icon,visibility:"",hidden:!1,badge:null,pill:null,colour:"",highlight:null,touched:!1}),e.tool={...e.tool,persisted:!1}}const Ae=e=>!G().has(e.id)&&!(e.tool&&!e.tool.orphan);async function de(e){const t=P(e);if(!t||!Ae(t))return;const a=d=>{let X=d;for(;X;){if(X.id===e)return!0;X=h.find(B=>B.id===X.parentId)}return!1},m=h.filter(d=>d.id!==e&&a(d));if(m.length&&!await E.confirm(`Remove "${He(t.text||t.type||"this item")}" and the ${m.length} item${m.length===1?"":"s"} beneath it?`))return;const f=G(),l=m.filter(d=>d.tool),b=m.filter(d=>!d.tool&&f.has(d.id));h=h.filter(d=>!a(d)||l.includes(d)||b.includes(d));for(const d of b)d.parentId=t.parentId??null;for(const d of l)d.tool.orphan?d.parentId=t.parentId??null:fe(d);_()}function ue(e){const t=P(e);!t||ae(t)||G().has(e)||(t.hidden=!t.hidden,t.touched=!0,_())}function V(e){const t=P(e);!t||ae(t)||(t.locked=D&&G().has(t.id),tn(t,se.resources||[],te,Oe,()=>{_(),$(e)}))}function oe(e){const t={id:_e(),parentId:null,...e==="separator"||e==="theme-switcher"?{type:e}:e==="spacer"?{type:e,size:"md"}:{text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}};h.push(t),_(),ae(t)?$(t.id):V(t.id)}const A=e=>t=>{e(t)&&(_(),$(t))},u=A(e=>mt(h,e,-1)),Q=A(e=>mt(h,e,1)),me=A(e=>Mt(h,e)),ge=A(e=>qt(h,e));function ve(e){const t=P(e);if(!t)return[];const a=ae(t),m=G().has(e),f=Ut(h,e),l=f.findIndex(B=>B.id===e),b=t.tool,d=b&&(b.persisted||t.touched),X=B=>()=>{const ke=zt(h,e,B,_e());_(),ke&&!ae(ke)?V(ke.id):ke&&$(ke.id)};return[{header:a?{separator:"Separator",spacer:"Spacer","theme-switcher":"Theme switcher"}[t.type]:t.text||b?.label||"(untitled)"},...a?[]:[{label:"Edit\u2026",icon:"edit",action:()=>V(e)}],{type:"divider"},{label:"Add item below",icon:"plus",action:X("item")},{label:"Add sub-item",icon:"corner-down-right",disabled:a,action:()=>{const B=Pt(h,e,_e());_(),B&&V(B.id)}},{label:"Add separator below",icon:"minus",action:X("separator")},{label:"Add spacer below",icon:"move",action:X("spacer")},...D?[]:[{label:"Add theme switcher below",icon:"palette",action:X("theme-switcher")}],{type:"divider"},{label:"Move up",icon:"arrow-up",shortcut:"Alt+\u2191",disabled:l<=0,action:()=>u(e)},{label:"Move down",icon:"arrow-down",shortcut:"Alt+\u2193",disabled:l<0||l>=f.length-1,action:()=>Q(e)},{label:"Indent",icon:"chevron-right",shortcut:"Alt+\u2192",disabled:l<=0||ae(f[l-1]),action:()=>me(e)},{label:"Outdent",icon:"chevron-left",shortcut:"Alt+\u2190",disabled:t.parentId==null,action:()=>ge(e)},{type:"divider"},...a?[]:[{label:t.hidden?"Show":"Hide",icon:t.hidden?"eye":"eye-off",disabled:m,action:()=>ue(e)}],{label:"Duplicate",icon:"copy",disabled:!!b,action:()=>{const B=Ft(h,e,_e);_(),B&&$(B.id)}},...b&&!b.orphan&&d?[{label:"Back to its default place",icon:"rotate-ccw",action:()=>{fe(t),_()}}]:[],{label:m?"Remove - always in the sidebar":b&&!b.orphan?"Remove - a Tool is hidden, not removed":"Remove\u2026",icon:"trash",danger:!0,disabled:!Ae(t),action:()=>de(e)}]}const be=E.contextMenu(L,{cursor:"glyph",match:".menu-tree-row",inherit:!1,density:"compact",items:e=>ve(e.dataset.id)});i(()=>be?.destroy?.());const ye=xt(L,()=>h,_,{items:".menu-tree-row",handle:".menu-tree-grip"});i(()=>ye?.destroy());const we=e=>{const t=e.target.closest(".menu-tree-row");if(!t)return;const a=t.dataset.id;if(e.target.closest(".mex-kebab")){Ge(e.target.closest(".mex-kebab"));return}if(e.target.closest(".me-hidden")){ue(a);return}if(e.target.closest(".me-edit")){V(a);return}e.target.closest("select, .menu-tree-grip, button")||V(a)},Ve=e=>{const t=e.target.closest(".menu-tree-row");if(!t||e.target!==t){t&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),Ge(t));return}const a=t.dataset.id;if(e.key==="F10"&&e.shiftKey)e.preventDefault(),Ge(t);else if(e.key==="Enter")e.preventDefault(),V(a);else if(e.key==="Delete")e.preventDefault(),de(a);else if(e.altKey&&e.key==="ArrowUp")e.preventDefault(),u(a);else if(e.altKey&&e.key==="ArrowDown")e.preventDefault(),Q(a);else if(e.altKey&&e.key==="ArrowRight")e.preventDefault(),me(a);else if(e.altKey&&e.key==="ArrowLeft")e.preventDefault(),ge(a);else if(e.key==="ArrowDown"||e.key==="ArrowUp"){const m=[...L.querySelectorAll(".menu-tree-row")],f=m.indexOf(t)+(e.key==="ArrowDown"?1:-1);m[f]&&(e.preventDefault(),m[f].focus())}},Ye=e=>{if(!e.target.matches(".me-spacer-size"))return;const t=P(e.target.closest(".menu-tree-row")?.dataset.id);t&&(t.size=e.target.value,Y())};L.addEventListener("click",we),L.addEventListener("keydown",Ve),L.addEventListener("change",Ye),i(()=>{L.removeEventListener("click",we),L.removeEventListener("keydown",Ve),L.removeEventListener("change",Ye)});const n=e=>o.find(e),Ee=n("#me-float-accent"),De=n("#me-float-accent-hex"),je=()=>{De.get(0).style.display=Ee.val()==="__custom__"?"":"none"},gt=()=>Ee.val()==="__custom__"?De.val():Ee.val(),$e=n("#me-highlight-accent"),Pe=n("#me-highlight-accent-hex"),Xe=()=>{Pe.get(0).style.display=$e.val()==="__custom__"?"":"none"},vt=()=>$e.val()==="__custom__"?Pe.val():$e.val(),Je=()=>{const e=!!(n("#me-entrance-enabled").get(0)||{}).checked;n(".dm-entrance-field").each(function(){this.style.display=e?"":"none"});const t=n("#me-position").val()==="floating";n(".dm-float-field").each(function(){this.style.display=t?"":"none"}),t&&je();const a=n("#me-orientation").val()==="vertical";n(".dm-vertical-field").each(function(){this.style.display=a?"":"none"}),n(".dm-dock-rail-field").each(function(){this.style.display=a&&!t?"":"none"}),n(".dm-dock-bar-field").each(function(){this.style.display=!a&&!t?"":"none"});const m=!!(n("#me-dock-collapsible").get(0)||{}).checked,f=!!(n("#me-dock-autohide").get(0)||{}).checked;n(".dm-dock-collapsed-field").each(function(){this.style.display=m&&!f?"":"none"});const l=!!(n("#me-float-collapsible").get(0)||{}).checked;n(".dm-float-collapsed-field").each(function(){this.style.display=t&&l?"":"none"})},Qe=(e,t,a,m)=>{const f=n(e).get(0);f&&(f.innerHTML="",f.add(new Option(m,"")),t.forEach(l=>f.add(new Option(a[l]||l,l))))};Qe("#me-highlight-hover",lt,ct,"- theme default -"),Qe("#me-highlight-active",it,rt,"- theme default -");{const e=n("#me-entrance-animation").get(0);e&&(e.innerHTML="",qe.forEach(t=>e.add(new Option(Et[t]||t,t))))}const Be=n("#me-bind-slot"),Fe=n("#me-bind-projects"),xe=n("#me-bind-pages");Be.html('<option value="">- not bound -</option>'+(le||[]).map(e=>`<option value="${y(e.slot)}">${y(e.label||e.slot)} (${y(e.slot)})</option>`).join("")),Fe.html(q.map(e=>`<option value="${y(e.slug)}">${y(e.name||e.slug)}</option>`).join("")),n("#me-bind-page-pick").html('<option value="">- pick a page -</option>'+ne.map(e=>{const t=e.urlPath||e.url||"";return t?`<option value="${y(t)}">${y(e.title||t)} - ${y(t)}</option>`:""}).join(""));const Ze=()=>Bt({slot:Be.val(),projects:Array.from(Fe.get(0).selectedOptions).map(e=>e.value),pagesText:xe.val()});function Me(){const e=Ze(),t=n("#me-bind-summary");if(!e){t.html('<span class="text-muted">Not bound - this menu renders only where it is mapped or placed by a shortcode.</span>');return}const a=[];e.projects.length&&a.push(`pages in ${e.projects.map(l=>`<code>${y(l)}</code>`).join(", ")}`),e.pages.length&&a.push(`pages matching ${e.pages.map(l=>`<code>${y(l)}</code>`).join(", ")}`);const m=e.pages.filter(l=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(l)),f=m.length?`<div class="text-danger" style="margin-top:.4rem">Not valid page patterns: ${m.map(l=>`<code>${y(l)}</code>`).join(", ")}</div>`:"";t.html(a.length?`Takes over the <code>${y(e.slot)}</code> slot on ${a.join(" and ")}.${f}`:`<span class="text-warning">Slot <code>${y(e.slot)}</code> selected but no project or page chosen - the binding will never match.</span>`)}const{getProjectFromHash:bt}=await import("../lib/project-context.js");if(!v())return;const yt=x?bt():null;n("#me-project").html('<option value="">- none -</option>'+q.map(e=>`<option value="${y(e.slug)}">${y(e.name||e.slug)}</option>`).join(""));function ze(e){h=ie(e.items),n("#me-variant").val(e.variant||""),n("#me-position").val(e.position||"");const t=n("#me-appearOnHover").get(0);t&&(t.checked=e.appearOnHover===!0),n("#me-float-anchor").val(e.float?.anchor||"TL"),n("#me-float-offsetX").val(e.float?.offsetX??""),n("#me-float-offsetY").val(e.float?.offsetY??""),n("#me-float-width").val(e.float?.width??""),n("#me-float-radius").val(e.float?.radius??""),n("#me-float-shadow").val(e.float?.shadow||""),n("#me-float-opacity").val(e.float?.opacity??""),n("#me-float-title").val(e.float?.title||"");for(const[l,b]of[["#me-float-collapsible",e.float?.collapsible],["#me-float-collapsed",e.float?.collapsed],["#me-float-movable",e.float?.movable],["#me-float-resizable",e.float?.resizable]]){const d=n(l).get(0);d&&(d.checked=b===!0)}const a=e.float?.accent||"";Ee.val(a.startsWith("#")?"__custom__":a),a.startsWith("#")&&De.val(a),je(),n("#me-orientation").val(e.orientation||""),n("#me-align").val(e.align||""),n("#me-mobile-hamburger").get(0).checked=e.mobile?.hamburgerOnly===!0,n("#me-mobile-brand").get(0).checked=e.mobile?.hideBrand===!0,n("#me-side").val(e.side||"left"),n("#me-dock-width").val(e.dock?.width??"");for(const l of["collapsible","collapsed","autohide","hideOnScroll"]){const b=n(`#me-dock-${l}`).get(0);b&&(b.checked=e.dock?.[l]===!0)}n("#me-class").val(e.class||""),n("#me-highlight-hover").val(e.highlight?.hover||""),n("#me-highlight-active").val(e.highlight?.active||"");const m=e.highlight?.accent||"";$e.val(m.startsWith("#")?"__custom__":m),m.startsWith("#")&&Pe.val(m),Xe();{const l=n("#me-entrance-enabled").get(0);l&&(l.checked=e.entrance?.enabled===!0)}n("#me-entrance-animation").val(qe.includes(e.entrance?.animation)?e.entrance.animation:qe[0]),n("#me-entrance-stagger").val(e.entrance?.stagger??""),n("#me-entrance-duration").val(e.entrance?.duration??""),n("#me-fontFamily").val(e.style?.fontFamily||""),n("#me-fontSize").val(e.style?.fontSize||""),n("#me-fontWeight").val(e.style?.fontWeight||""),n("#me-letterSpacing").val(e.style?.letterSpacing||""),n("#me-iconSize").val(e.style?.iconSize||""),Be.val(e.binding?.slot||"");const f=new Set(e.binding?.projects||[]);Array.from(Fe.get(0).options).forEach(l=>{l.selected=f.has(l.value)}),xe.val((e.binding?.pages||[]).join(`
28
- `)),n("#me-slug").val(e.slug||""),n("#me-name").val(e.name||""),n("#me-description").val(e.description||""),n("#me-project").val(e.meta?.project||yt||""),Je(),Me(),re()}const N=e=>!!(n(e).get(0)||{}).checked;function Ce(){const e={variant:n("#me-variant").val(),position:n("#me-position").val(),appearOnHover:N("#me-appearOnHover"),floatAnchor:n("#me-float-anchor").val(),floatOffsetX:n("#me-float-offsetX").val(),floatOffsetY:n("#me-float-offsetY").val(),floatWidth:n("#me-float-width").val(),floatRadius:n("#me-float-radius").val(),floatShadow:n("#me-float-shadow").val(),floatAccent:gt(),floatOpacity:n("#me-float-opacity").val(),floatTitle:n("#me-float-title").val(),floatCollapsible:N("#me-float-collapsible"),floatCollapsed:N("#me-float-collapsed"),floatMovable:N("#me-float-movable"),floatResizable:N("#me-float-resizable"),highlightHover:n("#me-highlight-hover").val(),highlightActive:n("#me-highlight-active").val(),highlightAccent:vt(),class:n("#me-class").val(),entranceEnabled:N("#me-entrance-enabled"),entranceAnimation:n("#me-entrance-animation").val(),entranceStagger:n("#me-entrance-stagger").val(),entranceDuration:n("#me-entrance-duration").val(),orientation:n("#me-orientation").val(),align:n("#me-align").val(),side:n("#me-side").val(),fontFamily:n("#me-fontFamily").val(),fontSize:n("#me-fontSize").val(),fontWeight:n("#me-fontWeight").val(),letterSpacing:n("#me-letterSpacing").val(),iconSize:n("#me-iconSize").val(),dockWidth:n("#me-dock-width").val(),dockCollapsible:N("#me-dock-collapsible"),dockCollapsed:N("#me-dock-collapsed"),dockAutohide:N("#me-dock-autohide"),mobileHamburgerOnly:N("#me-mobile-hamburger"),mobileHideBrand:N("#me-mobile-brand"),dockHideOnScroll:N("#me-dock-hideOnScroll")},t={slug:n("#me-slug").val(),name:n("#me-name").val(),description:n("#me-description").val(),project:n("#me-project").val()};return Wt(e,t,Ze(),Nt(h),g.meta||{})}const et=e=>[...Kt(e),...dt(e.class).map(t=>`CSS classes: ${t} (Behaviour tab).`)];let tt=!1,We=null;function Y(){if(!W)return;const e=Ce();We=e,j.value=Ie(e)!==W,he.value=j.peek()||tt?et(e).join(" "):""}const Le=e=>{e.target.closest("#me-items-list")||(e.target.matches("#me-position, #me-orientation, #me-float-collapsible, #me-dock-collapsible, #me-dock-autohide, #me-entrance-enabled")&&Je(),e.target.matches("#me-float-accent")&&je(),e.target.matches("#me-highlight-accent")&&Xe(),e.target.closest("#me-binding")&&Me(),Y())};s.addEventListener("input",Le),s.addEventListener("change",Le),i(()=>{s.removeEventListener("input",Le),s.removeEventListener("change",Le)}),n("#me-bind-page-add").on("click",()=>{const e=n("#me-bind-page-pick").val();if(!e)return;const t=xe.val().split(`
29
- `).map(a=>a.trim()).filter(Boolean);t.includes(e)||t.push(e),xe.val(t.join(`
30
- `)),Me(),Y()}),ze(g),W=Ie(Ce()),Y();const nt=()=>{try{S.remove?S.remove(J):S.set(J,null)}catch{}};async function Ke(){if(ce.peek())return;const e=Ce(),t=et(e);if(t.length){tt=!0,Y(),E.toast(t.join(" "),{type:"warning",duration:7e3});return}ce.value=!0;try{if(x)await O.menus.create(e);else if(e.slug!==g.slug){await O.menus.create(e);const a=await O.menuLocations.get();let m=!1;for(const[f,l]of Object.entries(a))l===g.slug&&(a[f]=e.slug,m=!0);m&&await O.menuLocations.save(a),await O.menus.remove(g.slug)}else await O.menus.update(g.slug,e);if(W=Ie(e),j.value=!1,nt(),jt("#/menus"),E.toast("Saved.",{type:"success"}),x||e.slug!==g.slug)location.hash="#/menus/edit/"+encodeURIComponent(e.slug);else{g={...g,...e};for(const a of h)a.tool&&a.touched&&(a.tool={...a.tool,persisted:!0});re(),W=Ie(Ce()),Y(),ft(o.get(0),e.name||e.slug)}if(D)try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}catch(a){E.toast(`Save failed: ${a.message||a}`,{type:"error"})}finally{ce.value=!1}}async function wt(){!j.peek()||!await E.confirm("Throw away the changes you have not saved?")||(ze(g),Y())}E.tabs(n("#me-tabs").get(0));const kt=M.applyBindings({dirty:j,saving:ce,problems:he,itemCount:Se},s,{methods:{save:()=>Ke(),discard:()=>wt(),addItem:()=>oe("item"),addSeparator:()=>oe("separator"),addSpacer:()=>oe("spacer"),addSwitcher:()=>oe("theme-switcher")}});i(()=>kt.dispose()),i(Ot(s,{position:"bottom"})),D&&n("#me-add-switcher").hide(),ft(o.get(0),x?"New menu":g.name||g.slug),Ue(o,{id:"menu-editor-back",icon:"arrow-left",title:"All menus",onClick:()=>{location.hash="#/menus"}}),Ue(o,{id:"menu-editor-locations",icon:"map-pin",title:"Menu locations",onClick:()=>{location.hash="#/menu-locations"}}),Ue(o,{id:"menu-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>Ke()});const ot=e=>{if(!(!(e.ctrlKey||e.metaKey)||e.key!=="s")){if(e.preventDefault(),e.stopImmediatePropagation(),document.querySelector(".me-item-form")?.offsetParent){E.toast("Apply the item first (Ctrl+Enter), then Save.",{type:"info"});return}Ke()}};document.addEventListener("keydown",ot,!0),i(()=>document.removeEventListener("keydown",ot,!0));const at=()=>{if(!(!j.peek()||!We))try{S.set(J,{menu:We,base:W,at:new Date().toISOString()})}catch{}},st=e=>{j.peek()&&(at(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",st),i(()=>window.removeEventListener("beforeunload",st)),K={path:k,dirty:()=>j.peek(),name:()=>n("#me-name").val()||g.name||g.slug||"new menu",dispose(){c=!0,at(),r.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(s);let Z=null;try{Z=S.get(J)}catch{}if(Z?.menu){nt();const e=Z.base&&Z.base!==W,t=Z.at?` (${Gt(Z.at)})`:"",a=x?"the new menu you started":`the menu "${He(g.name||g.slug)}"`,m=e?`You left unsaved changes to ${a}${t}, but the saved menu has changed since. Restore your version anyway?`:`You left unsaved changes to ${a}${t}. Restore them?`;await E.confirm(m)&&v()&&(ze({...Z.menu,meta:{...g.meta||{},...Z.menu.meta||{}}}),Y())}}};
26
+ ${Xe}
27
+ </div>`),k(u.id)}}k(null),n.innerHTML=l.join("")||'<p class="mex-note">No items yet - Add item, or right-click an item for more.</p>',I.scan(n)}function b(n,d={},...i){const r=document.createElement(n);Object.assign(r,d);for(const l of i)l!=null&&r.append(l);return r}function y(n,d,i){const r=b("label",{className:"form-field"},b("span",{className:"form-label",textContent:n}),d);return i&&r.append(b("small",{className:"form-hint",textContent:i,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),r}function tn(n,d){const i=[["","- Everyone -"],["public","Public - anyone, incl. logged-out"],...(d||[]).map(l=>[l.name,`${l.label||l.name} and above`]),...(d||[]).filter(l=>l.level!==0).map(l=>[`=${l.name}`,`${l.label||l.name} only`]),["private","Private - super-admin only"]];n&&!i.some(([l])=>l===n)&&i.push([n,`${n} (custom)`]);const r=W(i,n||"");return r.name="visibility",r}function W(n,d){const i=b("select",{className:"form-select"});return n.forEach(([r,l])=>i.add(new Option(l,r))),i.value=d??"",i}function De(n,d){const i=typeof d=="string"&&d.startsWith("#"),r=W([["","- none -"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],i?"__custom__":d||""),l=b("input",{type:"color",className:"form-input me-colour-hex",value:i?d:"#000000"});l.style.display=i?"":"none",r.addEventListener("change",()=>{l.style.display=r.value==="__custom__"?"":"none"});const k=y(n,r);return k.append(l),k._read=()=>r.value==="__custom__"?l.value:r.value,k}function nn(n,d){const i=b("input",{className:"form-input me-icon",name:"icon",value:d||""}),r=b("span",{className:"me-icon-preview"});d&&r.setAttribute("data-icon",d);const l=b("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),k=L=>{L?r.setAttribute("data-icon",L):r.removeAttribute("data-icon"),Domma.icons.scan(r.parentNode||r)};return l.addEventListener("click",()=>Et(L=>{i.value=L,k(L)})),i.addEventListener("input",()=>k(i.value.trim())),y(n,b("div",{className:"me-icon-field"},i,l,r))}function J(...n){return b("div",{className:"me-field-grid"},...n)}async function on(n,d,i,r,l,{cards:k=!0}={}){const L=n.text||n.tool?.label||"",u=m=>b("h4",{className:"me-form-section",textContent:m}),q=y("Label",b("input",{className:"form-input",name:"text",value:n.text||""}),"The text shown for this item."),C=b("input",{className:"form-input",name:"url",value:n.url||""}),Q=y("URL",C,n.tool||n.locked?"A Tool's address is its identity, so it cannot be changed here.":"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items.");(n.tool||n.locked)&&(C.readOnly=!0);const N=nn("Icon",n.icon||""),v=tn(n.visibility||"",r),le=y("Visibility",v,n.locked?"Always in the sidebar for everyone who may use it, so it cannot be restricted here.":'Who can see this item. "Role and above" shows it to that role and anyone more senior; "Role only" shows it to holders of that role alone (and the super-admin). Blank = everyone.');n.locked&&(v.disabled=!0);const Z=W([["","- no permission gate -"],...(d||[]).map(m=>[m.key,m.label||m.key])],n.permission||"");Z.name="permission";const se=y("Permission",Z,n.tool||n.locked?"A Tool is guarded by its own permission, which always applies.":"Hide unless the viewer holds this specific permission. Independent of Visibility - both must pass.");(n.tool||n.locked)&&(Z.disabled=!0);const ee=y("Badge text",b("input",{className:"form-input",name:"badgeText",value:n.badge?.text||""})),ie=W([["","- none -"],...(i||[]).map(m=>[m.slug,m.title||m.slug])],n.badge?.countFrom||"");ie.name="badgeCount";const ve=y("Badge count from",ie,"Show a live count of this collection\u2019s entries as the badge."),te=De("Badge colour",n.badge?.variant||""),be=W([["link","Link"],["pill","Pill"]],n.pill?"pill":"link");be.name="renderAs";const D=y("Render as",be,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),$e=y("Pill style",W([["filled","Filled"],["outline","Outline"]],n.pill?.style||"filled"));$e.querySelector("select").name="pillStyle";const Ce=De("Pill colour",n.pill?.variant||""),h=b("div",{className:"me-pill-wrap"},J($e,Ce));h.style.display=n.pill?"":"none",be.addEventListener("change",()=>{h.style.display=be.value==="pill"?"":"none"});const j=De("Text colour",n.colour||""),re=W([["","- use the menu default -"],...dt.map(m=>[m,mt[m]||m])],n.highlight?.hover||"");re.name="hlHover";const Pe=y("Hover effect",re,"Overrides the menu-wide hover effect for this item only."),Le=W([["","- use the menu default -"],...ut.map(m=>[m,pt[m]||m])],n.highlight?.active||"");Le.name="hlActive";const G=y("When selected",Le,"How this item is marked when it points at the page being viewed."),T=De("Highlight colour",n.highlight?.accent||""),ye=b("input",{className:"form-input",name:"class",value:n.class||"",placeholder:"nav-cta push-right",autocomplete:"off",spellcheck:!1}),B=y("CSS classes",ye,"Your own class names, separated by spaces, added to this item's <li> - target the link with .your-class > a. Style them in Custom CSS."),V=b("small",{className:"text-danger",style:"display:none;margin-top:.25rem"});B.append(V);const ce=b("textarea",{className:"form-input",name:"description",rows:2,maxLength:Ct,value:n.description||"",placeholder:"Answers every new lead in under a minute."}),de=y("Description",ce,"One short line under the label, shown when the menu's dropdowns are Cards."),Te=De("Tile colour",n.tile||""),_e=b("input",{type:"checkbox",name:"chip",checked:!!n.chip,style:"width:auto;margin:0"}),je=b("label",{className:"form-field",style:"display:flex;gap:.5rem;align-items:center;font-weight:400"},_e,b("span",{textContent:"Show as a small chip in the card's footer strip"})),ue=n.parentId==null,x=n.panel||{},O=(m,F,he,Ee=80)=>b("input",{className:"form-input",name:m,value:F||"",placeholder:he,maxLength:Ee}),ke=y("Card heading",O("panelHeading",x.heading,"The crew - six agents, one job each"),"Small capitals over the entries."),Fe=y("Link text",O("panelLinkText",x.link?.text,"Meet the crew",60)),Me=y("Link URL",O("panelLinkUrl",x.link?.url,"/crew",500)),Ie=W([["","Automatic"],["1","1"],["2","2"],["3","3"],["4","4"]],x.columns?String(x.columns):"");Ie.name="panelColumns";const ze=y("Columns",Ie),Je=y("Chip strip label",O("panelChipsLabel",x.chipsLabel,"Add to your crew",60),'Words before the chips - tick "Show as a small chip" on the sub-items that belong there.'),me=W([["","- no side box -"],["dark","Dark"],["light","Light"],["accent","Theme colour"]],x.feature?x.feature.tone||"dark":"");me.name="featureTone";const Qe=y("Side box",me,"A box down the right of the card - a call to action or a short list."),Ue=y("Box small heading",O("featureEyebrow",x.feature?.eyebrow,"Free - 20 minutes",60)),We=y("Box title",O("featureTitle",x.feature?.title,"See it live")),Ke=y("Box points",b("textarea",{className:"form-input",name:"featurePoints",rows:3,value:(x.feature?.points||[]).join(`
28
+ `),placeholder:"One per line, up to six"})),t=y("Button text",O("featureBtnText",x.feature?.button?.text,"Book a demo",60)),we=y("Button URL",O("featureBtnUrl",x.feature?.button?.url,"/book",500)),pe=b("div",{},J(Ue,We),Ke,J(t,we));pe.style.display=me.value?"":"none",me.addEventListener("change",()=>{pe.style.display=me.value?"":"none"});const He=k?[u("Card dropdown"),b("small",{className:"form-hint",style:"display:block;opacity:.7;margin-bottom:.4rem",textContent:"Used when the menu's Dropdown style is Cards (Behaviour tab)."}),de,J(Te,ue?null:je),...ue?[ke,J(Fe,Me),J(ze,Je),Qe,pe]:[]]:[],Ne=b("button",{className:"btn btn-primary",textContent:"Apply",title:"Apply to the item (Ctrl+Enter) - Save the menu to keep it"}),U=b("div",{className:"me-item-form"},u("Link"),q,Q,N,u("Access"),J(le,se),u("Badge"),J(ee,te),ve,u("Appearance"),D,h,j,u("Highlighting"),J(Pe,G),T,...He,u("Styling"),B,Ne),_=E.slideover({title:L?`Edit ${Ye(L)}`:"Edit menu item",size:"lg",position:"right"});_.element.appendChild(U);const Re=()=>JSON.stringify([...U.querySelectorAll("input, select, textarea")].map(m=>m.type==="checkbox"?m.checked:m.value)),Ze=Re();let xe=!1,Se=!1;Pt(_);const Be=_.close.bind(_);_.close=async m=>{if(!xe&&m!==!0&&Re()!==Ze){if(Se)return;Se=!0;const F=await E.confirm("Discard the changes to this item?");if(Se=!1,!F)return}return xe=!0,Be()},U.addEventListener("keydown",m=>{(m.ctrlKey||m.metaKey)&&m.key==="Enter"&&(m.preventDefault(),Ne.click())}),_.open();const Ae=_.element.closest(".dm-slideover")||_.element.querySelector(".dm-slideover")||_.element;Ae.style.setProperty("width",Xt+"px","important"),Ae.style.setProperty("max-width","95vw","important"),Domma.icons.scan(U);const f=m=>U.querySelector(`[name="${m}"]`)?.value||"";Ne.addEventListener("click",()=>{const m=ht(f("class"));if(m.length){V.textContent=m.join("; "),V.style.display="block",ye.focus();return}n.class=St(f("class")),n.text=f("text"),n.icon=f("icon"),n.locked||(n.visibility=f("visibility")),!n.tool&&!n.locked?(n.url=f("url"),n.permission=f("permission")):n.touched=!0;const F=f("badgeText"),he=f("badgeCount"),Ee=te._read();if(n.badge=F||he?{...F&&{text:F},...he&&{countFrom:he},...Ee&&{variant:Ee}}:null,f("renderAs")==="pill"){const oe=Ce._read();n.pill={style:f("pillStyle")||"filled",...oe&&{variant:oe}}}else n.pill=null;n.colour=j._read();const fe=f("hlHover"),$=f("hlActive"),ne=T._read();if(n.highlight=fe||$||ne?{...fe&&{hover:fe},...$&&{active:$},...ne&&{accent:ne}}:null,k&&(n.description=f("description").trim(),n.tile=Te._read(),n.chip=!ue&&_e.checked,ue)){const oe=f("featureTone");n.panel=$t({heading:f("panelHeading"),link:{text:f("panelLinkText"),url:f("panelLinkUrl")},columns:f("panelColumns"),chipsLabel:f("panelChipsLabel"),feature:oe?{tone:oe,eyebrow:f("featureEyebrow"),title:f("featureTitle"),points:f("featurePoints").split(`
29
+ `),button:{text:f("featureBtnText"),url:f("featureBtnUrl")}}:null})}xe=!0,_.close(!0),l()})}let K=null,bt=!1;function an(){bt||!window.R?.use||(bt=!0,R.use((n,d,i)=>{const r=d?.path||"";return!(Qt.test(r)&&(n?.path||"")!==r)||!K?.dirty()?i():E.confirm(`You have unsaved changes to the menu "${Ye(K.name())}". Leave anyway? They are kept as a draft for your next visit.`).then(k=>{k?i():history.replaceState(null,"",`#${r}`)})}),M.subscribe("router:afterChange",({to:n}={})=>{if(K&&(n?.path||"")!==K.path){const d=K;K=null,d.dispose()}}))}function yt(n,d){const i=n.querySelector(".dm-plugin-banner-name"),r=i?.querySelector(".dm-plugin-banner-sep");if(!(!i||!r)){for(;r.nextSibling;)r.nextSibling.remove();i.appendChild(document.createTextNode(d))}}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(n){if(an(),K){const e=K;K=null,e.dispose()}const d=[],i=e=>(d.push(e),e);let r=!1;const l=n.get(0).querySelector(".mex")||n.get(0),k=()=>!r&&l.isConnected,L=location.hash.replace(/^#/,""),u=location.hash.match(/^#\/menus\/edit\/([^?#]+)/),q=u?decodeURIComponent(u[1]):null,C=!q,Q=`menu-editor.draft.${q||"new"}`,N=E.loader(n.get(0),{type:"dots"});let v,le,Z,se,ee,ie,ve;try{if(C)v={slug:"",name:"",description:"",items:[]};else try{v=await P.menus.get(q)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}[le,Z,se,ee,ie,ve]=await Promise.all([P.projects.list().catch(()=>[]),H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),P.collections.list().catch(()=>[]),P.get("/collections/roles/entries?limit=100").catch(()=>null),P.menuLocations.registry().catch(()=>[]),P.pages.list().catch(()=>[])])}finally{N.destroy()}if(!l.isConnected)return;const be=(Array.isArray(ee)?ee:ee?.entries||[]).map(e=>({name:e.data?.name,label:e.data?.label,level:e.data?.level??99})).filter(e=>e.name).sort((e,o)=>o.level-e.level),D=!C&&q===Jt;let $e=[];if(D&&($e=await H.get("/api/sidebar/registered-items").catch(()=>(E.toast("Could not load the Tools - they are not shown, and saving keeps their places as they are.",{type:"warning",duration:8e3}),[])),!l.isConnected))return;const Ce=e=>ft(D?_t(Tt(e||[],$e,{forEditor:!0}),{leaveInHiddenFolders:!0}):e);let h=Ce(v.items);const j=M.observable(!1),re=M.observable(!1),Pe=M.observable(""),Le=M.observable(0);let G="";const T=l.querySelector("#me-items-list");function ye(){en(T,h,D),Le.value=h.filter(e=>!ge(e)).length}const B=()=>{ye(),Y()},V=()=>D?vt(h):new Set,ce=e=>h.find(o=>o.id===e);function de(e){requestAnimationFrame(()=>T.querySelector(`.menu-tree-row[data-id="${CSS.escape(e)}"]`)?.focus())}function Te(e){const o=g=>h.find(s=>!s.type&&!s.url&&!s.tool&&(s.key||"").toLowerCase()===g)||h.find(s=>!s.type&&!s.url&&!s.tool&&!s.key&&(s.text||"").toLowerCase()===g);let a=o(String(e.tool.folder||"").toLowerCase())||o(Bt);const p=String(e.tool.group||"").trim();if(a&&p){const g=Ht(a.key||Nt(a.text),p),s=c=>c!==e&&!c.type&&!c.url&&!c.tool;let w=h.find(c=>s(c)&&(c.key||"").toLowerCase()===g)||h.find(c=>s(c)&&c.parentId===a.id&&(!c.key||c.key.toLowerCase()===g)&&(c.text||"").toLowerCase()===p.toLowerCase());w||([w]=ft([{text:p,key:g,icon:Rt,synthesised:!0}],a.id),h.push(w)),a=w}e.parentId=a?a.id:null,h.splice(h.indexOf(e),1),h.push(e),Object.assign(e,{text:e.tool.defaults.text,icon:e.tool.defaults.icon,visibility:"",hidden:!1,badge:null,pill:null,colour:"",highlight:null,touched:!1}),e.tool={...e.tool,persisted:!1}}const _e=e=>!V().has(e.id)&&!(e.tool&&!e.tool.orphan);async function je(e){const o=ce(e);if(!o||!_e(o))return;const a=c=>{let X=c;for(;X;){if(X.id===e)return!0;X=h.find(z=>z.id===X.parentId)}return!1},p=h.filter(c=>c.id!==e&&a(c));if(p.length&&!await E.confirm(`Remove "${Ye(o.text||o.type||"this item")}" and the ${p.length} item${p.length===1?"":"s"} beneath it?`))return;const g=V(),s=p.filter(c=>c.tool),w=p.filter(c=>!c.tool&&g.has(c.id));h=h.filter(c=>!a(c)||s.includes(c)||w.includes(c));for(const c of w)c.parentId=o.parentId??null;for(const c of s)c.tool.orphan?c.parentId=o.parentId??null:Te(c);B()}function ue(e){const o=ce(e);!o||ge(o)||V().has(e)||(o.hidden=!o.hidden,o.touched=!0,B())}function x(e){const o=ce(e);!o||ge(o)||(o.locked=D&&V().has(o.id),on(o,Z.resources||[],se,be,()=>{B(),de(e)},{cards:!D}))}function O(e){const o={id:Ge(),parentId:null,...e==="separator"||e==="theme-switcher"?{type:e}:e==="spacer"?{type:e,size:"md"}:{text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}};h.push(o),B(),ge(o)?de(o.id):x(o.id)}const ke=e=>o=>{e(o)&&(B(),de(o))},Fe=ke(e=>gt(h,e,-1)),Me=ke(e=>gt(h,e,1)),Ie=ke(e=>Ut(h,e)),ze=ke(e=>Gt(h,e));function Je(e){const o=ce(e);if(!o)return[];const a=ge(o),p=V().has(e),g=Vt(h,e),s=g.findIndex(z=>z.id===e),w=o.tool,c=w&&(w.persisted||o.touched),X=z=>()=>{const Oe=Wt(h,e,z,Ge());B(),Oe&&!ge(Oe)?x(Oe.id):Oe&&de(Oe.id)};return[{header:a?{separator:"Separator",spacer:"Spacer","theme-switcher":"Theme switcher"}[o.type]:o.text||w?.label||"(untitled)"},...a?[]:[{label:"Edit\u2026",icon:"edit",action:()=>x(e)}],{type:"divider"},{label:"Add item below",icon:"plus",action:X("item")},{label:"Add sub-item",icon:"corner-down-right",disabled:a,action:()=>{const z=Ft(h,e,Ge());B(),z&&x(z.id)}},{label:"Add separator below",icon:"minus",action:X("separator")},{label:"Add spacer below",icon:"move",action:X("spacer")},...D?[]:[{label:"Add theme switcher below",icon:"palette",action:X("theme-switcher")}],{type:"divider"},{label:"Move up",icon:"arrow-up",shortcut:"Alt+\u2191",disabled:s<=0,action:()=>Fe(e)},{label:"Move down",icon:"arrow-down",shortcut:"Alt+\u2193",disabled:s<0||s>=g.length-1,action:()=>Me(e)},{label:"Indent",icon:"chevron-right",shortcut:"Alt+\u2192",disabled:s<=0||ge(g[s-1]),action:()=>Ie(e)},{label:"Outdent",icon:"chevron-left",shortcut:"Alt+\u2190",disabled:o.parentId==null,action:()=>ze(e)},{type:"divider"},...a?[]:[{label:o.hidden?"Show":"Hide",icon:o.hidden?"eye":"eye-off",disabled:p,action:()=>ue(e)}],{label:"Duplicate",icon:"copy",disabled:!!w,action:()=>{const z=zt(h,e,Ge);B(),z&&de(z.id)}},...w&&!w.orphan&&c?[{label:"Back to its default place",icon:"rotate-ccw",action:()=>{Te(o),B()}}]:[],{label:p?"Remove - always in the sidebar":w&&!w.orphan?"Remove - a Tool is hidden, not removed":"Remove\u2026",icon:"trash",danger:!0,disabled:!_e(o),action:()=>je(e)}]}const me=E.contextMenu(T,{cursor:"glyph",match:".menu-tree-row",inherit:!1,density:"compact",items:e=>Je(e.dataset.id)});i(()=>me?.destroy?.());const Qe=Lt(T,()=>h,B,{items:".menu-tree-row",handle:".menu-tree-grip"});i(()=>Qe?.destroy());const Ue=e=>{const o=e.target.closest(".menu-tree-row");if(!o)return;const a=o.dataset.id;if(e.target.closest(".mex-kebab")){at(e.target.closest(".mex-kebab"));return}if(e.target.closest(".me-hidden")){ue(a);return}if(e.target.closest(".me-edit")){x(a);return}e.target.closest("select, .menu-tree-grip, button")||x(a)},We=e=>{const o=e.target.closest(".menu-tree-row");if(!o||e.target!==o){o&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),at(o));return}const a=o.dataset.id;if(e.key==="F10"&&e.shiftKey)e.preventDefault(),at(o);else if(e.key==="Enter")e.preventDefault(),x(a);else if(e.key==="Delete")e.preventDefault(),je(a);else if(e.altKey&&e.key==="ArrowUp")e.preventDefault(),Fe(a);else if(e.altKey&&e.key==="ArrowDown")e.preventDefault(),Me(a);else if(e.altKey&&e.key==="ArrowRight")e.preventDefault(),Ie(a);else if(e.altKey&&e.key==="ArrowLeft")e.preventDefault(),ze(a);else if(e.key==="ArrowDown"||e.key==="ArrowUp"){const p=[...T.querySelectorAll(".menu-tree-row")],g=p.indexOf(o)+(e.key==="ArrowDown"?1:-1);p[g]&&(e.preventDefault(),p[g].focus())}},Ke=e=>{if(!e.target.matches(".me-spacer-size"))return;const o=ce(e.target.closest(".menu-tree-row")?.dataset.id);o&&(o.size=e.target.value,Y())};T.addEventListener("click",Ue),T.addEventListener("keydown",We),T.addEventListener("change",Ke),i(()=>{T.removeEventListener("click",Ue),T.removeEventListener("keydown",We),T.removeEventListener("change",Ke)});const t=e=>n.find(e),we=t("#me-float-accent"),pe=t("#me-float-accent-hex"),He=()=>{pe.get(0).style.display=we.val()==="__custom__"?"":"none"},Ne=()=>we.val()==="__custom__"?pe.val():we.val(),U=t("#me-highlight-accent"),_=t("#me-highlight-accent-hex"),Re=()=>{_.get(0).style.display=U.val()==="__custom__"?"":"none"},Ze=()=>U.val()==="__custom__"?_.val():U.val(),xe=()=>{const e=!!(t("#me-entrance-enabled").get(0)||{}).checked;t(".dm-entrance-field").each(function(){this.style.display=e?"":"none"});const o=t("#me-position").val()==="floating";t(".dm-float-field").each(function(){this.style.display=o?"":"none"}),o&&He();const a=t("#me-orientation").val()==="vertical";t(".dm-vertical-field").each(function(){this.style.display=a?"":"none"}),t(".dm-dock-rail-field").each(function(){this.style.display=a&&!o?"":"none"}),t(".dm-dock-bar-field").each(function(){this.style.display=!a&&!o?"":"none"});const p=!!(t("#me-dock-collapsible").get(0)||{}).checked,g=!!(t("#me-dock-autohide").get(0)||{}).checked;t(".dm-dock-collapsed-field").each(function(){this.style.display=p&&!g?"":"none"});const s=!!(t("#me-float-collapsible").get(0)||{}).checked;t(".dm-float-collapsed-field").each(function(){this.style.display=o&&s?"":"none"});const w=!a&&!D;t(".dm-look-field").each(function(){this.style.display=w?"":"none"});const c=w&&t("#me-look-dropdown").val()==="cards";t(".dm-look-cards-field").each(function(){this.style.display=c?"":"none"})},Se=(e,o,a,p)=>{const g=t(e).get(0);g&&(g.innerHTML="",g.add(new Option(p,"")),o.forEach(s=>g.add(new Option(a[s]||s,s))))};Se("#me-highlight-hover",dt,mt,"- theme default -"),Se("#me-highlight-active",ut,pt,"- theme default -");{const e=t("#me-entrance-animation").get(0);e&&(e.innerHTML="",nt.forEach(o=>e.add(new Option(At[o]||o,o))))}const Be=t("#me-bind-slot"),Ae=t("#me-bind-projects"),f=t("#me-bind-pages");Be.html('<option value="">- not bound -</option>'+(ie||[]).map(e=>`<option value="${A(e.slot)}">${A(e.label||e.slot)} (${A(e.slot)})</option>`).join("")),Ae.html(le.map(e=>`<option value="${A(e.slug)}">${A(e.name||e.slug)}</option>`).join("")),t("#me-bind-page-pick").html('<option value="">- pick a page -</option>'+ve.map(e=>{const o=e.urlPath||e.url||"";return o?`<option value="${A(o)}">${A(e.title||o)} - ${A(o)}</option>`:""}).join(""));const m=()=>Mt({slot:Be.val(),projects:Array.from(Ae.get(0).selectedOptions).map(e=>e.value),pagesText:f.val()});function F(){const e=m(),o=t("#me-bind-summary");if(!e){o.html('<span class="text-muted">Not bound - this menu renders only where it is mapped or placed by a shortcode.</span>');return}const a=[];e.projects.length&&a.push(`pages in ${e.projects.map(s=>`<code>${A(s)}</code>`).join(", ")}`),e.pages.length&&a.push(`pages matching ${e.pages.map(s=>`<code>${A(s)}</code>`).join(", ")}`);const p=e.pages.filter(s=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(s)),g=p.length?`<div class="text-danger" style="margin-top:.4rem">Not valid page patterns: ${p.map(s=>`<code>${A(s)}</code>`).join(", ")}</div>`:"";o.html(a.length?`Takes over the <code>${A(e.slot)}</code> slot on ${a.join(" and ")}.${g}`:`<span class="text-warning">Slot <code>${A(e.slot)}</code> selected but no project or page chosen - the binding will never match.</span>`)}const{getProjectFromHash:he}=await import("../lib/project-context.js");if(!k())return;const Ee=C?he():null;t("#me-project").html('<option value="">- none -</option>'+le.map(e=>`<option value="${A(e.slug)}">${A(e.name||e.slug)}</option>`).join(""));function fe(e){h=Ce(e.items),t("#me-variant").val(e.variant||""),t("#me-position").val(e.position||"");const o=t("#me-appearOnHover").get(0);o&&(o.checked=e.appearOnHover===!0),t("#me-float-anchor").val(e.float?.anchor||"TL"),t("#me-float-offsetX").val(e.float?.offsetX??""),t("#me-float-offsetY").val(e.float?.offsetY??""),t("#me-float-width").val(e.float?.width??""),t("#me-float-radius").val(e.float?.radius??""),t("#me-float-shadow").val(e.float?.shadow||""),t("#me-float-opacity").val(e.float?.opacity??""),t("#me-float-title").val(e.float?.title||"");for(const[s,w]of[["#me-float-collapsible",e.float?.collapsible],["#me-float-collapsed",e.float?.collapsed],["#me-float-movable",e.float?.movable],["#me-float-resizable",e.float?.resizable]]){const c=t(s).get(0);c&&(c.checked=w===!0)}const a=e.float?.accent||"";we.val(a.startsWith("#")?"__custom__":a),a.startsWith("#")&&pe.val(a),He(),t("#me-orientation").val(e.orientation||""),t("#me-align").val(e.align||""),t("#me-mobile-hamburger").get(0).checked=e.mobile?.hamburgerOnly===!0,t("#me-mobile-brand").get(0).checked=e.mobile?.hideBrand===!0,t("#me-look-bar").val(e.look?.bar||""),t("#me-look-dropdown").val(e.look?.dropdown||""),t("#me-look-tiles").val(e.look?.tiles||""),t("#me-look-morph").get(0).checked=e.look?.morph===!0,t("#me-side").val(e.side||"left"),t("#me-dock-width").val(e.dock?.width??"");for(const s of["collapsible","collapsed","autohide","hideOnScroll"]){const w=t(`#me-dock-${s}`).get(0);w&&(w.checked=e.dock?.[s]===!0)}t("#me-class").val(e.class||""),t("#me-highlight-hover").val(e.highlight?.hover||""),t("#me-highlight-active").val(e.highlight?.active||"");const p=e.highlight?.accent||"";U.val(p.startsWith("#")?"__custom__":p),p.startsWith("#")&&_.val(p),Re();{const s=t("#me-entrance-enabled").get(0);s&&(s.checked=e.entrance?.enabled===!0)}t("#me-entrance-animation").val(nt.includes(e.entrance?.animation)?e.entrance.animation:nt[0]),t("#me-entrance-stagger").val(e.entrance?.stagger??""),t("#me-entrance-duration").val(e.entrance?.duration??""),t("#me-fontFamily").val(e.style?.fontFamily||""),t("#me-fontSize").val(e.style?.fontSize||""),t("#me-fontWeight").val(e.style?.fontWeight||""),t("#me-letterSpacing").val(e.style?.letterSpacing||""),t("#me-iconSize").val(e.style?.iconSize||""),Be.val(e.binding?.slot||"");const g=new Set(e.binding?.projects||[]);Array.from(Ae.get(0).options).forEach(s=>{s.selected=g.has(s.value)}),f.val((e.binding?.pages||[]).join(`
30
+ `)),t("#me-slug").val(e.slug||""),t("#me-name").val(e.name||""),t("#me-description").val(e.description||""),t("#me-project").val(e.meta?.project||Ee||""),xe(),F(),ye()}const $=e=>!!(t(e).get(0)||{}).checked;function ne(){const e={variant:t("#me-variant").val(),position:t("#me-position").val(),appearOnHover:$("#me-appearOnHover"),floatAnchor:t("#me-float-anchor").val(),floatOffsetX:t("#me-float-offsetX").val(),floatOffsetY:t("#me-float-offsetY").val(),floatWidth:t("#me-float-width").val(),floatRadius:t("#me-float-radius").val(),floatShadow:t("#me-float-shadow").val(),floatAccent:Ne(),floatOpacity:t("#me-float-opacity").val(),floatTitle:t("#me-float-title").val(),floatCollapsible:$("#me-float-collapsible"),floatCollapsed:$("#me-float-collapsed"),floatMovable:$("#me-float-movable"),floatResizable:$("#me-float-resizable"),highlightHover:t("#me-highlight-hover").val(),highlightActive:t("#me-highlight-active").val(),highlightAccent:Ze(),class:t("#me-class").val(),entranceEnabled:$("#me-entrance-enabled"),entranceAnimation:t("#me-entrance-animation").val(),entranceStagger:t("#me-entrance-stagger").val(),entranceDuration:t("#me-entrance-duration").val(),orientation:t("#me-orientation").val(),align:t("#me-align").val(),side:t("#me-side").val(),fontFamily:t("#me-fontFamily").val(),fontSize:t("#me-fontSize").val(),fontWeight:t("#me-fontWeight").val(),letterSpacing:t("#me-letterSpacing").val(),iconSize:t("#me-iconSize").val(),dockWidth:t("#me-dock-width").val(),dockCollapsible:$("#me-dock-collapsible"),dockCollapsed:$("#me-dock-collapsed"),dockAutohide:$("#me-dock-autohide"),mobileHamburgerOnly:$("#me-mobile-hamburger"),mobileHideBrand:$("#me-mobile-brand"),lookBar:t("#me-look-bar").val(),lookDropdown:t("#me-look-dropdown").val(),lookTiles:t("#me-look-tiles").val(),lookMorph:$("#me-look-morph"),dockHideOnScroll:$("#me-dock-hideOnScroll")},o={slug:t("#me-slug").val(),name:t("#me-name").val(),description:t("#me-description").val(),project:t("#me-project").val()};return Kt(e,o,m(),Ot(h),v.meta||{})}const oe=e=>[...qt(e),...ht(e.class).map(o=>`CSS classes: ${o} (Behaviour tab).`)];let lt=!1,et=null;function Y(){if(!G)return;const e=ne();et=e,j.value=Ve(e)!==G,Pe.value=j.peek()||lt?oe(e).join(" "):""}const qe=e=>{e.target.closest("#me-items-list")||(e.target.matches("#me-position, #me-orientation, #me-look-dropdown, #me-float-collapsible, #me-dock-collapsible, #me-dock-autohide, #me-entrance-enabled")&&xe(),e.target.matches("#me-float-accent")&&He(),e.target.matches("#me-highlight-accent")&&Re(),e.target.closest("#me-binding")&&F(),Y())};l.addEventListener("input",qe),l.addEventListener("change",qe),i(()=>{l.removeEventListener("input",qe),l.removeEventListener("change",qe)}),t("#me-bind-page-add").on("click",()=>{const e=t("#me-bind-page-pick").val();if(!e)return;const o=f.val().split(`
31
+ `).map(a=>a.trim()).filter(Boolean);o.includes(e)||o.push(e),f.val(o.join(`
32
+ `)),F(),Y()}),fe(v),G=Ve(ne()),Y();const st=()=>{try{S.remove?S.remove(Q):S.set(Q,null)}catch{}};async function tt(){if(re.peek())return;const e=ne(),o=oe(e);if(o.length){lt=!0,Y(),E.toast(o.join(" "),{type:"warning",duration:7e3});return}re.value=!0;try{if(C)await P.menus.create(e);else if(e.slug!==v.slug){await P.menus.create(e);const a=await P.menuLocations.get();let p=!1;for(const[g,s]of Object.entries(a))s===v.slug&&(a[g]=e.slug,p=!0);p&&await P.menuLocations.save(a),await P.menus.remove(v.slug)}else await P.menus.update(v.slug,e);if(G=Ve(e),j.value=!1,st(),jt("#/menus"),E.toast("Saved.",{type:"success"}),C||e.slug!==v.slug)location.hash="#/menus/edit/"+encodeURIComponent(e.slug);else{v={...v,...e};for(const a of h)a.tool&&a.touched&&(a.tool={...a.tool,persisted:!0});ye(),G=Ve(ne()),Y(),yt(n.get(0),e.name||e.slug)}if(D)try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}catch(a){E.toast(`Save failed: ${a.message||a}`,{type:"error"})}finally{re.value=!1}}async function kt(){!j.peek()||!await E.confirm("Throw away the changes you have not saved?")||(fe(v),Y())}E.tabs(t("#me-tabs").get(0));const wt=M.applyBindings({dirty:j,saving:re,problems:Pe,itemCount:Le},l,{methods:{save:()=>tt(),discard:()=>kt(),addItem:()=>O("item"),addSeparator:()=>O("separator"),addSpacer:()=>O("spacer"),addSwitcher:()=>O("theme-switcher")}});i(()=>wt.dispose()),i(Dt(l,{position:"bottom"})),D&&t("#me-add-switcher").hide(),yt(n.get(0),C?"New menu":v.name||v.slug),ot(n,{id:"menu-editor-back",icon:"arrow-left",title:"All menus",onClick:()=>{location.hash="#/menus"}}),ot(n,{id:"menu-editor-locations",icon:"map-pin",title:"Menu locations",onClick:()=>{location.hash="#/menu-locations"}}),ot(n,{id:"menu-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>tt()});const it=e=>{if(!(!(e.ctrlKey||e.metaKey)||e.key!=="s")){if(e.preventDefault(),e.stopImmediatePropagation(),document.querySelector(".me-item-form")?.offsetParent){E.toast("Apply the item first (Ctrl+Enter), then Save.",{type:"info"});return}tt()}};document.addEventListener("keydown",it,!0),i(()=>document.removeEventListener("keydown",it,!0));const rt=()=>{if(!(!j.peek()||!et))try{S.set(Q,{menu:et,base:G,at:new Date().toISOString()})}catch{}},ct=e=>{j.peek()&&(rt(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",ct),i(()=>window.removeEventListener("beforeunload",ct)),K={path:L,dirty:()=>j.peek(),name:()=>t("#me-name").val()||v.name||v.slug||"new menu",dispose(){r=!0,rt(),d.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(l);let ae=null;try{ae=S.get(Q)}catch{}if(ae?.menu){st();const e=ae.base&&ae.base!==G,o=ae.at?` (${Yt(ae.at)})`:"",a=C?"the new menu you started":`the menu "${Ye(v.name||v.slug)}"`,p=e?`You left unsaved changes to ${a}${o}, but the saved menu has changed since. Restore your version anyway?`:`You left unsaved changes to ${a}${o}. Restore them?`;await E.confirm(p)&&k()&&(fe({...ae.menu,meta:{...v.meta||{},...ae.menu.meta||{}}}),Y())}}};
@@ -1,11 +1,11 @@
1
- import{api as B,apiRequest as x}from"../api.js";import{placeTools as Re}from"../lib/sidebar-grouping.js";import{addBannerAction as pe}from"../lib/plugin-chrome.js";import{helpPopovers as Ie}from"../lib/help-popover.js";import{openMenuAt as W,sidebarCount as me}from"../lib/tool-kit.js";import{changedTabs as Ae,describeCacheKey as Y,draftOf as $e,expiresIn as Pe,formFromSettings as je,gaIdError as fe,homeValue as Be,payloadFromForm as De,siteUrlError as he,settingsProblems as He,snapshot as ve,TAB_FIELDS as qe}from"../lib/site-settings-arrange.js";import{pickMedia as Fe}from"../lib/media-picker.js";import{faviconGenerator as Ue}from"../lib/favicon-generator.js";import{makeIconInput as _e}from"../lib/shortcode-modal.js";import{fmtDateTime as Ke}from"../lib/dates.js";const J=/^\/settings(\?.*)?$/,be="#/settings",w="settings.draft",Ne=`
1
+ import{api as B,apiRequest as C}from"../api.js";import{placeTools as Ie}from"../lib/sidebar-grouping.js";import{addBannerAction as me}from"../lib/plugin-chrome.js";import{helpPopovers as Ae}from"../lib/help-popover.js";import{openMenuAt as Y,sidebarCount as fe}from"../lib/tool-kit.js";import{changedTabs as Pe,describeCacheKey as J,draftOf as $e,expiresIn as je,footerShowcaseErrors as Be,formFromSettings as De,gaIdError as he,homeValue as He,payloadFromForm as qe,siteUrlError as ve,settingsProblems as Fe,snapshot as be,TAB_FIELDS as Ue}from"../lib/site-settings-arrange.js";import{pickMedia as _e}from"../lib/media-picker.js";import{faviconGenerator as Ke}from"../lib/favicon-generator.js";import{makeIconInput as Ne}from"../lib/shortcode-modal.js";import{fmtDateTime as ze}from"../lib/dates.js";const Q=/^\/settings(\?.*)?$/,ge="#/settings",w="settings.draft",Ge=`
2
2
  <div class="stx-problem">
3
3
  <span class="stx-chip" data-bind-class="tone === 'danger' ? 'is-bad' : 'is-warn'">{{label}}</span>
4
4
  <span>{{text}}</span>
5
5
  <button type="button" class="btn btn-sm btn-ghost" data-go="{{tab}}">Go there</button>
6
- </div>`,ze=`
6
+ </div>`,Ve=`
7
7
  <div class="stx-row" data-id="{{id}}" tabindex="0">
8
8
  <span class="stx-main"><strong>{{path}}</strong><small>{{meta}}</small></span>
9
9
  <span class="stx-chip">{{expires}}</span>
10
10
  <button type="button" class="stx-kebab" aria-label="Actions for {{path}}" title="Actions">\u22EE</button>
11
- </div>`,Ge=["general","branding","favicon","seo","footer","email","pageNavigation","cookies","analytics","breadcrumbs","cache"];async function Ve(l,u){const[r,k]=await Promise.all([H.get("/api/sidebar/menu").catch(()=>({items:[]})),H.get("/api/sidebar/registered-items").catch(()=>[])]),n=new Map([["/","Dashboard (built in)"]]),i=(v,d)=>{for(const o of v||[]){if(!o||o.hidden||o.restricted)continue;const m=typeof o.url=="string"?o.url:"";m.startsWith("#/")&&m.length>2&&!n.has(m.slice(1))&&n.set(m.slice(1),`${d?`${d} \u203A `:""}${o.text||m}`),i(o.items,o.url?d:o.text||d)}};i(Re(r?.items||[],k||[]),""),n.has(u)||n.set(u,`${u} (not in the sidebar)`),l.replaceChildren();for(const[v,d]of n){const o=document.createElement("option");o.value=v,o.textContent=d,l.appendChild(o)}}let f=null,ge=!1;function We(){ge||!window.R?.use||(ge=!0,R.use((l,u,r)=>!(J.test(u?.path||"")&&!J.test(l?.path||""))||!f?.dirty()?r():E.confirm("You have unsaved settings. Leave anyway? They are kept as a draft for your next visit (not the SMTP password).").then(n=>{n?r():history.replaceState(null,"","#/settings")})),M.subscribe("router:afterChange",({to:l}={})=>{if(f&&!J.test(l?.path||"")){const u=f;f=null,u.dispose()}}))}export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(l){if(We(),f){const e=f;f=null,e.dispose()}const u=[],r=e=>(u.push(e),e);let k=!1;const n=l.get(0).querySelector(".stx")||l.get(0),i=()=>!k&&n.isConnected,v=E.loader(l.get(0),{type:"dots"});let d;try{d=await B.settings.get()}catch(e){v.destroy(),E.toast(`Could not load the settings: ${e.message||e}`,{type:"error"});return}if(v.destroy(),!n.isConnected)return;const o=M.observable(d||{}),m=je(d||{}),s={};for(const[e,t]of Object.entries(m))s[e]=M.observable(t);const D=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].value])),Q=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].peek()])),b=M.observable(m),q=M.computed(()=>ve(b.value)),T=M.computed(()=>ve(D())!==q.value),L=M.observable(!1),O=M.observable(""),X=M.observable(!1);B.plugins.policy().then(e=>{i()&&(X.value=e?.signedOnly===!1)}).catch(()=>{});const F=M.observable(!1),Z=M.computed(()=>{const e=new Set(Ae(D(),b.value));return Object.fromEntries(Object.keys(qe).map(t=>[t,e.has(t)]))}),U=M.observable([]);r(M.effect(()=>{U.value=He(D()).map(e=>({...e,label:e.tone==="danger"?"Mail":"Check"}))}));const ee=M.observable(""),A=M.observable(!1),$=M.observable(""),P=M.observable(""),p=M.observable(null),_=M.observable([]),K=M.observable(""),N=M.observable(!0),z=M.observable(""),te=M.observable([]);r(M.effect(()=>{const e=K.value.trim().toLowerCase(),t=Date.now();te.value=[..._.value].reverse().filter(a=>!e||String(a.key).toLowerCase().includes(e)||(a.tags||[]).some(c=>String(c).toLowerCase().includes(e))).map(a=>{const c=Y(a.key);return{id:a.key,path:c.path,meta:[c.page?`as ${c.role}`:"not a page",(a.tags||[]).join(", ")].filter(Boolean).join(" \xB7 "),expires:Pe(a.expiresAt,t)}})}));const ae=Ue(s.favicon,s.brandLogo,i),ye={...ae.data,...s,saving:L,dirty:T,tabs:Z,problems:U,testTo:ee,testing:A,testResult:$,testTone:P,cacheQuery:K,cacheLoading:N,cacheError:z,cacheRows:te,errorText:O,problemsText:M.computed(()=>U.value.length?"yes":""),siteUrlError:M.computed(()=>he(s.siteUrl.value)),gaIdError:M.computed(()=>fe(s.gaId.value)),gaForcesBanner:M.computed(()=>!!String(s.gaId.value||"").trim()&&s.ccEnabled.value!==!0),noSmtp:M.computed(()=>!String(s.smtpHost.value||"").trim()),mayAllowInsecureTLS:X,dirtyEmail:M.computed(()=>Z.value.email),seoLength:M.computed(()=>{const e=String(s.seoDescription.value||"").trim().length;return e?`${e} characters${e>160?" - search engines usually cut it off after about 150":""}`:""}),cacheOn:M.computed(()=>p.value?.enabled===!0),cacheState:M.computed(()=>{const e=p.value;return e?e.enabled?`On (${e.driver})`:"Off":"Status unknown"}),cacheSize:M.computed(()=>{const e=p.value;return e?.enabled&&e.size!==null&&e.size!==void 0?`${e.size}${e.maxItems?` of ${e.maxItems}`:""} entries`:""}),cacheHits:M.computed(()=>{const e=p.value?.hitRate;return e==null?"":`${Math.round(e*100)}% served from the cache`}),cacheToggleText:M.computed(()=>p.value?.enabled?"Turn off":"Turn on"),cacheEmpty:M.computed(()=>K.value.trim()?"Nothing matches.":p.value?.enabled?"The cache is empty - pages are cached as visitors open them.":"The cache is off.")};async function G(){if(L.peek())return;const e=Q(),t=b.peek(),a=he(e.siteUrl);if(a){E.toast(`Site URL: ${a}`,{type:"error"});return}const c=fe(e.gaId);if(c){E.toast(`Google Analytics: ${c}`,{type:"error"});return}L.value=!0,O.value="";const C=String(e.title).trim()!==String(t.title).trim();try{await B.settings.save(De(e,o.peek(),{adminHomeReady:F.peek()}));const y=await B.settings.get().catch(()=>null);if(!i())return;y&&(o.value=y);const Oe={...e,smtpPass:""};s.smtpPass.value="",b.value=Oe;try{S.remove?S.remove(w):S.set(w,null)}catch{}document.dispatchEvent(new CustomEvent("dm:admin-home-changed")),(C||["adminBrandTitle","adminBrandLogo","adminBrandIcon"].some(de=>e[de]!==t[de]))&&document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild")),me(be),E.toast("Settings saved.",{type:"success"})}catch(y){O.value=y.message||"Could not save the settings.",E.toast("Settings not saved - see the message at the top.",{type:"error"})}finally{L.value=!1}}async function we(){await E.confirm("Throw away your unsaved settings?")&&(se(b.peek()),O.value="")}function se(e){for(const[t,a]of Object.entries(e))s[t]&&(s[t].value=a)}async function ke(){if(!A.peek()){A.value=!0,P.value="",$.value="Sending\u2026";try{const e=await x("/settings/test-email",{method:"POST",body:JSON.stringify({to:ee.peek().trim()||void 0})});if(!i())return;$.value=e?.message||"Test email sent.",P.value="is-ok"}catch(e){if(!i())return;$.value=e.message||"Could not send the test email.",P.value="is-bad"}finally{A.value=!1}}}async function j(){N.value=!0;try{const[e,t]=await Promise.all([x("/cache/status").catch(()=>null),x("/cache/keys")]);if(!i())return;p.value=e,_.value=Array.isArray(t?.entries)?t.entries:[],z.value=""}catch(e){if(!i())return;z.value=`Could not read the cache: ${e.message||e}`}finally{N.value=!1}}async function Ee(){if(await E.confirm("Clear the whole response cache? The next visitor to each page renders it afresh.")){try{await x("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}i()&&await j()}}async function Se(){const e=p.peek()?.enabled===!0;if(!(e&&!await E.confirm("Turn the response cache off? Every visit will render its page afresh until it is turned back on. The choice is saved (config/cache.json)."))){try{p.value=await x("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:!e})}),E.toast(e?"Cache turned off.":"Cache turned on.",{type:"success"}),me(be)}catch(t){E.toast(t.message||"Could not change the cache.",{type:"error"})}i()&&await j()}}const ne=e=>_.peek().find(t=>t.key===e);function Me(e){const t=ne(e);if(!t)return[];const a=Y(t.key);return[{header:a.path},{label:"Open the page",icon:"external-link",disabled:!a.page,action:()=>window.open(a.path,"_blank","noopener")},{label:"Copy key",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(t.key),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}},...a.page?[{label:"Copy address",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(new URL(a.path,location.origin).href),E.toast("Address copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}}]:[]]}n.querySelector(".stx-problems").innerHTML=Ne;const g=n.querySelector(".stx-list");g.innerHTML=ze;const Ce=E.tabs(n.querySelector(".stx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),xe=M.applyBindings(ye,n,{methods:{...ae.methods,save:G,discard:we,sendTest:ke,refreshCache:j,clearCache:Ee,toggleCache:Se,pickBrandImage:async e=>{const t=await Fe({title:"Choose a branding image",current:s[e].peek()});t&&i()&&(s[e].value=t)},clearBrandImage:e=>{s[e].value=""},corner:e=>{s.bcPosition.value=e}}});r(()=>xe.dispose());for(const e of n.querySelectorAll("[data-brand-icon]")){const t=e.dataset.brandIcon,a=_e("Choose an icon",s[t].peek());a.input.id=`stx-${t}`,a.input.classList.add("form-input");const c=()=>{s[t].value=a.input.value};a.input.addEventListener("input",c),r(()=>a.input.removeEventListener("input",c)),r(M.effect(()=>{a.input.value=s[t].value})),e.appendChild(a.el)}r(Ie(n,{position:"bottom"}));const Te=e=>{const t=e.target.closest("[data-go]");if(!t)return;const a=Ge.indexOf(t.dataset.go);a>=0&&Ce.activate(a)};n.querySelector(".stx-problems").addEventListener("click",Te);const V=n.querySelector("#stx-admin-home");Ve(V,Be(s.adminHome.peek())).then(()=>{i()&&(V.value=s.adminHome.peek(),F.value=!0)}).catch(()=>{});const Le=E.contextMenu(g,{cursor:"glyph",match:".stx-row",inherit:!1,density:"compact",items:e=>Me(e.dataset.id)});r(()=>Le?.destroy?.());const oe=e=>{const t=ne(e.dataset.id),a=t&&Y(t.key);a?.page?window.open(a.path,"_blank","noopener"):W(e)},re=e=>{const t=e.target.closest(".stx-row");if(!t)return;const a=e.target.closest(".stx-kebab");if(a){W(a);return}oe(t)},ie=e=>{const t=e.target.closest(".stx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),W(t)):e.key==="Enter"&&e.target===t&&oe(t))};g.addEventListener("click",re),g.addEventListener("keydown",ie),r(()=>{g.removeEventListener("click",re),g.removeEventListener("keydown",ie)}),pe(l,{id:"settings-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),pe(l,{id:"settings-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>G()});const ce=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),G())};document.addEventListener("keydown",ce,!0),r(()=>document.removeEventListener("keydown",ce,!0));const le=()=>{if(T.peek())try{S.set(w,{form:$e(Q()),base:q.peek(),at:new Date().toISOString()})}catch{}},ue=e=>{T.peek()&&(le(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",ue),r(()=>window.removeEventListener("beforeunload",ue)),f={dirty:()=>T.peek(),dispose(){k=!0,le(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(n),j();let h=null;try{h=S.get(w)}catch{}if(h?.form){try{S.remove?S.remove(w):S.set(w,null)}catch{}const e=h.base&&h.base!==q.peek(),t=h.at?` (${Ke(h.at)})`:"",a=e?`You left unsaved settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved settings${t}. Restore them?`;if(await E.confirm(a)&&i()){const c={...b.peek()};for(const[C,y]of Object.entries(h.form))C in c&&C!=="smtpPass"&&(c[C]=y);se(c),F.peek()&&(V.value=s.adminHome.peek())}}}};
11
+ </div>`,We=["general","branding","favicon","seo","footer","email","pageNavigation","cookies","analytics","breadcrumbs","cache"];async function Ye(l,u){const[r,k]=await Promise.all([H.get("/api/sidebar/menu").catch(()=>({items:[]})),H.get("/api/sidebar/registered-items").catch(()=>[])]),n=new Map([["/","Dashboard (built in)"]]),i=(v,d)=>{for(const o of v||[]){if(!o||o.hidden||o.restricted)continue;const m=typeof o.url=="string"?o.url:"";m.startsWith("#/")&&m.length>2&&!n.has(m.slice(1))&&n.set(m.slice(1),`${d?`${d} \u203A `:""}${o.text||m}`),i(o.items,o.url?d:o.text||d)}};i(Ie(r?.items||[],k||[]),""),n.has(u)||n.set(u,`${u} (not in the sidebar)`),l.replaceChildren();for(const[v,d]of n){const o=document.createElement("option");o.value=v,o.textContent=d,l.appendChild(o)}}let f=null,ye=!1;function Je(){ye||!window.R?.use||(ye=!0,R.use((l,u,r)=>!(Q.test(u?.path||"")&&!Q.test(l?.path||""))||!f?.dirty()?r():E.confirm("You have unsaved settings. Leave anyway? They are kept as a draft for your next visit (not the SMTP password).").then(n=>{n?r():history.replaceState(null,"","#/settings")})),M.subscribe("router:afterChange",({to:l}={})=>{if(f&&!Q.test(l?.path||"")){const u=f;f=null,u.dispose()}}))}export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(l){if(Je(),f){const e=f;f=null,e.dispose()}const u=[],r=e=>(u.push(e),e);let k=!1;const n=l.get(0).querySelector(".stx")||l.get(0),i=()=>!k&&n.isConnected,v=E.loader(l.get(0),{type:"dots"});let d;try{d=await B.settings.get()}catch(e){v.destroy(),E.toast(`Could not load the settings: ${e.message||e}`,{type:"error"});return}if(v.destroy(),!n.isConnected)return;const o=M.observable(d||{}),m=De(d||{}),s={};for(const[e,t]of Object.entries(m))s[e]=M.observable(t);const D=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].value])),X=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].peek()])),b=M.observable(m),q=M.computed(()=>be(b.value)),x=M.computed(()=>be(D())!==q.value),T=M.observable(!1),L=M.observable(""),Z=M.observable(!1);B.plugins.policy().then(e=>{i()&&(Z.value=e?.signedOnly===!1)}).catch(()=>{});const F=M.observable(!1),ee=M.computed(()=>{const e=new Set(Pe(D(),b.value));return Object.fromEntries(Object.keys(Ue).map(t=>[t,e.has(t)]))}),U=M.observable([]);r(M.effect(()=>{U.value=Fe(D()).map(e=>({...e,label:e.tone==="danger"?"Mail":"Check"}))}));const te=M.observable(""),O=M.observable(!1),A=M.observable(""),P=M.observable(""),p=M.observable(null),_=M.observable([]),K=M.observable(""),N=M.observable(!0),z=M.observable(""),ae=M.observable([]);r(M.effect(()=>{const e=K.value.trim().toLowerCase(),t=Date.now();ae.value=[..._.value].reverse().filter(a=>!e||String(a.key).toLowerCase().includes(e)||(a.tags||[]).some(c=>String(c).toLowerCase().includes(e))).map(a=>{const c=J(a.key);return{id:a.key,path:c.path,meta:[c.page?`as ${c.role}`:"not a page",(a.tags||[]).join(", ")].filter(Boolean).join(" \xB7 "),expires:je(a.expiresAt,t)}})}));const se=Ke(s.favicon,s.brandLogo,i),we={...se.data,...s,saving:T,dirty:x,tabs:ee,problems:U,testTo:te,testing:O,testResult:A,testTone:P,cacheQuery:K,cacheLoading:N,cacheError:z,cacheRows:ae,errorText:L,problemsText:M.computed(()=>U.value.length?"yes":""),siteUrlError:M.computed(()=>ve(s.siteUrl.value)),gaIdError:M.computed(()=>he(s.gaId.value)),gaForcesBanner:M.computed(()=>!!String(s.gaId.value||"").trim()&&s.ccEnabled.value!==!0),noSmtp:M.computed(()=>!String(s.smtpHost.value||"").trim()),mayAllowInsecureTLS:Z,dirtyEmail:M.computed(()=>ee.value.email),seoLength:M.computed(()=>{const e=String(s.seoDescription.value||"").trim().length;return e?`${e} characters${e>160?" - search engines usually cut it off after about 150":""}`:""}),cacheOn:M.computed(()=>p.value?.enabled===!0),cacheState:M.computed(()=>{const e=p.value;return e?e.enabled?`On (${e.driver})`:"Off":"Status unknown"}),cacheSize:M.computed(()=>{const e=p.value;return e?.enabled&&e.size!==null&&e.size!==void 0?`${e.size}${e.maxItems?` of ${e.maxItems}`:""} entries`:""}),cacheHits:M.computed(()=>{const e=p.value?.hitRate;return e==null?"":`${Math.round(e*100)}% served from the cache`}),cacheToggleText:M.computed(()=>p.value?.enabled?"Turn off":"Turn on"),cacheEmpty:M.computed(()=>K.value.trim()?"Nothing matches.":p.value?.enabled?"The cache is empty - pages are cached as visitors open them.":"The cache is off.")};async function G(){if(T.peek())return;const e=X(),t=b.peek(),a=ve(e.siteUrl);if(a){E.toast(`Site URL: ${a}`,{type:"error"});return}const c=he(e.gaId);if(c){E.toast(`Google Analytics: ${c}`,{type:"error"});return}const y=Be(e)[0];if(y){E.toast(y,{type:"error"});return}T.value=!0,L.value="";const W=String(e.title).trim()!==String(t.title).trim();try{await B.settings.save(qe(e,o.peek(),{adminHomeReady:F.peek()}));const j=await B.settings.get().catch(()=>null);if(!i())return;j&&(o.value=j);const Re={...e,smtpPass:""};s.smtpPass.value="",b.value=Re;try{S.remove?S.remove(w):S.set(w,null)}catch{}document.dispatchEvent(new CustomEvent("dm:admin-home-changed")),(W||["adminBrandTitle","adminBrandLogo","adminBrandIcon"].some(pe=>e[pe]!==t[pe]))&&document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild")),fe(ge),E.toast("Settings saved.",{type:"success"})}catch(j){L.value=j.message||"Could not save the settings.",E.toast("Settings not saved - see the message at the top.",{type:"error"})}finally{T.value=!1}}async function ke(){await E.confirm("Throw away your unsaved settings?")&&(ne(b.peek()),L.value="")}function ne(e){for(const[t,a]of Object.entries(e))s[t]&&(s[t].value=a)}async function Ee(){if(!O.peek()){O.value=!0,P.value="",A.value="Sending\u2026";try{const e=await C("/settings/test-email",{method:"POST",body:JSON.stringify({to:te.peek().trim()||void 0})});if(!i())return;A.value=e?.message||"Test email sent.",P.value="is-ok"}catch(e){if(!i())return;A.value=e.message||"Could not send the test email.",P.value="is-bad"}finally{O.value=!1}}}async function $(){N.value=!0;try{const[e,t]=await Promise.all([C("/cache/status").catch(()=>null),C("/cache/keys")]);if(!i())return;p.value=e,_.value=Array.isArray(t?.entries)?t.entries:[],z.value=""}catch(e){if(!i())return;z.value=`Could not read the cache: ${e.message||e}`}finally{N.value=!1}}async function Se(){if(await E.confirm("Clear the whole response cache? The next visitor to each page renders it afresh.")){try{await C("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}i()&&await $()}}async function Me(){const e=p.peek()?.enabled===!0;if(!(e&&!await E.confirm("Turn the response cache off? Every visit will render its page afresh until it is turned back on. The choice is saved (config/cache.json)."))){try{p.value=await C("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:!e})}),E.toast(e?"Cache turned off.":"Cache turned on.",{type:"success"}),fe(ge)}catch(t){E.toast(t.message||"Could not change the cache.",{type:"error"})}i()&&await $()}}const oe=e=>_.peek().find(t=>t.key===e);function Ce(e){const t=oe(e);if(!t)return[];const a=J(t.key);return[{header:a.path},{label:"Open the page",icon:"external-link",disabled:!a.page,action:()=>window.open(a.path,"_blank","noopener")},{label:"Copy key",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(t.key),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}},...a.page?[{label:"Copy address",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(new URL(a.path,location.origin).href),E.toast("Address copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}}]:[]]}n.querySelector(".stx-problems").innerHTML=Ge;const g=n.querySelector(".stx-list");g.innerHTML=Ve;const xe=E.tabs(n.querySelector(".stx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Te=M.applyBindings(we,n,{methods:{...se.methods,save:G,discard:ke,sendTest:Ee,refreshCache:$,clearCache:Se,toggleCache:Me,pickBrandImage:async e=>{const t=await _e({title:"Choose a branding image",current:s[e].peek()});t&&i()&&(s[e].value=t)},clearBrandImage:e=>{s[e].value=""},corner:e=>{s.bcPosition.value=e}}});r(()=>Te.dispose());for(const e of n.querySelectorAll("[data-brand-icon]")){const t=e.dataset.brandIcon,a=Ne("Choose an icon",s[t].peek());a.input.id=`stx-${t}`,a.input.classList.add("form-input");const c=()=>{s[t].value=a.input.value};a.input.addEventListener("input",c),r(()=>a.input.removeEventListener("input",c)),r(M.effect(()=>{a.input.value=s[t].value})),e.appendChild(a.el)}r(Ae(n,{position:"bottom"}));const Le=e=>{const t=e.target.closest("[data-go]");if(!t)return;const a=We.indexOf(t.dataset.go);a>=0&&xe.activate(a)};n.querySelector(".stx-problems").addEventListener("click",Le);const V=n.querySelector("#stx-admin-home");Ye(V,He(s.adminHome.peek())).then(()=>{i()&&(V.value=s.adminHome.peek(),F.value=!0)}).catch(()=>{});const Oe=E.contextMenu(g,{cursor:"glyph",match:".stx-row",inherit:!1,density:"compact",items:e=>Ce(e.dataset.id)});r(()=>Oe?.destroy?.());const re=e=>{const t=oe(e.dataset.id),a=t&&J(t.key);a?.page?window.open(a.path,"_blank","noopener"):Y(e)},ie=e=>{const t=e.target.closest(".stx-row");if(!t)return;const a=e.target.closest(".stx-kebab");if(a){Y(a);return}re(t)},ce=e=>{const t=e.target.closest(".stx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Y(t)):e.key==="Enter"&&e.target===t&&re(t))};g.addEventListener("click",ie),g.addEventListener("keydown",ce),r(()=>{g.removeEventListener("click",ie),g.removeEventListener("keydown",ce)}),me(l,{id:"settings-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),me(l,{id:"settings-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>G()});const le=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),G())};document.addEventListener("keydown",le,!0),r(()=>document.removeEventListener("keydown",le,!0));const ue=()=>{if(x.peek())try{S.set(w,{form:$e(X()),base:q.peek(),at:new Date().toISOString()})}catch{}},de=e=>{x.peek()&&(ue(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",de),r(()=>window.removeEventListener("beforeunload",de)),f={dirty:()=>x.peek(),dispose(){k=!0,ue(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(n),$();let h=null;try{h=S.get(w)}catch{}if(h?.form){try{S.remove?S.remove(w):S.set(w,null)}catch{}const e=h.base&&h.base!==q.peek(),t=h.at?` (${ze(h.at)})`:"",a=e?`You left unsaved settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved settings${t}. Restore them?`;if(await E.confirm(a)&&i()){const c={...b.peek()};for(const[y,W]of Object.entries(h.form))y in c&&y!=="smtpPass"&&(c[y]=W);ne(c),F.peek()&&(V.value=s.adminHome.peek())}}}};
@@ -353,8 +353,18 @@ headings (Layouts → Page contents settings, and `[contents]` per page).
353
353
  - **Bindings** - a menu can take over a slot only on chosen pages or projects.
354
354
  - **Overlay slot** - menus bound to the core `overlay` slot render as floating panels anywhere on the page; floating panels can be collapsible, movable, resizable, with radius, shadow, accent and opacity. Since 0.42.0.
355
355
  - **Presentation** - variant, position (sticky, fixed, static, floating), orientation (vertical navbar docks as a side rail), alignment (left, centre, right, spread), and highlight effects for hover (colour, underline, overline, background, uppercase, grow, breathe, glow) and current page (bold, underline, overline, background, darken, pill).
356
+ - **Pill bar and card dropdowns** - a menu's `look` block, for the horizontal public navbar: `bar: pill` gathers the items into one rounded capsule with a soft pill on hover and on the current page; `dropdown: cards` opens each dropdown as a large card with a coloured icon tile and a line of description per sub-item, in columns (a sub-item with its own sub-items becomes a headed column). `tiles` picks rainbow (default), accent, neutral or none; `morph` slides one card into the next. Per item: `description`, `tile` colour, `chip` (a small chip in the card's footer strip). A top-level item's `panel` adds a heading with a "See all" link, `columns`, a chip strip label and a side box (`feature`: dark/light/accent, eyebrow, title, up to six points, a button). The mobile drawer keeps tiles and descriptions as a list. Page override: `[navbar bar=… dropdown=… tiles=… morph=… /]`. Navbar only - the `[menu]` shortcode is unchanged. Vocabulary in `public/js/menu-look.mjs` (shared by server and browser), drawing in `menu-cards.mjs` + `menu-cards.css`.
357
+ - Where: **Content > Menus > Behaviour** (Bar shape, Dropdown style) and each item's **Card dropdown** section
356
358
  - **`[menu]` shortcode** - render a menu by `slug` or `location` inline, with `depth`, `orientation` and `float="no"` per placement; per-user filtering is server-side.
357
359
 
360
+ ### Moving text and swooshing cards
361
+
362
+ - **Gradient text with a shine** - `[text gradient="sunset" shine="hover|sweep"]` paints words in a gradient (named: brand, sunset, ocean, forest, fire, candy, or two hex colours); the shine runs a highlight through them, following the pointer or sweeping on its own. `[text]` tags nest.
363
+ - **Light up as you read** - `[text light-up]` dims a passage and lights it word by word as it scrolls up the window.
364
+ - **Rise word by word** - `[text words]` brings a line in word by word as it arrives.
365
+ - **Swooshing cards** - `[row reveal reveal-animation="swoosh|swoosh-sides" reveal-replay tilt]`: columns rise out of a blur (or in from the sides), can replay each time, and lean toward the pointer with a soft light.
366
+ - Where: any page; the shortcode forms in the editor offer every option. Reduced motion turns it all off. `public/js/page-motion.mjs`, `public/css/page-motion.css`, the row reveal in `site.js`.
367
+
358
368
  ### Context menus (right-click)
359
369
 
360
370
  - **Authored context menus** - attach a right-click menu to any region of the public site by CSS selector (plus optional match and exclude selectors), scoped site-wide or bound to pages and projects, with priority and a nesting rule when regions overlap (append, prepend, or only mine).
@@ -1089,6 +1099,7 @@ Where: `server/services/presetCollections.js` (`PRESETS`, `seedAll()`), `server/
1089
1099
  - General: title, tagline, Site URL, admin home, default spacer size.
1090
1100
  - SEO defaults: default title, separator, meta description.
1091
1101
  - Footer: copyright, a "Reduce motion" switch, social icons for X, Facebook, Instagram, LinkedIn, GitHub and YouTube.
1102
+ - Showcase footer (Footer tab): `footer.look: showcase` (glowing top line, more room), `footer.cta` (a big call to action, some words in a gradient, optional link and sub-line), `footer.watermark` (a huge faint name behind the footer that drifts on scroll), `footer.email` / `footer.address` under the brand, `footer.status` (green dot) and up to four `footer.facts`, `footer.tagline` in the bottom bar, `footer.gradient` `{from, to}`. Columns layout only; each part shows when it has words. Drawn by `public/js/footer-showcase.mjs` + `footer-showcase.css`.
1092
1103
  - Email: SMTP host, port, user, password and TLS; From address and name; send a test email.
1093
1104
  - Back to top: on/off, label, threshold, position, offsets, smooth scrolling.
1094
1105
  - Cookie consent: message, position, layout, colours, categories, policy links, button text, consent version.
@@ -281,6 +281,12 @@ Right column content.
281
281
  | `reveal-stagger` | number | Delay between each child in ms (default: `60`) |
282
282
  | `reveal-delay` | number | Initial delay before first animation in ms (default: `0`) |
283
283
  | `reveal-direction` | string | `"ltr"` (default) or `"rtl"` |
284
+ | `reveal-replay` | flag | Hide again when the row drops back below the window, so it plays every time it comes into view |
285
+ | `tilt` | flag | Each column lifts and leans toward the pointer (mouse only), flat again when it leaves |
286
+
287
+ `reveal-animation` also takes `swoosh` (the columns rise out of a soft blur with a little scale) and
288
+ `swoosh-sides` (the first column from the left, the last from the right, the middle ones up). Both
289
+ default to 700 ms with a 90 ms stagger and are skipped for visitors who ask for reduced motion.
284
290
 
285
291
  ### Row with staggered reveal
286
292
 
@@ -697,6 +703,15 @@ Wraps inline text in a `<span>` with custom styles. All attributes are optional.
697
703
  | `style` | Raw CSS declarations | Appended to the generated inline style |
698
704
  | `class` | CSS class name(s) | Added to the `<span>` element |
699
705
  | `id` | Element ID | Added to the `<span>` element |
706
+ | `gradient` | `brand` `sunset` `ocean` `forest` `fire` `candy`, or two hex colours `"#f26b3a,#8b5cf6"`; a bare flag is `brand` | The words are painted in the gradient (`.dm-tg`) |
707
+ | `shine` | `hover` `sweep` | With `gradient`: a highlight runs through the words - following the pointer, or sweeping across every few seconds |
708
+ | `light-up` | flag | The text starts dim and lights up word by word as it scrolls up the window |
709
+ | `words` | flag | The text rises into view word by word |
710
+
711
+ `[text]` tags nest, so a gradient phrase can sit inside a headline that rises word by word:
712
+ `## [text words]Nothing goes out [text gradient="sunset" shine="sweep"]without you.[/text][/text]`.
713
+ Motion is off for visitors who ask for reduced motion (the text shows whole and lit). Drawn by
714
+ `public/js/page-motion.mjs` and `public/css/page-motion.css`.
700
715
 
701
716
  ### Notes
702
717
 
@@ -758,8 +773,8 @@ nothing itself.
758
773
 
759
774
  Every Behaviour setting of a menu is an attribute: `position`, `orientation`, `side`, `align`,
760
775
  `variant`, `hover-open`, `hover`, `selected`, `accent`, `class`, `font`, `font-size`, `font-weight`,
761
- `letter-spacing`, `icon-size`, `width`, `collapsible`, `collapsed`, `autohide`, `hide-on-scroll`, and the
762
- floating panel's `float-anchor`, `float-x`, `float-y`, `float-width`, `float-radius`, `float-shadow`,
776
+ `letter-spacing`, `icon-size`, `width`, `collapsible`, `collapsed`, `autohide`, `hide-on-scroll`, the look's
777
+ `bar` (`pill`), `dropdown` (`cards`), `tiles` and `morph`, and the floating panel's `float-anchor`, `float-x`, `float-y`, `float-width`, `float-radius`, `float-shadow`,
763
778
  `float-accent`, `float-opacity`, `float-title`, `float-collapsible`, `float-collapsed`, `float-movable`,
764
779
  `float-resizable`. `true` / `false` switch a setting on or off (so a page can turn off what the menu
765
780
  turns on); an empty value clears it back to the default. The first `[navbar]` on a page counts. The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.10.38",
3
+ "version": "1.11.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",
@@ -50,6 +50,7 @@ const {fillMessageAction, loadMessageActions, messageFacts} = await import(versi
50
50
  const {isPlainKey, keyBelongsElsewhere, resolveKey, SHORTCUTS} = await import(versioned('keyboard.js'));
51
51
  const {menuPoint, quoteSelection, readBridgeMessage, selectionItems} = await import(versioned('body-menu.js'));
52
52
  const {describeWhen, quickTimes} = await import(versioned('scheduling.js'));
53
+ const {ruleFromMessage} = await import(versioned('rules.js'));
53
54
  // Optional for the same reason as oauth-panel.js below.
54
55
  const {securityNote} = await import(versioned('security-note.js')).catch(() => ({securityNote: () => null}));
55
56
  // Sign in with Microsoft / Google (Email Pro). Optional: an older copy of this
@@ -194,6 +195,8 @@ function formatSize(bytes) {
194
195
  */
195
196
  export function createReaderView({pluginName, assetVersion = ASSET_VERSION, settingsSections = [], features = {},
196
197
  extensions = {}}) {
198
+ // Filing rules come with an edition's Rules tab (Email Pro).
199
+ const hasRules = settingsSections.some(section => section?.id === 'rules');
197
200
  const BASE = `/api/plugins/${pluginName}`;
198
201
 
199
202
  // Deliberately NOT keyed by plugin. The two editions never run together - the
@@ -910,13 +913,16 @@ return {
910
913
  *
911
914
  * @returns {void}
912
915
  */
913
- function openSettings() {
916
+ function openSettings(target = null) {
914
917
  // One at a time: a second click, or a needs-password error while
915
918
  // it is open, must not stack a second panel on the first.
916
- if (!settingsOnce.claim()) return;
919
+ if (!settingsOnce.claim()) {
920
+ if (target) E.toast('Mail Settings is already open. Close it first.', {type: 'info'});
921
+ return;
922
+ }
917
923
  let panel = null;
918
924
  try {
919
- buildSettings((so) => { panel = so; });
925
+ buildSettings((so) => { panel = so; }, target);
920
926
  } catch (err) {
921
927
  // Failed before it opened: let go of everything it took.
922
928
  panel?.__mailAbandon?.();
@@ -926,7 +932,13 @@ return {
926
932
  }
927
933
 
928
934
  /** @returns {void} */
929
- function buildSettings(made = () => {}) {
935
+ /**
936
+ * @param {Function} [made] - handed the slideover once it exists
937
+ * @param {{section: string, request?: object}|null} [target] - open on
938
+ * this tab, and hand its section `ctx.request` (once) - e.g. the
939
+ * rule a message's "Create a rule…" starts
940
+ */
941
+ function buildSettings(made = () => {}, target = null) {
930
942
  // Help popovers per drawn section, let go when the panel closes.
931
943
  const helpDisposers = [];
932
944
  // What sections registered through ctx.guard / ctx.onClose.
@@ -1007,8 +1019,11 @@ return {
1007
1019
  *
1008
1020
  * @returns {object}
1009
1021
  */
1010
- function context() {
1022
+ function context(section = null) {
1011
1023
  return {
1024
+ // What opened this section, when something did (a menu
1025
+ // item asking for a new rule). Only for that section.
1026
+ request: target && section?.id === target.section ? (target.request ?? null) : null,
1012
1027
  account: mailboxes.find(b => b.id === currentAccountId) ?? mailboxes[0] ?? null,
1013
1028
  mailboxes,
1014
1029
  base: BASE,
@@ -1086,7 +1101,7 @@ return {
1086
1101
  panel.appendChild(head);
1087
1102
  }
1088
1103
  try {
1089
- await section.render(panel, context());
1104
+ await section.render(panel, context(section));
1090
1105
  } catch (err) {
1091
1106
  const message = document.createElement('p');
1092
1107
  message.className = 'text-sm text-danger';
@@ -1135,8 +1150,15 @@ return {
1135
1150
  host.appendChild(tabs);
1136
1151
  slideover.element.appendChild(host);
1137
1152
  E.tabs(tabs);
1138
- panels[0].drawn = true;
1139
- draw(panels[0].section, panels[0].panel);
1153
+ // The tab asked for, through its own trigger so the tabs
1154
+ // component switches as it would on a click; else the first.
1155
+ const first = panels.findIndex(p => p.section.id === target?.section);
1156
+ if (first > 0) {
1157
+ list.children[first].click();
1158
+ } else {
1159
+ panels[0].drawn = true;
1160
+ draw(panels[0].section, panels[0].panel);
1161
+ }
1140
1162
  slideover.open();
1141
1163
  return;
1142
1164
  }
@@ -3742,6 +3764,12 @@ return {
3742
3764
  {type: 'divider'}
3743
3765
  ] : []),
3744
3766
  ...toolActionItems(message),
3767
+ // An edition with filing rules: start one from this message.
3768
+ ...(hasRules ? [{
3769
+ label: 'Create a rule from this message\u2026',
3770
+ icon: 'filter',
3771
+ action: () => openSettings({section: 'rules', request: {newRule: ruleFromMessage(message)}})
3772
+ }] : []),
3745
3773
  {type: 'divider'},
3746
3774
  {
3747
3775
  label: 'Mark as spam',