domma-cms 1.16.0 → 1.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin-chrome.js";import{helpPopovers as qe}from"../lib/help-popover.js";import{onceClosed as it,openMenuAt as Ne}from"../lib/tool-kit.js";import{makeIconInput as rt}from"/admin/js/lib/shortcode-modal.js";import{makePageInput as ct}from"/admin/js/lib/page-picker.js";import{ACCENTS as lt,ACTION_ARG as B,ACTION_GROUPS as dt,EDITABLE_TYPES as ut,NEEDS_TARGET as mt,SELF_DISABLING as pt,checkMenu as vt,duplicateItem as ft,formFromMenu as Re,insertItem as ht,itemForm as bt,itemFromForm as Pe,itemRow as yt,moveItem as Fe,newItem as gt,previewItems as xt,previewable as Ke,recordFromForm as kt,removeItem as W,slugify as wt,snapshot as ue,splitList as Q,themeFromForm as Et}from"../lib/context-menu-editor-arrange.js";import{fmtDateTime as St}from"../lib/dates.js";import{rowPlaceKeeper as Mt}from"../lib/menu-drag.js";import{cssClassProblems as At}from"/public/js/css-classes.mjs";const Ct=/^\/context-menus\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,F=l=>`context-menu-editor.draft.${l||"new"}`,It=`
1
+ import{api as pe}from"../api.js";import{addBannerAction as Re}from"../lib/plugin-chrome.js";import{helpPopovers as me}from"../lib/help-popover.js";import{CONTEXT_ACTION_HELP as dt}from"../lib/context-menu-action-help.js";import{onceClosed as ut,openMenuAt as Pe}from"../lib/tool-kit.js";import{makeIconInput as pt}from"/admin/js/lib/shortcode-modal.js";import{makePageInput as mt}from"/admin/js/lib/page-picker.js";import{ACCENTS as ht,ACTION_ARG as _,ACTION_GROUPS as Fe,EDITABLE_TYPES as vt,NEEDS_TARGET as ft,SELF_DISABLING as bt,checkMenu as yt,duplicateItem as gt,formFromMenu as He,insertItem as xt,itemForm as kt,itemFromForm as Ke,itemRow as wt,moveItem as Be,newItem as Et,previewItems as St,previewable as _e,recordFromForm as Mt,removeItem as X,slugify as At,snapshot as he,splitList as Q,themeFromForm as Ct}from"../lib/context-menu-editor-arrange.js";import{fmtDateTime as Tt}from"../lib/dates.js";import{rowPlaceKeeper as It}from"../lib/menu-drag.js";import{cssClassProblems as Lt}from"/public/js/css-classes.mjs";const Dt=/^\/context-menus\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,F=d=>`context-menu-editor.draft.${d||"new"}`,Ot=`
2
2
  <div class="cmx-row" data-index="{{index}}" tabindex="0" data-bind-class="cls">
3
3
  <span class="cmx-grip" title="Drag to reorder" aria-hidden="true"></span>
4
4
  <span class="cmx-icon" aria-hidden="true" data-name="{{icon}}"></span>
@@ -10,43 +10,47 @@ import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin
10
10
  <button type="button" class="cmx-down" data-bind-disabled="last" aria-label="Move down" title="Move down (Alt+\u2193)">\u2193</button>
11
11
  </span>
12
12
  <button type="button" class="cmx-kebab" aria-label="Actions for {{text}}" title="Actions">\u22EE</button>
13
- </div>`,Tt='<span class="cmx-chip" data-bind-class="cls">{{text}}</span>',Lt="<small>{{text}}</small>";function $(l){return String(l??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function $t(l){const u=document.createElement("template");return u.innerHTML=l.trim(),u.content.firstElementChild}let X=null;function me(){if(X){try{X.destroy()}catch{}X=null}const l=document.getElementById("ctx-preview");if(l&&E.contextMenu&&E.contextMenu.registry)for(const u of E.contextMenu.registry(l))try{u.destroy()}catch{}}function pe(l,u){const c=l.querySelector(".dm-plugin-banner-name");if(!c)return;const T=c.querySelector(".dm-plugin-banner-sep");if(T){for(;T.nextSibling;)T.nextSibling.remove();c.appendChild(document.createTextNode(u))}}let w=null,He=!1;const Z=l=>String(l||"").split("?")[0].replace(/\/$/,"");function Dt(){He||!window.R?.use||(He=!0,R.use((l,u,c)=>!(Ct.test(u?.path||"")&&Z(l?.path)!==Z(u?.path))||!w?.dirty()?c():E.confirm("This context menu has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(A=>{A?c():history.replaceState(null,"",`#${u.path}`)})),M.subscribe("router:afterChange",({to:l}={})=>{if(w&&Z(l?.path)!==w.path){const u=w;w=null,u.dispose()}}))}export const contextMenuEditorView={templateUrl:"/admin/js/templates/context-menu-editor.html",async onMount(l){if(Dt(),w){const e=w;w=null,e.dispose()}me();const u=[],c=e=>(u.push(e),e);let T=!1;const A=l.get(0),m=A.querySelector(".cmx")||A,_=()=>!T&&m.isConnected,ve=location.hash.match(/^#\/context-menus\/edit\/([^?#]+)/),p=ve?decodeURIComponent(ve[1]):null,h=!p,Be=Z(location.hash.replace(/^#/,""));let x=null;if(!h){const e=E.loader(A,{type:"dots"});try{x=await de.contextMenus.get(p)}catch(n){e.destroy(),E.toast(`Could not load "${p}": ${n.message||n}`,{type:"error"});return}if(e.destroy(),!m.isConnected)return}const D=Re(x),r={};for(const[e,n]of Object.entries(D))r[e]=M.observable(n);const U=()=>Object.fromEntries(Object.keys(r).map(e=>[e,r[e].peek()])),fe=()=>Object.fromEntries(Object.keys(r).map(e=>[e,r[e].value])),s=M.observable(Array.isArray(x?.items)?x.items:[]),z=M.observable(ue(D,s.peek())),G=M.computed(()=>ue(fe(),s.value)!==z.value),V=M.observable(!1),Y=M.observable(!1),K=M.observable([]),O=M.computed(()=>vt(fe(),s.value,{isNew:h})),_e=M.computed(()=>{const e=O.value.errors;if(Y.value)return e;const n={};for(const t of Object.keys(e))t!=="items"&&String(r[t]?.value??"").trim()&&(n[t]=e[t]);return n}),he=M.computed(()=>{const e=Y.value?O.value.errors:{};return{binding:!!(e.name||e.slug||e.selector),items:!!e.items||O.value.itemErrors.some(Boolean),appearance:!!(e.opacity||e.accent)}});let ee=!h;c(M.effect(()=>{const e=r.name.value;ee||(r.slug.value=wt(e))}));const te=M.observable([]);c(M.effect(()=>{const e=s.value,n=O.value.itemErrors;te.value=e.map((t,i)=>({...yt(t,i,e.length,n[i]||""),key:`${i}:${JSON.stringify(t)}`}))}));const b=m.querySelector("#ctx-items");c(M.effect(()=>{te.value,requestAnimationFrame(()=>{b?.isConnected&&(b.querySelectorAll(".cmx-icon[data-name]").forEach(e=>{const n=e.dataset.name;if(!n||e.dataset.drawn===n)return;e.dataset.drawn=n;const t=document.createElement("span");t.setAttribute("data-icon",n),t.setAttribute("data-icon-size","14"),e.replaceChildren(t)}),I.scan(b))})}));const Ue=M.computed(()=>{const e=[];e.push(r.enabled.value?{text:"On",cls:"is-ok"}:{text:"Off",cls:"is-warn"});const n=Q(r.pages.value).length,t=Q(r.projects.value).length;e.push({text:n||t?`${n+t} scope rule${n+t===1?"":"s"}`:"Site-wide",cls:""});const i=s.value.filter(a=>!["divider","header"].includes(a?.type||"item")).length;return e.push({text:`${i} item${i===1?"":"s"} to click`,cls:i?"":"is-danger"}),r.selector.value.trim()&&e.push({text:r.selector.value.trim(),cls:""}),e});let ne=null;function ze(){if(!_())return;me();const e=s.peek();if(!Ke(e))return;const n=Et(U());X=E.contextMenu("#ctx-preview",{cursor:"glyph",items:xt(e),inherit:!1,accent:n.accent,transition:n.transition,density:n.density,radius:n.radius,shadow:n.shadow,opacity:n.opacity})}c(M.effect(()=>{s.value,r.accent.value,r.transition.value,r.density.value,r.radius.value,r.shadow.value,r.opacity.value,clearTimeout(ne),ne=setTimeout(ze,120)})),c(()=>{clearTimeout(ne),me()});const Ge=M.computed(()=>Ke(s.value)?"Right-click here to try the menu":"Add an item to try the menu here"),ae=e=>be(e),k=e=>{s.value=e};function j(e){requestAnimationFrame(()=>b?.querySelector(`.cmx-row[data-index="${e}"]`)?.focus())}function q(e,n){n<0||n>=s.peek().length||(k(Fe(s.peek(),e,n)),j(n))}function N(e,n=s.peek().length){const t=gt(e);k(ht(s.peek(),n,t)),e!=="divider"&&be(n,{isAdd:!0})}function Ve(e){const n=s.peek(),t=n[e];if(!t)return[];const i=t.type||"item";return[{header:i==="divider"?"Divider":t.text||"(no text)"},{label:i==="divider"?"Change\u2026":"Edit\u2026",icon:"edit",action:()=>ae(e)},{type:"divider"},{label:"Move up",icon:"arrow-up",shortcut:"Alt+\u2191",disabled:e===0,action:()=>q(e,e-1)},{label:"Move down",icon:"arrow-down",shortcut:"Alt+\u2193",disabled:e===n.length-1,action:()=>q(e,e+1)},{label:"Duplicate",icon:"copy",action:()=>{k(ft(s.peek(),e)),j(e+1)}},{label:"Add below",icon:"plus",submenu:[{label:"Item",action:()=>N("item",e+1)},{label:"Heading",action:()=>N("header",e+1)},{label:"Divider",action:()=>N("divider",e+1)}]},{type:"divider"},{label:"Remove",icon:"trash",danger:!0,action:()=>{k(W(s.peek(),e)),j(Math.max(0,e-1))}}]}let v=null;function be(e,{isAdd:n=!1}={}){const t=s.peek()[e];if(!t)return;if(v)try{v.so.close(!0)}catch{}const i=bt(t),a={};for(const[o,d]of Object.entries(i))a[o]=M.observable(d);const J=()=>Object.fromEntries(Object.keys(a).map(o=>[o,a[o].peek()])),Ze=JSON.stringify(i),se=()=>JSON.stringify(J())!==Ze,Ae=!ut.includes(i.type),ie=Array.isArray(t.submenu)&&t.submenu.length,Ce={...a,locked:Ae,notLocked:!Ae,hasSub:ie,subText:ie?`Opens a submenu of ${t.submenu.length} - kept as it is (edit it in the file).`:"",isItem:M.computed(()=>a.type.value!=="divider"&&a.type.value!=="header"),hasText:M.computed(()=>a.type.value!=="divider"),showAction:M.computed(()=>a.type.value!=="divider"&&a.type.value!=="header"&&!ie),argLabel:M.computed(()=>B[a.action.value]?.label||""),note:M.computed(()=>pt[a.action.value]||mt[a.action.value]||""),problem:M.computed(()=>{const o=Pe(Object.fromEntries(Object.keys(a).map(L=>[L,a[L].value])),t),d=At(o.class)[0];return o.type==="divider"?"":String(o.text||"").trim()?d?`CSS classes: ${d}.`:B[o.action?.type]?.required&&!String(o.action?.[B[o.action.type].key]||"").trim()?`Needs a ${B[o.action.type].label.toLowerCase()}.`:"":"Needs text."})},g=E.slideover({title:n?"New item":i.type==="divider"?"Divider":`Edit ${$(i.text||"item")}`,size:"sm",position:"right"}),et=dt.map(o=>`<optgroup label="${$(o.label)}">${Object.entries(o.actions).map(([d,L])=>`<option value="${$(d)}">${$(L)}</option>`).join("")}</optgroup>`).join(""),C=$t(`
13
+ </div>`,$t='<span class="cmx-chip" data-bind-class="cls">{{text}}</span>',qt="<small>{{text}}</small>";function D(d){return String(d??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function jt(d){const u=document.createElement("template");return u.innerHTML=d.trim(),u.content.firstElementChild}let Z=null;function ve(){if(Z){try{Z.destroy()}catch{}Z=null}const d=document.getElementById("ctx-preview");if(d&&E.contextMenu&&E.contextMenu.registry)for(const u of E.contextMenu.registry(d))try{u.destroy()}catch{}}function fe(d,u){const l=d.querySelector(".dm-plugin-banner-name");if(!l)return;const T=l.querySelector(".dm-plugin-banner-sep");if(T){for(;T.nextSibling;)T.nextSibling.remove();l.appendChild(document.createTextNode(u))}}let A=null,Ue=!1;const ee=d=>String(d||"").split("?")[0].replace(/\/$/,"");function Nt(){Ue||!window.R?.use||(Ue=!0,R.use((d,u,l)=>!(Dt.test(u?.path||"")&&ee(d?.path)!==ee(u?.path))||!A?.dirty()?l():E.confirm("This context menu has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(C=>{C?l():history.replaceState(null,"",`#${u.path}`)})),M.subscribe("router:afterChange",({to:d}={})=>{if(A&&ee(d?.path)!==A.path){const u=A;A=null,u.dispose()}}))}export const contextMenuEditorView={templateUrl:"/admin/js/templates/context-menu-editor.html",async onMount(d){if(Nt(),A){const e=A;A=null,e.dispose()}ve();const u=[],l=e=>(u.push(e),e);let T=!1;const C=d.get(0),p=C.querySelector(".cmx")||C,U=()=>!T&&p.isConnected,be=location.hash.match(/^#\/context-menus\/edit\/([^?#]+)/),m=be?decodeURIComponent(be[1]):null,f=!m,ze=ee(location.hash.replace(/^#/,""));let x=null;if(!f){const e=E.loader(C,{type:"dots"});try{x=await pe.contextMenus.get(m)}catch(n){e.destroy(),E.toast(`Could not load "${m}": ${n.message||n}`,{type:"error"});return}if(e.destroy(),!p.isConnected)return}const O=He(x),r={};for(const[e,n]of Object.entries(O))r[e]=M.observable(n);const z=()=>Object.fromEntries(Object.keys(r).map(e=>[e,r[e].peek()])),ye=()=>Object.fromEntries(Object.keys(r).map(e=>[e,r[e].value])),s=M.observable(Array.isArray(x?.items)?x.items:[]),G=M.observable(he(O,s.peek())),V=M.computed(()=>he(ye(),s.value)!==G.value),J=M.observable(!1),Y=M.observable(!1),H=M.observable([]),$=M.computed(()=>yt(ye(),s.value,{isNew:f})),Ge=M.computed(()=>{const e=$.value.errors;if(Y.value)return e;const n={};for(const t of Object.keys(e))t!=="items"&&String(r[t]?.value??"").trim()&&(n[t]=e[t]);return n}),ge=M.computed(()=>{const e=Y.value?$.value.errors:{};return{binding:!!(e.name||e.slug||e.selector),items:!!e.items||$.value.itemErrors.some(Boolean),appearance:!!(e.opacity||e.accent)}});let te=!f;l(M.effect(()=>{const e=r.name.value;te||(r.slug.value=At(e))}));const ne=M.observable([]);l(M.effect(()=>{const e=s.value,n=$.value.itemErrors;ne.value=e.map((t,i)=>({...wt(t,i,e.length,n[i]||""),key:`${i}:${JSON.stringify(t)}`}))}));const b=p.querySelector("#ctx-items");l(M.effect(()=>{ne.value,requestAnimationFrame(()=>{b?.isConnected&&(b.querySelectorAll(".cmx-icon[data-name]").forEach(e=>{const n=e.dataset.name;if(!n||e.dataset.drawn===n)return;e.dataset.drawn=n;const t=document.createElement("span");t.setAttribute("data-icon",n),t.setAttribute("data-icon-size","14"),e.replaceChildren(t)}),I.scan(b))})}));const Ve=M.computed(()=>{const e=[];e.push(r.enabled.value?{text:"On",cls:"is-ok"}:{text:"Off",cls:"is-warn"});const n=Q(r.pages.value).length,t=Q(r.projects.value).length;e.push({text:n||t?`${n+t} scope rule${n+t===1?"":"s"}`:"Site-wide",cls:""});const i=s.value.filter(o=>!["divider","header"].includes(o?.type||"item")).length;return e.push({text:`${i} item${i===1?"":"s"} to click`,cls:i?"":"is-danger"}),r.selector.value.trim()&&e.push({text:r.selector.value.trim(),cls:""}),e});let ae=null;function Je(){if(!U())return;ve();const e=s.peek();if(!_e(e))return;const n=Ct(z());Z=E.contextMenu("#ctx-preview",{cursor:"glyph",items:St(e),inherit:!1,accent:n.accent,transition:n.transition,density:n.density,radius:n.radius,shadow:n.shadow,opacity:n.opacity})}l(M.effect(()=>{s.value,r.accent.value,r.transition.value,r.density.value,r.radius.value,r.shadow.value,r.opacity.value,clearTimeout(ae),ae=setTimeout(Je,120)})),l(()=>{clearTimeout(ae),ve()});const Ye=M.computed(()=>_e(s.value)?"Right-click here to try the menu":"Add an item to try the menu here"),oe=e=>xe(e),k=e=>{s.value=e};function q(e){requestAnimationFrame(()=>b?.querySelector(`.cmx-row[data-index="${e}"]`)?.focus())}function j(e,n){n<0||n>=s.peek().length||(k(Be(s.peek(),e,n)),q(n))}function N(e,n=s.peek().length){const t=Et(e);k(xt(s.peek(),n,t)),e!=="divider"&&xe(n,{isAdd:!0})}function We(e){const n=s.peek(),t=n[e];if(!t)return[];const i=t.type||"item";return[{header:i==="divider"?"Divider":t.text||"(no text)"},{label:i==="divider"?"Change\u2026":"Edit\u2026",icon:"edit",action:()=>oe(e)},{type:"divider"},{label:"Move up",icon:"arrow-up",shortcut:"Alt+\u2191",disabled:e===0,action:()=>j(e,e-1)},{label:"Move down",icon:"arrow-down",shortcut:"Alt+\u2193",disabled:e===n.length-1,action:()=>j(e,e+1)},{label:"Duplicate",icon:"copy",action:()=>{k(gt(s.peek(),e)),q(e+1)}},{label:"Add below",icon:"plus",submenu:[{label:"Item",action:()=>N("item",e+1)},{label:"Heading",action:()=>N("header",e+1)},{label:"Divider",action:()=>N("divider",e+1)}]},{type:"divider"},{label:"Remove",icon:"trash",danger:!0,action:()=>{k(X(s.peek(),e)),q(Math.max(0,e-1))}}]}let h=null;function xe(e,{isAdd:n=!1}={}){const t=s.peek()[e];if(!t)return;if(h)try{h.so.close(!0)}catch{}const i=kt(t),o={};for(const[a,c]of Object.entries(i))o[a]=M.observable(c);const W=()=>Object.fromEntries(Object.keys(o).map(a=>[a,o[a].peek()])),nt=JSON.stringify(i),ie=()=>JSON.stringify(W())!==nt,Ie=!vt.includes(i.type),re=Array.isArray(t.submenu)&&t.submenu.length,Le={...o,locked:Ie,notLocked:!Ie,hasSub:re,subText:re?`Opens a submenu of ${t.submenu.length} - kept as it is (edit it in the file).`:"",isItem:M.computed(()=>o.type.value!=="divider"&&o.type.value!=="header"),hasText:M.computed(()=>o.type.value!=="divider"),showAction:M.computed(()=>o.type.value!=="divider"&&o.type.value!=="header"&&!re),argLabel:M.computed(()=>_[o.action.value]?.label||""),note:M.computed(()=>bt[o.action.value]||ft[o.action.value]||""),problem:M.computed(()=>{const a=Ke(Object.fromEntries(Object.keys(o).map(L=>[L,o[L].value])),t),c=Lt(a.class)[0];return a.type==="divider"?"":String(a.text||"").trim()?c?`CSS classes: ${c}.`:_[a.action?.type]?.required&&!String(a.action?.[_[a.action.type].key]||"").trim()?`Needs a ${_[a.action.type].label.toLowerCase()}.`:"":"Needs text."})},g=E.slideover({title:n?"New item":i.type==="divider"?"Divider":`Edit ${D(i.text||"item")}`,size:"sm",position:"right"}),at=Fe.map(a=>`<optgroup label="${D(a.label)}">${Object.entries(a.actions).map(([c,L])=>`<option value="${D(c)}">${D(L)}</option>`).join("")}</optgroup>`).join(""),w=jt(`
14
14
  <div class="cmx-form">
15
15
  <div data-bind-hidden="locked">
16
16
  <label class="form-label" for="cmx-i-type">Type
17
- <span data-help="An Item does something when clicked, a Heading labels a group, a Divider draws a line."></span></label>
17
+ <span data-help-title="Type" data-help="Item creates a clickable action with text and an optional icon. Heading labels a group without being clickable. Divider draws a separating line and has no text or action. Choose a type appropriate to the row purpose. Every saved menu needs at least one actionable item; a menu made only of headings and dividers is refused."></span></label>
18
18
  <select id="cmx-i-type" class="form-select" data-model="type.value">
19
19
  <option value="item">Item</option><option value="header">Heading</option><option value="divider">Divider</option>
20
20
  </select>
21
21
  </div>
22
- <p class="cmx-note" data-bind-hidden="notLocked">A ${$(i.type)} item - its text, icon and action can be changed here; its type is kept.</p>
22
+ <p class="cmx-note" data-bind-hidden="notLocked">A ${D(i.type)} item - its text, icon and action can be changed here; its type is kept.</p>
23
23
  <div data-bind-hidden="!hasText.value">
24
- <label class="form-label" for="cmx-i-text">Text</label>
24
+ <label class="form-label" for="cmx-i-text">Text
25
+ <span data-help-title="Text" data-help="Set the words visitors see for this item or heading, up to 80 characters. Use a short action phrase such as Copy link or Open contact form for clickable items. This label does not choose the behaviour: set Does separately. Dividers have no text. Apply with Done, then Save the menu to keep it."></span></label>
25
26
  <input id="cmx-i-text" type="text" class="form-input" maxlength="80" data-model="text.value">
26
27
  </div>
27
28
  <div data-bind-hidden="!isItem.value">
28
- <label class="form-label" for="cmx-i-icon">Icon <small class="cmx-hint-inline">(optional)</small></label>
29
+ <label class="form-label" for="cmx-i-icon">Icon <small class="cmx-hint-inline">(optional)</small>
30
+ <span data-help-title="Icon (optional)" data-help="Choose an optional icon shown beside the item text. Use the icon picker to find a recognisable symbol that supports the label, such as a printer for Print. Leaving it empty keeps a text-only item. Choosing an icon does not change the action, and headings and dividers do not need one."></span></label>
29
31
  <div class="cmx-icon-mount"></div>
30
32
  </div>
31
- <p class="cmx-note" data-bind-hidden="!hasSub">${$(Ce.subText)}</p>
33
+ <p class="cmx-note" data-bind-hidden="!hasSub">${D(Le.subText)}</p>
32
34
  <div data-bind-hidden="!showAction.value">
33
35
  <label class="form-label" for="cmx-i-action">Does
34
- <span data-help-title="What it does" data-help="A fixed set of actions, on purpose: nothing typed here runs as script on your pages. Some grey themselves out when they do not apply to the click - the note under the choice says when."></span></label>
35
- <select id="cmx-i-action" class="form-select" data-model="action.value">${et}</select>
36
+ <span data-help-title="Does" data-help="Choose the action performed when this item is clicked. Page actions navigate, reload, print or open site search; clipboard actions work on the clicked selection or editable field; display actions target a collection display; site actions open a block or run a saved Action. Extra fields appear when needed. Some actions are unavailable when the click target cannot support them."></span></label>
37
+ <select id="cmx-i-action" class="form-select" data-model="action.value">${at}</select>
38
+ <small class="cmx-hint">About this action <span class="cmx-action-help"></span></small>
36
39
  <small class="cmx-hint" data-bind-hidden="!note.value" data-bind-text="note.value"></small>
37
40
  </div>
38
41
  <div data-bind-hidden="!argLabel.value || !showAction.value">
39
- <label class="form-label" for="cmx-i-arg" data-bind-text="argLabel.value">Argument</label>
42
+ <label class="form-label" for="cmx-i-arg"><span data-bind-text="argLabel.value">Argument</span>
43
+ <span data-help-title="Argument" data-help="Supply the value required by the chosen action. URL is a destination; Action slug names a saved Action; Block name names an existing content block. Field names a collection field for filtering or sorting, Format chooses the export format, and Flag field optionally overrides the spam marker. The label and input change with Does. Required values must be supplied before saving."></span></label>
40
44
  <div class="cmx-arg-mount"></div>
41
45
  </div>
42
46
  <div data-bind-hidden="!isItem.value">
43
47
  <label class="form-label" for="cmx-i-class">CSS classes <small class="cmx-hint-inline">(optional)</small>
44
- <span data-help="Your own class names, separated by spaces, added to this item - style them in Custom CSS."></span></label>
48
+ <span data-help-title="CSS classes (optional)" data-help="Add CSS class names, separated by spaces, to this item. Style them in Custom CSS to distinguish one action from the rest. Enter class names without leading dots; this field does not accept CSS declarations or scripts. Panel-wide styling belongs in the menu Appearance tab instead."></span></label>
45
49
  <input id="cmx-i-class" type="text" class="form-input" placeholder="menu-cta" autocomplete="off" spellcheck="false" data-model="className.value">
46
50
  </div>
47
51
  <div class="cmx-setting" data-bind-hidden="!isItem.value">
48
52
  <label class="form-label" for="cmx-i-danger">Destructive (red)
49
- <span data-help="Drawn in red, for things like Delete - a warning, not a guard."></span></label>
53
+ <span data-help-title="Destructive (red)" data-help="Draw this item in red to signal a destructive or sensitive action. This is a visual warning only: it does not add a confirmation, grant permissions or change what the selected action does. Use it for an action whose consequences warrant attention. Done applies the choice to the item; Save keeps the menu."></span></label>
50
54
  <input id="cmx-i-danger" type="checkbox" class="form-switch" data-model="danger.value">
51
55
  </div>
52
56
  <small class="cmx-err" data-bind-hidden="!problem.value" data-bind-text="problem.value"></small>
@@ -57,4 +61,4 @@ import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin
57
61
  <button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
58
62
  <button type="button" class="btn btn-sm btn-primary" data-on-click="done"><span data-icon="check" data-icon-size="13"></span> Done</button>
59
63
  </div>
60
- </div>`),P=rt("edit",i.icon);P.input.id="cmx-i-icon",P.input.classList.add("form-input");const Ie=()=>{a.icon.value=P.input.value};P.input.addEventListener("input",Ie),C.querySelector(".cmx-icon-mount").appendChild(P.el);const re=C.querySelector(".cmx-arg-mount");let f=null;const ce=()=>{a.arg.value=f.value};function Te(){f?.removeEventListener("input",ce),re.replaceChildren();const o=B[a.action.peek()];if(!o){f=null;return}if(o.key==="url"){const d=ct(o.placeholder,a.arg.peek());f=d.input,re.appendChild(d.el)}else f=document.createElement("input"),f.type="text",f.className="form-input",f.placeholder=o.placeholder,f.value=a.arg.peek(),re.appendChild(f);f.id="cmx-i-arg",f.addEventListener("input",ce)}let Le=a.action.peek();const tt=M.effect(()=>{const o=a.action.value;o!==Le&&(Le=o,a.arg.value="",Te())});Te();const $e=()=>{const o=Pe(J(),t),d=[...s.peek()],L=d.indexOf(t);L!==-1&&(d[L]=o,k(d))};let H=!1,le=!1;const De={done:()=>{$e(),H=!0,g.close(),j(e)},cancel:()=>g.close(),remove:()=>{const o=s.peek().indexOf(t);o!==-1&&k(W(s.peek(),o)),H=!0,g.close()}};g.element.appendChild(C);const nt=qe(C),at=M.applyBindings(Ce,C,{methods:De}),ot=o=>{(o.ctrlKey||o.metaKey)&&o.key==="Enter"&&(o.preventDefault(),De.done())};C.addEventListener("keydown",ot);const Oe={so:g,dirty:se,apply:$e};v=Oe,it(g,()=>{tt(),at.dispose(),nt(),P.input.removeEventListener("input",Ie),f?.removeEventListener("input",ce),v===Oe&&(v=null)});const st=g.close.bind(g);g.close=async o=>{if(!H&&o!==!0&&se()){if(le)return;le=!0;const d=await E.confirm("Discard the changes to this item?");if(le=!1,!d)return}if(!H&&n&&o!==!0&&!se()){const d=s.peek().indexOf(t);d!==-1&&k(W(s.peek(),d))}return H=!0,st()},I.scan(C),g.open(),requestAnimationFrame(()=>C.querySelector(i.type==="divider"?"#cmx-i-type":"#cmx-i-text")?.focus())}c(()=>{if(v){const e=v;e.dirty()&&e.apply();try{e.so.close(!0)}catch{}}});async function oe(){if(V.peek())return;Y.value=!0,K.value=[];const e=O.peek();if(!e.ok){const t=Object.keys(e.errors).length;E.toast(`Not saved yet - ${t===1?"one thing needs":`${t} things need`} fixing (marked in red).`,{type:"warning"});const i=he.peek(),a=i.binding?0:i.items?1:i.appearance?2:-1;a>=0&&We?.activate?.(a);return}V.value=!0;const n=kt(U(),s.peek());try{if(h)await de.contextMenus.create(n);else{const t=await de.contextMenus.update(p,n);t?.items&&(s.value=t.items)}if(!_())return;z.value=ue(U(),s.peek());try{S.remove?S.remove(F(p)):S.set(F(p),null)}catch{}window.dispatchEvent(new CustomEvent("dm:sidebar-count",{detail:{url:"#/context-menus"}})),h?(E.toast("Context menu created.",{type:"success"}),location.hash="#/context-menus"):(E.toast("Context menu saved.",{type:"success"}),pe(A,r.name.peek()||p))}catch(t){K.value=String(t.message||t).split("; ").map((i,a)=>({key:String(a),text:i})),m.querySelector(".cmx-alert")?.scrollIntoView({behavior:"smooth",block:"nearest"})}finally{V.value=!1}}function Ye(){E.confirm("Throw away your unsaved changes to this menu?").then(e=>{if(!e||!_())return;const n=Re(x);for(const[t,i]of Object.entries(n))r[t].value=i;s.value=Array.isArray(x?.items)?x.items:[],K.value=[]})}m.querySelector("#ctx-items").innerHTML=It,m.querySelector(".cmx-chips").innerHTML=Tt,m.querySelector(".cmx-server-errors").innerHTML=Lt;const ye=m.querySelector("#ctx-accent");if(D.accent&&!lt.includes(D.accent)&&ye){const e=document.createElement("option");e.value=D.accent,e.textContent=`Custom (${D.accent})`,ye.appendChild(e)}const Je={...r,items:s,rows:te,chips:Ue,dirty:G,saving:V,err:_e,tabBad:he,serverErrors:K,isNew:h,notNew:!h,previewText:Ge,serverError:M.computed(()=>K.value.length>0),saveText:M.computed(()=>h?"Create menu":"Save"),problemText:M.computed(()=>Y.value&&!O.value.ok?" - some need fixing before they can be saved":""),scopeText:M.computed(()=>{const e=Q(r.pages.value),n=Q(r.projects.value);return!e.length&&!n.length?"Site-wide: every page carries this menu.":`Only ${[e.length&&`pages matching ${e.join(", ")}`,n.length&&`pages in ${n.join(", ")}`].filter(Boolean).join(", or ")}.`})},We=E.tabs(m.querySelector("#ctx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Qe=M.applyBindings(Je,m,{methods:{save:()=>oe(),discard:()=>Ye(),addItem:()=>N("item"),addHeader:()=>N("header"),addDivider:()=>N("divider")}});c(()=>Qe.dispose()),c(qe(m,{position:"bottom"}));const ge=m.querySelector("#ctx-slug"),xe=()=>{ee=!0};ge?.addEventListener("input",xe),c(()=>ge?.removeEventListener("input",xe));const Xe=E.contextMenu(b,{cursor:"glyph",match:".cmx-row",inherit:!1,density:"compact",items:e=>Ve(Number(e.dataset.index))});c(()=>Xe?.destroy?.());const ke=e=>{const n=e.target.closest(".cmx-row");if(!n)return;const t=Number(n.dataset.index);if(e.target.closest(".cmx-kebab")){Ne(e.target.closest(".cmx-kebab"));return}if(e.target.closest(".cmx-up")){q(t,t-1);return}if(e.target.closest(".cmx-down")){q(t,t+1);return}ae(t)},we=e=>{const n=e.target.closest(".cmx-row");if(!n||e.target!==n)return;const t=Number(n.dataset.index);e.key==="F10"&&e.shiftKey?(e.preventDefault(),Ne(n)):e.key==="Enter"?(e.preventDefault(),ae(t)):e.altKey&&e.key==="ArrowUp"?(e.preventDefault(),q(t,t-1)):e.altKey&&e.key==="ArrowDown"?(e.preventDefault(),q(t,t+1)):e.key==="Delete"&&(e.preventDefault(),k(W(s.peek(),t)),j(Math.max(0,t-1)))};if(b.addEventListener("click",ke),b.addEventListener("keydown",we),c(()=>{b.removeEventListener("click",ke),b.removeEventListener("keydown",we)}),typeof E.sortable=="function"){const e=Mt(),n=E.sortable(b,{items:".cmx-row",key:null,keyboard:!1,onStart:e.start,onEnd:({item:t})=>e.restore(t),onSort:({item:t,from:i,to:a})=>{e.restore(t),k(Fe(s.peek(),i,a)),j(a)}});c(()=>n.destroy())}h?pe(A,"New menu"):pe(A,x?.name||p),je(l,{id:"ctx-editor-back",icon:"arrow-left",title:"All context menus",onClick:()=>{location.hash="#/context-menus"}}),je(l,{id:"ctx-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>oe()});const Ee=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||v||(e.preventDefault(),e.stopImmediatePropagation(),oe())};document.addEventListener("keydown",Ee,!0),c(()=>document.removeEventListener("keydown",Ee,!0));const Se=()=>{if(G.peek())try{S.set(F(p),{form:U(),items:s.peek(),base:z.peek(),at:new Date().toISOString()})}catch{}},Me=e=>{v?.dirty()&&v.apply(),G.peek()&&(Se(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Me),c(()=>window.removeEventListener("beforeunload",Me)),w={path:Be,dirty:()=>G.peek()||!!v?.dirty(),dispose(){if(T=!0,v){const e=v;e.dirty()&&e.apply();try{e.so.close(!0)}catch{}}Se(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(m);let y=null;try{y=S.get(F(p))}catch{}if(y?.form){try{S.remove?S.remove(F(p)):S.set(F(p),null)}catch{}const e=y.base&&y.base!==z.peek(),n=y.at?` (${St(y.at)})`:"",t=h?"the new context menu you started":`"${$(x?.name||p)}"`,i=e?`You left unsaved changes to ${t}${n}, but it has been saved since. Restore your version anyway?`:`You left unsaved changes to ${t}${n}. Restore them?`;if(await E.confirm(i)&&_()){for(const[a,J]of Object.entries(y.form))r[a]&&a!==(h?"":"slug")&&(r[a].value=J);h&&y.form.slug&&(ee=!0),Array.isArray(y.items)&&(s.value=y.items)}}}};
64
+ </div>`),P=pt("edit",i.icon);P.input.id="cmx-i-icon",P.input.classList.add("form-input");const De=()=>{o.icon.value=P.input.value};P.input.addEventListener("input",De),w.querySelector(".cmx-icon-mount").appendChild(P.el);const ce=w.querySelector(".cmx-arg-mount");let v=null;const le=()=>{o.arg.value=v.value};function Oe(){v?.removeEventListener("input",le),ce.replaceChildren();const a=_[o.action.peek()];if(!a){v=null;return}if(a.key==="url"){const c=mt(a.placeholder,o.arg.peek());v=c.input,ce.appendChild(c.el)}else v=document.createElement("input"),v.type="text",v.className="form-input",v.placeholder=a.placeholder,v.value=o.arg.peek(),ce.appendChild(v);v.id="cmx-i-arg",v.addEventListener("input",le)}let $e=o.action.peek();const ot=M.effect(()=>{const a=o.action.value;a!==$e&&($e=a,o.arg.value="",Oe())});Oe();const qe=()=>{const a=Ke(W(),t),c=[...s.peek()],L=c.indexOf(t);L!==-1&&(c[L]=a,k(c))};let K=!1,de=!1;const je={done:()=>{qe(),K=!0,g.close(),q(e)},cancel:()=>g.close(),remove:()=>{const a=s.peek().indexOf(t);a!==-1&&k(X(s.peek(),a)),K=!0,g.close()}};g.element.appendChild(w);const st=me(w),it=M.applyBindings(Le,w,{methods:je}),B=w.querySelector(".cmx-action-help");let ue=()=>{};const rt=M.effect(()=>{const a=o.action.value;ue(),B.replaceChildren(),B.removeAttribute("data-help-ready"),B.dataset.helpTitle=Fe.flatMap(c=>Object.entries(c.actions)).find(([c])=>c===a)?.[1]||"Selected action",B.dataset.help=dt[a]||"Choose an action to see what it does.",ue=me(B)}),ct=a=>{(a.ctrlKey||a.metaKey)&&a.key==="Enter"&&(a.preventDefault(),je.done())};w.addEventListener("keydown",ct);const Ne={so:g,dirty:ie,apply:qe};h=Ne,ut(g,()=>{ot(),it.dispose(),st(),rt(),ue(),P.input.removeEventListener("input",De),v?.removeEventListener("input",le),h===Ne&&(h=null)});const lt=g.close.bind(g);g.close=async a=>{if(!K&&a!==!0&&ie()){if(de)return;de=!0;const c=await E.confirm("Discard the changes to this item?");if(de=!1,!c)return}if(!K&&n&&a!==!0&&!ie()){const c=s.peek().indexOf(t);c!==-1&&k(X(s.peek(),c))}return K=!0,lt()},I.scan(w),g.open(),requestAnimationFrame(()=>w.querySelector(i.type==="divider"?"#cmx-i-type":"#cmx-i-text")?.focus())}l(()=>{if(h){const e=h;e.dirty()&&e.apply();try{e.so.close(!0)}catch{}}});async function se(){if(J.peek())return;Y.value=!0,H.value=[];const e=$.peek();if(!e.ok){const t=Object.keys(e.errors).length;E.toast(`Not saved yet - ${t===1?"one thing needs":`${t} things need`} fixing (marked in red).`,{type:"warning"});const i=ge.peek(),o=i.binding?0:i.items?1:i.appearance?2:-1;o>=0&&Ze?.activate?.(o);return}J.value=!0;const n=Mt(z(),s.peek());try{if(f)await pe.contextMenus.create(n);else{const t=await pe.contextMenus.update(m,n);t?.items&&(s.value=t.items)}if(!U())return;G.value=he(z(),s.peek());try{S.remove?S.remove(F(m)):S.set(F(m),null)}catch{}window.dispatchEvent(new CustomEvent("dm:sidebar-count",{detail:{url:"#/context-menus"}})),f?(E.toast("Context menu created.",{type:"success"}),location.hash="#/context-menus"):(E.toast("Context menu saved.",{type:"success"}),fe(C,r.name.peek()||m))}catch(t){H.value=String(t.message||t).split("; ").map((i,o)=>({key:String(o),text:i})),p.querySelector(".cmx-alert")?.scrollIntoView({behavior:"smooth",block:"nearest"})}finally{J.value=!1}}function Xe(){E.confirm("Throw away your unsaved changes to this menu?").then(e=>{if(!e||!U())return;const n=He(x);for(const[t,i]of Object.entries(n))r[t].value=i;s.value=Array.isArray(x?.items)?x.items:[],H.value=[]})}p.querySelector("#ctx-items").innerHTML=Ot,p.querySelector(".cmx-chips").innerHTML=$t,p.querySelector(".cmx-server-errors").innerHTML=qt;const ke=p.querySelector("#ctx-accent");if(O.accent&&!ht.includes(O.accent)&&ke){const e=document.createElement("option");e.value=O.accent,e.textContent=`Custom (${O.accent})`,ke.appendChild(e)}const Qe={...r,items:s,rows:ne,chips:Ve,dirty:V,saving:J,err:Ge,tabBad:ge,serverErrors:H,isNew:f,notNew:!f,previewText:Ye,serverError:M.computed(()=>H.value.length>0),saveText:M.computed(()=>f?"Create menu":"Save"),problemText:M.computed(()=>Y.value&&!$.value.ok?" - some need fixing before they can be saved":""),scopeText:M.computed(()=>{const e=Q(r.pages.value),n=Q(r.projects.value);return!e.length&&!n.length?"Site-wide: every page carries this menu.":`Only ${[e.length&&`pages matching ${e.join(", ")}`,n.length&&`pages in ${n.join(", ")}`].filter(Boolean).join(", or ")}.`})},Ze=E.tabs(p.querySelector("#ctx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),et=M.applyBindings(Qe,p,{methods:{save:()=>se(),discard:()=>Xe(),addItem:()=>N("item"),addHeader:()=>N("header"),addDivider:()=>N("divider")}});l(()=>et.dispose()),l(me(p,{position:"bottom"}));const we=p.querySelector("#ctx-slug"),Ee=()=>{te=!0};we?.addEventListener("input",Ee),l(()=>we?.removeEventListener("input",Ee));const tt=E.contextMenu(b,{cursor:"glyph",match:".cmx-row",inherit:!1,density:"compact",items:e=>We(Number(e.dataset.index))});l(()=>tt?.destroy?.());const Se=e=>{const n=e.target.closest(".cmx-row");if(!n)return;const t=Number(n.dataset.index);if(e.target.closest(".cmx-kebab")){Pe(e.target.closest(".cmx-kebab"));return}if(e.target.closest(".cmx-up")){j(t,t-1);return}if(e.target.closest(".cmx-down")){j(t,t+1);return}oe(t)},Me=e=>{const n=e.target.closest(".cmx-row");if(!n||e.target!==n)return;const t=Number(n.dataset.index);e.key==="F10"&&e.shiftKey?(e.preventDefault(),Pe(n)):e.key==="Enter"?(e.preventDefault(),oe(t)):e.altKey&&e.key==="ArrowUp"?(e.preventDefault(),j(t,t-1)):e.altKey&&e.key==="ArrowDown"?(e.preventDefault(),j(t,t+1)):e.key==="Delete"&&(e.preventDefault(),k(X(s.peek(),t)),q(Math.max(0,t-1)))};if(b.addEventListener("click",Se),b.addEventListener("keydown",Me),l(()=>{b.removeEventListener("click",Se),b.removeEventListener("keydown",Me)}),typeof E.sortable=="function"){const e=It(),n=E.sortable(b,{items:".cmx-row",key:null,keyboard:!1,onStart:e.start,onEnd:({item:t})=>e.restore(t),onSort:({item:t,from:i,to:o})=>{e.restore(t),k(Be(s.peek(),i,o)),q(o)}});l(()=>n.destroy())}f?fe(C,"New menu"):fe(C,x?.name||m),Re(d,{id:"ctx-editor-back",icon:"arrow-left",title:"All context menus",onClick:()=>{location.hash="#/context-menus"}}),Re(d,{id:"ctx-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>se()});const Ae=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||h||(e.preventDefault(),e.stopImmediatePropagation(),se())};document.addEventListener("keydown",Ae,!0),l(()=>document.removeEventListener("keydown",Ae,!0));const Ce=()=>{if(V.peek())try{S.set(F(m),{form:z(),items:s.peek(),base:G.peek(),at:new Date().toISOString()})}catch{}},Te=e=>{h?.dirty()&&h.apply(),V.peek()&&(Ce(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Te),l(()=>window.removeEventListener("beforeunload",Te)),A={path:ze,dirty:()=>V.peek()||!!h?.dirty(),dispose(){if(T=!0,h){const e=h;e.dirty()&&e.apply();try{e.so.close(!0)}catch{}}Ce(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(p);let y=null;try{y=S.get(F(m))}catch{}if(y?.form){try{S.remove?S.remove(F(m)):S.set(F(m),null)}catch{}const e=y.base&&y.base!==G.peek(),n=y.at?` (${Tt(y.at)})`:"",t=f?"the new context menu you started":`"${D(x?.name||m)}"`,i=e?`You left unsaved changes to ${t}${n}, but it has been saved since. Restore your version anyway?`:`You left unsaved changes to ${t}${n}. Restore them?`;if(await E.confirm(i)&&U()){for(const[o,W]of Object.entries(y.form))r[o]&&o!==(f?"":"slug")&&(r[o].value=W);f&&y.form.slug&&(te=!0),Array.isArray(y.items)&&(s.value=y.items)}}}};
@@ -1,32 +1,33 @@
1
- import{api as P}from"../api.js";import{colourToCss as St,HIGHLIGHT_HOVER as dt,HIGHLIGHT_ACTIVE as ut,HIGHLIGHT_HOVER_LABELS as mt,HIGHLIGHT_ACTIVE_LABELS as pt,cssClassProblems as ht,normaliseCssClasses as xt,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-bar-shapes";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}>
1
+ import{api as P}from"../api.js";import{colourToCss as Et,HIGHLIGHT_HOVER as ut,HIGHLIGHT_ACTIVE as pt,HIGHLIGHT_HOVER_LABELS as mt,HIGHLIGHT_ACTIVE_LABELS as ht,cssClassProblems as ft,normaliseCssClasses as At,ENTRANCE_ANIMATIONS as ot,ENTRANCE_ANIMATION_LABELS as Ct}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{openIconPicker as $t}from"../lib/card-builder.js";import{normalisePanel as Lt,DESCRIPTION_MAX as Tt}from"/public/js/menu-look.mjs?v=20261029-bar-shapes";import{sortableMenuRows as _t}from"../lib/menu-drag.js";import{placeTools as Ht,ensureLockedTools as It,isLockedUrl as Nt,groupKeyFor as Ot,slugify as Rt,GROUP_ICON as Bt,TOOLS_FOLDER_KEY as Pt}from"../lib/sidebar-grouping.js";import{flatten as gt,nest as Dt,uid as Xe}from"../lib/menu-rows.js";import{addBannerAction as at}from"../lib/plugin-chrome.js";import{helpPopovers as st}from"../lib/help-popover.js";import{MENU_ITEM_HELP as jt}from"../lib/menu-item-help.js";import{onceClosed as Ft,openMenuAt as lt,sidebarCount as Mt}from"../lib/tool-kit.js";import{addChild as zt,bindingFrom as Ut,duplicateRow as Wt,indentRow as Kt,insertBelow as qt,isStructural as ve,menuFromForm as Gt,menuProblems as Vt,moveRow as vt,outdentRow as Yt,siblingsOf as Xt,snapshot as Je}from"../lib/menu-editor-arrange.js";import{fmtDateTime as Jt}from"../lib/dates.js";const Qt=760,Zt="admin-sidebar",en=/^\/menus\/(new|edit\/[^?#]+)(\?.*)?$/;function x(n){return String(n||"").replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;")}const De=n=>String(n??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function tn(n,u){let l=0,r=u.find(s=>s.id===n);for(;r&&r.parentId;)l++,r=u.find(s=>s.id===r.parentId);return l}function bt(n){const u=new Set,l=new Set,r=s=>{for(const y of n.filter(T=>T.parentId===s))!y.type&&Nt(y.url)&&!l.has(y.url)&&(l.add(y.url),u.add(y.id)),r(y.id)};return r(null),u}const Qe='<button type="button" class="btn btn-sm btn-ghost mex-kebab" aria-label="Actions" title="Actions (right-click, or Shift+F10)">\u22EE</button>';function nn(n,u,l=!1){const r=l?bt(u):new Set,s=[];function y(T){for(const p of u.filter(G=>G.parentId===T)){const $=`style="margin-left:${tn(p.id,u)*28}px"`;if(p.type==="separator"){s.push(`
2
+ <div class="menu-tree-row menu-tree-row--separator" data-id="${p.id}" tabindex="0" ${$}>
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
- ${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}>
5
+ ${Qe}
6
+ </div>`);continue}if(p.type==="spacer"){const be=["xs","sm","md","lg","xl","flex"].map(ae=>`<option value="${ae}"${(p.size||"md")===ae?" selected":""}>${ae==="flex"?"flexible":ae}</option>`).join("");s.push(`
7
+ <div class="menu-tree-row menu-tree-row--spacer" data-id="${p.id}" tabindex="0" ${$}>
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">${ve}</select>
11
+ <select class="form-input me-spacer-size" title="Spacer size - flexible fills the leftover room" aria-label="Spacer size">${be}</select>
12
+ <span data-help-title="Spacer size" data-help="Choose a fixed gap from xs (smallest) to xl (largest), or flexible to fill leftover room along the menu. A fixed size separates neighbouring links; a flexible spacer can push groups apart in a horizontal row. This row supplies space only, with no label or destination. Save the menu to keep a size change."></span>
12
13
  </span>
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}>
14
+ ${Qe}
15
+ </div>`);continue}if(p.type==="theme-switcher"){s.push(`
16
+ <div class="menu-tree-row menu-tree-row--switcher" data-id="${p.id}" tabindex="0" ${$}>
16
17
  <span class="menu-tree-grip" data-icon="grip-vertical" data-icon-size="18" title="Drag to move"></span>
17
18
  <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
- ${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=St(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}>
19
+ ${Qe}
20
+ </div>`);continue}const V=p.badge&&(p.badge.text||p.badge.countFrom)?`<span class="dm-menu-badge">${x(String(p.badge.text||"#"))}</span>`:"",_=p.tool,f=_&&(_.persisted||p.touched),re=_?_.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="${f?"A Tool, placed here by this menu":"A Tool in its default place - move or edit it to place it here"}">${x(_.source||"Tool")}${f?"":" \xB7 default"}</span>`:"",ne=r.has(p.id),ce=Et(p.colour),oe=ce?` style="color:${x(ce)}"`:"",de=ne?'<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(`
21
+ <div class="menu-tree-row menu-tree-row--slim${p.hidden?" menu-tree-row--hidden":""}${_?" menu-tree-row--tool":""}${_&&!f?" menu-tree-row--default":""}" data-id="${p.id}" tabindex="0" ${$}>
21
22
  <span class="menu-tree-grip" data-icon="grip-vertical" data-icon-size="18" title="Drag to move"></span>
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}
23
+ <span class="me-row-label${!p.text&&!_?" mex-untitled":""}"${oe}>${x(p.text||_?.label||"(no label - click to add one)")}${V}${re}</span>
24
+ <span class="me-row-url">${x(p.url||"")}</span>
25
+ ${de}
25
26
  <button type="button" class="btn btn-sm btn-ghost me-edit" title="Edit" aria-label="Edit"><span data-icon="edit"></span></button>
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 Se=!1,xe=!1;Pt(_);const Be=_.close.bind(_);_.close=async m=>{if(!Se&&m!==!0&&Re()!==Ze){if(xe)return;xe=!0;const F=await E.confirm("Discard the changes to this item?");if(xe=!1,!F)return}return Se=!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=xt(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})}Se=!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(),Se=()=>{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"})},xe=(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))))};xe("#me-highlight-hover",dt,mt,"- theme default -"),xe("#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-brand").val(e.look?.brand||""),t("#me-look-strip").val(e.look?.strip||""),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-look-shrink").get(0).checked=e.look?.shrink===!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||""),Se(),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(),lookBrand:t("#me-look-brand").val(),lookStrip:t("#me-look-strip").val(),lookTiles:t("#me-look-tiles").val(),lookMorph:$("#me-look-morph"),lookShrink:$("#me-look-shrink"),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")&&Se(),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())}}};
27
+ ${Qe}
28
+ </div>`),y(p.id)}}y(null),n._disposeMenuHelp?.(),n.innerHTML=s.join("")||'<p class="mex-note">No items yet - Add item, or right-click an item for more.</p>',n._disposeMenuHelp=st(n),I.scan(n)}function b(n,u={},...l){const r=document.createElement(n);Object.assign(r,u);for(const s of l)s!=null&&r.append(s);return r}function w(n,u,l){const r=b("label",{className:"form-field"},b("span",{className:"form-label",textContent:n}),u),s=jt[n];if(s){const y=b("span");y.dataset.help=s+(l?" "+l:""),y.dataset.helpTitle=n,r.querySelector(".form-label").append(" ",y)}return l&&r.append(b("small",{className:"form-hint",textContent:l,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),r}function on(n,u){const l=[["","- Everyone -"],["public","Public - anyone, incl. logged-out"],...(u||[]).map(s=>[s.name,`${s.label||s.name} and above`]),...(u||[]).filter(s=>s.level!==0).map(s=>[`=${s.name}`,`${s.label||s.name} only`]),["private","Private - super-admin only"]];n&&!l.some(([s])=>s===n)&&l.push([n,`${n} (custom)`]);const r=K(l,n||"");return r.name="visibility",r}function K(n,u){const l=b("select",{className:"form-select"});return n.forEach(([r,s])=>l.add(new Option(s,r))),l.value=u??"",l}function je(n,u){const l=typeof u=="string"&&u.startsWith("#"),r=K([["","- none -"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],l?"__custom__":u||""),s=b("input",{type:"color",className:"form-input me-colour-hex",value:l?u:"#000000"});s.style.display=l?"":"none",r.addEventListener("change",()=>{s.style.display=r.value==="__custom__"?"":"none"});const y=w(n,r);return y.append(s),y._read=()=>r.value==="__custom__"?s.value:r.value,y}function an(n,u){const l=b("input",{className:"form-input me-icon",name:"icon",value:u||""}),r=b("span",{className:"me-icon-preview"});u&&r.setAttribute("data-icon",u);const s=b("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),y=T=>{T?r.setAttribute("data-icon",T):r.removeAttribute("data-icon"),Domma.icons.scan(r.parentNode||r)};return s.addEventListener("click",()=>$t(T=>{l.value=T,y(T)})),l.addEventListener("input",()=>y(l.value.trim())),w(n,b("div",{className:"me-icon-field"},l,s,r))}function te(...n){return b("div",{className:"me-field-grid"},...n)}async function sn(n,u,l,r,s,{cards:y=!0}={}){const T=n.text||n.tool?.label||"",p=m=>b("h4",{className:"me-form-section",textContent:m}),G=w("Label",b("input",{className:"form-input",name:"text",value:n.text||""}),"The text shown for this item."),$=b("input",{className:"form-input",name:"url",value:n.url||""}),V=w("URL",$,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)&&($.readOnly=!0);const _=an("Icon",n.icon||""),f=on(n.visibility||"",r),re=w("Visibility",f,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&&(f.disabled=!0);const ne=K([["","- no permission gate -"],...(u||[]).map(m=>[m.key,m.label||m.key])],n.permission||"");ne.name="permission";const ce=w("Permission",ne,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)&&(ne.disabled=!0);const oe=w("Badge text",b("input",{className:"form-input",name:"badgeText",value:n.badge?.text||""})),de=K([["","- none -"],...(l||[]).map(m=>[m.slug,m.title||m.slug])],n.badge?.countFrom||"");de.name="badgeCount";const be=w("Badge count from",de,"Show a live count of this collection\u2019s entries as the badge."),ae=je("Badge colour",n.badge?.variant||""),ye=K([["link","Link"],["pill","Pill"]],n.pill?"pill":"link");ye.name="renderAs";const D=w("Render as",ye,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),$e=w("Pill style",K([["filled","Filled"],["outline","Outline"]],n.pill?.style||"filled"));$e.querySelector("select").name="pillStyle";const Le=je("Pill colour",n.pill?.variant||""),h=b("div",{className:"me-pill-wrap"},te($e,Le));h.style.display=n.pill?"":"none",ye.addEventListener("change",()=>{h.style.display=ye.value==="pill"?"":"none"});const j=je("Text colour",n.colour||""),ue=K([["","- use the menu default -"],...ut.map(m=>[m,mt[m]||m])],n.highlight?.hover||"");ue.name="hlHover";const Fe=w("Hover effect",ue,"Overrides the menu-wide hover effect for this item only."),Te=K([["","- use the menu default -"],...pt.map(m=>[m,ht[m]||m])],n.highlight?.active||"");Te.name="hlActive";const U=w("When selected",Te,"How this item is marked when it points at the page being viewed."),L=je("Highlight colour",n.highlight?.accent||""),ke=b("input",{className:"form-input",name:"class",value:n.class||"",placeholder:"nav-cta push-right",autocomplete:"off",spellcheck:!1}),N=w("CSS classes",ke,"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."),Y=b("small",{className:"text-danger",style:"display:none;margin-top:.25rem"});N.append(Y);const pe=b("textarea",{className:"form-input",name:"description",rows:2,maxLength:Tt,value:n.description||"",placeholder:"Answers every new lead in under a minute."}),me=w("Description",pe,"One short line under the label, shown when the menu's dropdowns are Cards."),_e=je("Tile colour",n.tile||""),He=b("input",{type:"checkbox",name:"chip",checked:!!n.chip,style:"width:auto;margin:0"}),Ie=b("label",{className:"form-field",style:"display:flex;gap:.5rem;align-items:center;font-weight:400"},He,b("span",{textContent:"Show as a small chip in the card's footer strip"})),we=b("span");we.dataset.helpTitle="Show as a small chip",we.dataset.help="Place this sub-item in the compact chip strip at the bottom of its parent Cards dropdown instead of the main entries. Its label and destination remain the same. The parent card can provide a Chip strip label. This applies to sub-items in Cards dropdowns, rather than top-level navbar links.",Ie.append(we);const F=n.parentId==null,C=n.panel||{},O=(m,X,J,A=80)=>b("input",{className:"form-input",name:m,value:X||"",placeholder:J,maxLength:A}),Me=w("Card heading",O("panelHeading",C.heading,"The crew - six agents, one job each"),"Small capitals over the entries."),ze=w("Link text",O("panelLinkText",C.link?.text,"Meet the crew",60)),Ue=w("Link URL",O("panelLinkUrl",C.link?.url,"/crew",500)),Ne=K([["","Automatic"],["1","1"],["2","2"],["3","3"],["4","4"]],C.columns?String(C.columns):"");Ne.name="panelColumns";const Ze=w("Columns",Ne),et=w("Chip strip label",O("panelChipsLabel",C.chipsLabel,"Add to your crew",60),'Words before the chips - tick "Show as a small chip" on the sub-items that belong there.'),he=K([["","- no side box -"],["dark","Dark"],["light","Light"],["accent","Theme colour"]],C.feature?C.feature.tone||"dark":"");he.name="featureTone";const We=w("Side box",he,"A box down the right of the card - a call to action or a short list."),Ke=w("Box small heading",O("featureEyebrow",C.feature?.eyebrow,"Free - 20 minutes",60)),qe=w("Box title",O("featureTitle",C.feature?.title,"See it live")),t=w("Box points",b("textarea",{className:"form-input",name:"featurePoints",rows:3,value:(C.feature?.points||[]).join(`
29
+ `),placeholder:"One per line, up to six"})),Se=w("Button text",O("featureBtnText",C.feature?.button?.text,"Book a demo",60)),Oe=w("Button URL",O("featureBtnUrl",C.feature?.button?.url,"/book",500)),fe=b("div",{},te(Ke,qe),t,te(Se,Oe));fe.style.display=he.value?"":"none",he.addEventListener("change",()=>{fe.style.display=he.value?"":"none"});const tt=y?[p("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)."}),me,te(_e,F?null:Ie),...F?[Me,te(ze,Ue),te(Ze,et),We,fe]:[]]:[],se=b("button",{className:"btn btn-primary",textContent:"Apply",title:"Apply to the item (Ctrl+Enter) - Save the menu to keep it"}),W=b("div",{className:"me-item-form"},p("Link"),G,V,_,p("Access"),te(re,ce),p("Badge"),te(oe,ae),be,p("Appearance"),D,h,j,p("Highlighting"),te(Fe,U),L,...tt,p("Styling"),N,se),B=E.slideover({title:T?`Edit ${De(T)}`:"Edit menu item",size:"lg",position:"right"});B.element.appendChild(W);const Ge=()=>JSON.stringify([...W.querySelectorAll("input, select, textarea")].map(m=>m.type==="checkbox"?m.checked:m.value)),Ve=Ge();let xe=!1,ge=!1;const Re=st(W,{position:"bottom"});Ft(B,Re);const Ee=B.close.bind(B);B.close=async m=>{if(!xe&&m!==!0&&Ge()!==Ve){if(ge)return;ge=!0;const X=await E.confirm("Discard the changes to this item?");if(ge=!1,!X)return}return xe=!0,Ee()},W.addEventListener("keydown",m=>{(m.ctrlKey||m.metaKey)&&m.key==="Enter"&&(m.preventDefault(),se.click())}),B.open();const Be=B.element.closest(".dm-slideover")||B.element.querySelector(".dm-slideover")||B.element;Be.style.setProperty("width",Qt+"px","important"),Be.style.setProperty("max-width","95vw","important"),Domma.icons.scan(W);const g=m=>W.querySelector(`[name="${m}"]`)?.value||"";se.addEventListener("click",()=>{const m=ft(g("class"));if(m.length){Y.textContent=m.join("; "),Y.style.display="block",ke.focus();return}n.class=At(g("class")),n.text=g("text"),n.icon=g("icon"),n.locked||(n.visibility=g("visibility")),!n.tool&&!n.locked?(n.url=g("url"),n.permission=g("permission")):n.touched=!0;const X=g("badgeText"),J=g("badgeCount"),A=ae._read();if(n.badge=X||J?{...X&&{text:X},...J&&{countFrom:J},...A&&{variant:A}}:null,g("renderAs")==="pill"){const Q=Le._read();n.pill={style:g("pillStyle")||"filled",...Q&&{variant:Q}}}else n.pill=null;n.colour=j._read();const le=g("hlHover"),Ae=g("hlActive"),Ce=L._read();if(n.highlight=le||Ae||Ce?{...le&&{hover:le},...Ae&&{active:Ae},...Ce&&{accent:Ce}}:null,y&&(n.description=g("description").trim(),n.tile=_e._read(),n.chip=!F&&He.checked,F)){const Q=g("featureTone");n.panel=Lt({heading:g("panelHeading"),link:{text:g("panelLinkText"),url:g("panelLinkUrl")},columns:g("panelColumns"),chipsLabel:g("panelChipsLabel"),feature:Q?{tone:Q,eyebrow:g("featureEyebrow"),title:g("featureTitle"),points:g("featurePoints").split(`
30
+ `),button:{text:g("featureBtnText"),url:g("featureBtnUrl")}}:null})}xe=!0,B.close(!0),s()})}let q=null,yt=!1;function ln(){yt||!window.R?.use||(yt=!0,R.use((n,u,l)=>{const r=u?.path||"";return!(en.test(r)&&(n?.path||"")!==r)||!q?.dirty()?l():E.confirm(`You have unsaved changes to the menu "${De(q.name())}". Leave anyway? They are kept as a draft for your next visit.`).then(y=>{y?l():history.replaceState(null,"",`#${r}`)})}),M.subscribe("router:afterChange",({to:n}={})=>{if(q&&(n?.path||"")!==q.path){const u=q;q=null,u.dispose()}}))}function kt(n,u){const l=n.querySelector(".dm-plugin-banner-name"),r=l?.querySelector(".dm-plugin-banner-sep");if(!(!l||!r)){for(;r.nextSibling;)r.nextSibling.remove();l.appendChild(document.createTextNode(u))}}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(n){if(ln(),q){const e=q;q=null,e.dispose()}const u=[],l=e=>(u.push(e),e);let r=!1;const s=n.get(0).querySelector(".mex")||n.get(0),y=()=>!r&&s.isConnected,T=location.hash.replace(/^#/,""),p=location.hash.match(/^#\/menus\/edit\/([^?#]+)/),G=p?decodeURIComponent(p[1]):null,$=!G,V=`menu-editor.draft.${G||"new"}`,_=E.loader(n.get(0),{type:"dots"});let f,re,ne,ce,oe,de,be;try{if($)f={slug:"",name:"",description:"",items:[]};else try{f=await P.menus.get(G)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}[re,ne,ce,oe,de,be]=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{_.destroy()}if(!s.isConnected)return;const ye=(Array.isArray(oe)?oe:oe?.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=!$&&G===Zt;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}),[])),!s.isConnected))return;const Le=e=>gt(D?It(Ht(e||[],$e,{forEditor:!0}),{leaveInHiddenFolders:!0}):e);let h=Le(f.items);const j=M.observable(!1),ue=M.observable(!1),Fe=M.observable(""),Te=M.observable(0);let U="";const L=s.querySelector("#me-items-list");l(()=>L._disposeMenuHelp?.());function ke(){nn(L,h,D),Te.value=h.filter(e=>!ve(e)).length}const N=()=>{ke(),Z()},Y=()=>D?bt(h):new Set,pe=e=>h.find(o=>o.id===e);function me(e){requestAnimationFrame(()=>L.querySelector(`.menu-tree-row[data-id="${CSS.escape(e)}"]`)?.focus())}function _e(e){const o=v=>h.find(i=>!i.type&&!i.url&&!i.tool&&(i.key||"").toLowerCase()===v)||h.find(i=>!i.type&&!i.url&&!i.tool&&!i.key&&(i.text||"").toLowerCase()===v);let a=o(String(e.tool.folder||"").toLowerCase())||o(Pt);const c=String(e.tool.group||"").trim();if(a&&c){const v=Ot(a.key||Rt(a.text),c),i=d=>d!==e&&!d.type&&!d.url&&!d.tool;let k=h.find(d=>i(d)&&(d.key||"").toLowerCase()===v)||h.find(d=>i(d)&&d.parentId===a.id&&(!d.key||d.key.toLowerCase()===v)&&(d.text||"").toLowerCase()===c.toLowerCase());k||([k]=gt([{text:c,key:v,icon:Bt,synthesised:!0}],a.id),h.push(k)),a=k}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 He=e=>!Y().has(e.id)&&!(e.tool&&!e.tool.orphan);async function Ie(e){const o=pe(e);if(!o||!He(o))return;const a=d=>{let ee=d;for(;ee;){if(ee.id===e)return!0;ee=h.find(z=>z.id===ee.parentId)}return!1},c=h.filter(d=>d.id!==e&&a(d));if(c.length&&!await E.confirm(`Remove "${De(o.text||o.type||"this item")}" and the ${c.length} item${c.length===1?"":"s"} beneath it?`))return;const v=Y(),i=c.filter(d=>d.tool),k=c.filter(d=>!d.tool&&v.has(d.id));h=h.filter(d=>!a(d)||i.includes(d)||k.includes(d));for(const d of k)d.parentId=o.parentId??null;for(const d of i)d.tool.orphan?d.parentId=o.parentId??null:_e(d);N()}function we(e){const o=pe(e);!o||ve(o)||Y().has(e)||(o.hidden=!o.hidden,o.touched=!0,N())}function F(e){const o=pe(e);!o||ve(o)||(o.locked=D&&Y().has(o.id),sn(o,ne.resources||[],ce,ye,()=>{N(),me(e)},{cards:!D}))}function C(e){const o={id:Xe(),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),N(),ve(o)?me(o.id):F(o.id)}const O=e=>o=>{e(o)&&(N(),me(o))},Me=O(e=>vt(h,e,-1)),ze=O(e=>vt(h,e,1)),Ue=O(e=>Kt(h,e)),Ne=O(e=>Yt(h,e));function Ze(e){const o=pe(e);if(!o)return[];const a=ve(o),c=Y().has(e),v=Xt(h,e),i=v.findIndex(z=>z.id===e),k=o.tool,d=k&&(k.persisted||o.touched),ee=z=>()=>{const Pe=qt(h,e,z,Xe());N(),Pe&&!ve(Pe)?F(Pe.id):Pe&&me(Pe.id)};return[{header:a?{separator:"Separator",spacer:"Spacer","theme-switcher":"Theme switcher"}[o.type]:o.text||k?.label||"(untitled)"},...a?[]:[{label:"Edit\u2026",icon:"edit",action:()=>F(e)}],{type:"divider"},{label:"Add item below",icon:"plus",action:ee("item")},{label:"Add sub-item",icon:"corner-down-right",disabled:a,action:()=>{const z=zt(h,e,Xe());N(),z&&F(z.id)}},{label:"Add separator below",icon:"minus",action:ee("separator")},{label:"Add spacer below",icon:"move",action:ee("spacer")},...D?[]:[{label:"Add theme switcher below",icon:"palette",action:ee("theme-switcher")}],{type:"divider"},{label:"Move up",icon:"arrow-up",shortcut:"Alt+\u2191",disabled:i<=0,action:()=>Me(e)},{label:"Move down",icon:"arrow-down",shortcut:"Alt+\u2193",disabled:i<0||i>=v.length-1,action:()=>ze(e)},{label:"Indent",icon:"chevron-right",shortcut:"Alt+\u2192",disabled:i<=0||ve(v[i-1]),action:()=>Ue(e)},{label:"Outdent",icon:"chevron-left",shortcut:"Alt+\u2190",disabled:o.parentId==null,action:()=>Ne(e)},{type:"divider"},...a?[]:[{label:o.hidden?"Show":"Hide",icon:o.hidden?"eye":"eye-off",disabled:c,action:()=>we(e)}],{label:"Duplicate",icon:"copy",disabled:!!k,action:()=>{const z=Wt(h,e,Xe);N(),z&&me(z.id)}},...k&&!k.orphan&&d?[{label:"Back to its default place",icon:"rotate-ccw",action:()=>{_e(o),N()}}]:[],{label:c?"Remove - always in the sidebar":k&&!k.orphan?"Remove - a Tool is hidden, not removed":"Remove\u2026",icon:"trash",danger:!0,disabled:!He(o),action:()=>Ie(e)}]}const et=E.contextMenu(L,{cursor:"glyph",match:".menu-tree-row",inherit:!1,density:"compact",items:e=>Ze(e.dataset.id)});l(()=>et?.destroy?.());const he=_t(L,()=>h,N,{items:".menu-tree-row",handle:".menu-tree-grip"});l(()=>he?.destroy());const We=e=>{const o=e.target.closest(".menu-tree-row");if(!o)return;const a=o.dataset.id;if(e.target.closest(".mex-kebab")){lt(e.target.closest(".mex-kebab"));return}if(e.target.closest(".me-hidden")){we(a);return}if(e.target.closest(".me-edit")){F(a);return}e.target.closest("select, .menu-tree-grip, button")||F(a)},Ke=e=>{const o=e.target.closest(".menu-tree-row");if(!o||e.target!==o){o&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),lt(o));return}const a=o.dataset.id;if(e.key==="F10"&&e.shiftKey)e.preventDefault(),lt(o);else if(e.key==="Enter")e.preventDefault(),F(a);else if(e.key==="Delete")e.preventDefault(),Ie(a);else if(e.altKey&&e.key==="ArrowUp")e.preventDefault(),Me(a);else if(e.altKey&&e.key==="ArrowDown")e.preventDefault(),ze(a);else if(e.altKey&&e.key==="ArrowRight")e.preventDefault(),Ue(a);else if(e.altKey&&e.key==="ArrowLeft")e.preventDefault(),Ne(a);else if(e.key==="ArrowDown"||e.key==="ArrowUp"){const c=[...L.querySelectorAll(".menu-tree-row")],v=c.indexOf(o)+(e.key==="ArrowDown"?1:-1);c[v]&&(e.preventDefault(),c[v].focus())}},qe=e=>{if(!e.target.matches(".me-spacer-size"))return;const o=pe(e.target.closest(".menu-tree-row")?.dataset.id);o&&(o.size=e.target.value,Z())};L.addEventListener("click",We),L.addEventListener("keydown",Ke),L.addEventListener("change",qe),l(()=>{L.removeEventListener("click",We),L.removeEventListener("keydown",Ke),L.removeEventListener("change",qe)});const t=e=>n.find(e),Se=t("#me-float-accent"),Oe=t("#me-float-accent-hex"),fe=()=>{Oe.get(0).style.display=Se.val()==="__custom__"?"":"none"},tt=()=>Se.val()==="__custom__"?Oe.val():Se.val(),se=t("#me-highlight-accent"),W=t("#me-highlight-accent-hex"),B=()=>{W.get(0).style.display=se.val()==="__custom__"?"":"none"},Ge=()=>se.val()==="__custom__"?W.val():se.val(),Ve=()=>{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&&fe();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 c=!!(t("#me-dock-collapsible").get(0)||{}).checked,v=!!(t("#me-dock-autohide").get(0)||{}).checked;t(".dm-dock-collapsed-field").each(function(){this.style.display=c&&!v?"":"none"});const i=!!(t("#me-float-collapsible").get(0)||{}).checked;t(".dm-float-collapsed-field").each(function(){this.style.display=o&&i?"":"none"});const k=!a&&!D;t(".dm-look-field").each(function(){this.style.display=k?"":"none"});const d=k&&t("#me-look-dropdown").val()==="cards";t(".dm-look-cards-field").each(function(){this.style.display=d?"":"none"})},xe=(e,o,a,c)=>{const v=t(e).get(0);v&&(v.innerHTML="",v.add(new Option(c,"")),o.forEach(i=>v.add(new Option(a[i]||i,i))))};xe("#me-highlight-hover",ut,mt,"- theme default -"),xe("#me-highlight-active",pt,ht,"- theme default -");{const e=t("#me-entrance-animation").get(0);e&&(e.innerHTML="",ot.forEach(o=>e.add(new Option(Ct[o]||o,o))))}const ge=t("#me-bind-slot"),Re=t("#me-bind-projects"),Ee=t("#me-bind-pages");ge.html('<option value="">- not bound -</option>'+(de||[]).map(e=>`<option value="${x(e.slot)}">${x(e.label||e.slot)} (${x(e.slot)})</option>`).join("")),Re.html(re.map(e=>`<option value="${x(e.slug)}">${x(e.name||e.slug)}</option>`).join("")),t("#me-bind-page-pick").html('<option value="">- pick a page -</option>'+be.map(e=>{const o=e.urlPath||e.url||"";return o?`<option value="${x(o)}">${x(e.title||o)} - ${x(o)}</option>`:""}).join(""));const Be=()=>Ut({slot:ge.val(),projects:Array.from(Re.get(0).selectedOptions).map(e=>e.value),pagesText:Ee.val()});function g(){const e=Be(),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(i=>`<code>${x(i)}</code>`).join(", ")}`),e.pages.length&&a.push(`pages matching ${e.pages.map(i=>`<code>${x(i)}</code>`).join(", ")}`);const c=e.pages.filter(i=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(i)),v=c.length?`<div class="text-danger" style="margin-top:.4rem">Not valid page patterns: ${c.map(i=>`<code>${x(i)}</code>`).join(", ")}</div>`:"";o.html(a.length?`Takes over the <code>${x(e.slot)}</code> slot on ${a.join(" and ")}.${v}`:`<span class="text-warning">Slot <code>${x(e.slot)}</code> selected but no project or page chosen - the binding will never match.</span>`)}const{getProjectFromHash:m}=await import("../lib/project-context.js");if(!y())return;const X=$?m():null;t("#me-project").html('<option value="">- none -</option>'+re.map(e=>`<option value="${x(e.slug)}">${x(e.name||e.slug)}</option>`).join(""));function J(e){h=Le(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[i,k]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=t(i).get(0);d&&(d.checked=k===!0)}const a=e.float?.accent||"";Se.val(a.startsWith("#")?"__custom__":a),a.startsWith("#")&&Oe.val(a),fe(),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-brand").val(e.look?.brand||""),t("#me-look-strip").val(e.look?.strip||""),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-look-shrink").get(0).checked=e.look?.shrink===!0,t("#me-side").val(e.side||"left"),t("#me-dock-width").val(e.dock?.width??"");for(const i of["collapsible","collapsed","autohide","hideOnScroll"]){const k=t(`#me-dock-${i}`).get(0);k&&(k.checked=e.dock?.[i]===!0)}t("#me-class").val(e.class||""),t("#me-highlight-hover").val(e.highlight?.hover||""),t("#me-highlight-active").val(e.highlight?.active||"");const c=e.highlight?.accent||"";se.val(c.startsWith("#")?"__custom__":c),c.startsWith("#")&&W.val(c),B();{const i=t("#me-entrance-enabled").get(0);i&&(i.checked=e.entrance?.enabled===!0)}t("#me-entrance-animation").val(ot.includes(e.entrance?.animation)?e.entrance.animation:ot[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||""),ge.val(e.binding?.slot||"");const v=new Set(e.binding?.projects||[]);Array.from(Re.get(0).options).forEach(i=>{i.selected=v.has(i.value)}),Ee.val((e.binding?.pages||[]).join(`
31
+ `)),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||X||""),Ve(),g(),ke()}const A=e=>!!(t(e).get(0)||{}).checked;function le(){const e={variant:t("#me-variant").val(),position:t("#me-position").val(),appearOnHover:A("#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:tt(),floatOpacity:t("#me-float-opacity").val(),floatTitle:t("#me-float-title").val(),floatCollapsible:A("#me-float-collapsible"),floatCollapsed:A("#me-float-collapsed"),floatMovable:A("#me-float-movable"),floatResizable:A("#me-float-resizable"),highlightHover:t("#me-highlight-hover").val(),highlightActive:t("#me-highlight-active").val(),highlightAccent:Ge(),class:t("#me-class").val(),entranceEnabled:A("#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:A("#me-dock-collapsible"),dockCollapsed:A("#me-dock-collapsed"),dockAutohide:A("#me-dock-autohide"),mobileHamburgerOnly:A("#me-mobile-hamburger"),mobileHideBrand:A("#me-mobile-brand"),lookBar:t("#me-look-bar").val(),lookDropdown:t("#me-look-dropdown").val(),lookBrand:t("#me-look-brand").val(),lookStrip:t("#me-look-strip").val(),lookTiles:t("#me-look-tiles").val(),lookMorph:A("#me-look-morph"),lookShrink:A("#me-look-shrink"),dockHideOnScroll:A("#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 Gt(e,o,Be(),Dt(h),f.meta||{})}const Ae=e=>[...Vt(e),...ft(e.class).map(o=>`CSS classes: ${o} (Behaviour tab).`)];let Ce=!1,Q=null;function Z(){if(!U)return;const e=le();Q=e,j.value=Je(e)!==U,Fe.value=j.peek()||Ce?Ae(e).join(" "):""}const Ye=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")&&Ve(),e.target.matches("#me-float-accent")&&fe(),e.target.matches("#me-highlight-accent")&&B(),e.target.closest("#me-binding")&&g(),Z())};s.addEventListener("input",Ye),s.addEventListener("change",Ye),l(()=>{s.removeEventListener("input",Ye),s.removeEventListener("change",Ye)}),t("#me-bind-page-add").on("click",()=>{const e=t("#me-bind-page-pick").val();if(!e)return;const o=Ee.val().split(`
32
+ `).map(a=>a.trim()).filter(Boolean);o.includes(e)||o.push(e),Ee.val(o.join(`
33
+ `)),g(),Z()}),J(f),U=Je(le()),Z();const it=()=>{try{S.remove?S.remove(V):S.set(V,null)}catch{}};async function nt(){if(ue.peek())return;const e=le(),o=Ae(e);if(o.length){Ce=!0,Z(),E.toast(o.join(" "),{type:"warning",duration:7e3});return}ue.value=!0;let a=null;try{if($)await P.menus.create(e);else if(e.slug!==f.slug){await P.menus.create(e);const c=await P.menuLocations.get();let v=!1;for(const[i,k]of Object.entries(c))k===f.slug&&(c[i]=e.slug,v=!0);v&&await P.menuLocations.save(c),await P.menus.remove(f.slug)}else{const c=await wt(e);if(!c)return;a=c.revision}if(U=Je(e),j.value=!1,it(),Mt("#/menus"),E.toast("Saved.",{type:"success"}),$||e.slug!==f.slug)location.hash="#/menus/edit/"+encodeURIComponent(e.slug);else{f={...f,...e,revision:a};for(const c of h)c.tool&&c.touched&&(c.tool={...c.tool,persisted:!0});ke(),U=Je(le()),Z(),kt(n.get(0),e.name||e.slug)}if(D)try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}catch(c){E.toast(`Save failed: ${c.message||c}`,{type:"error"})}finally{ue.value=!1}}async function wt(e){try{return await P.menus.update(f.slug,{...e,revision:f.revision})}catch(a){if(a.status!==409)throw a}if(await E.confirm(`The menu "${De(f.name||f.slug)}" was changed somewhere else after you opened it - in another tab, by someone else, or on the server. Saving now would undo those changes.<br><br><strong>OK</strong> saves yours over them. <strong>Cancel</strong> loads the latest version, and offers your changes back as a draft.`))return P.menus.update(f.slug,e);try{S.set(V,{menu:e,base:U,at:new Date().toISOString()})}catch{}return j.value=!1,location.reload(),null}async function St(){!j.peek()||!await E.confirm("Throw away the changes you have not saved?")||(J(f),Z())}E.tabs(t("#me-tabs").get(0));const xt=M.applyBindings({dirty:j,saving:ue,problems:Fe,itemCount:Te},s,{methods:{save:()=>nt(),discard:()=>St(),addItem:()=>C("item"),addSeparator:()=>C("separator"),addSpacer:()=>C("spacer"),addSwitcher:()=>C("theme-switcher")}});l(()=>xt.dispose()),l(st(s,{position:"bottom"})),D&&t("#me-add-switcher").hide(),kt(n.get(0),$?"New menu":f.name||f.slug),at(n,{id:"menu-editor-back",icon:"arrow-left",title:"All menus",onClick:()=>{location.hash="#/menus"}}),at(n,{id:"menu-editor-locations",icon:"map-pin",title:"Menu locations",onClick:()=>{location.hash="#/menu-locations"}}),at(n,{id:"menu-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>nt()});const rt=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}nt()}};document.addEventListener("keydown",rt,!0),l(()=>document.removeEventListener("keydown",rt,!0));const ct=()=>{if(!(!j.peek()||!Q))try{S.set(V,{menu:Q,base:U,at:new Date().toISOString()})}catch{}},dt=e=>{j.peek()&&(ct(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",dt),l(()=>window.removeEventListener("beforeunload",dt)),q={path:T,dirty:()=>j.peek(),name:()=>t("#me-name").val()||f.name||f.slug||"new menu",dispose(){r=!0,ct(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(s);let ie=null;try{ie=S.get(V)}catch{}if(ie?.menu){it();const e=ie.base&&ie.base!==U,o=ie.at?` (${Jt(ie.at)})`:"",a=$?"the new menu you started":`the menu "${De(f.name||f.slug)}"`,c=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(c)&&y()&&(J({...ie.menu,meta:{...f.meta||{},...ie.menu.meta||{}}}),Z())}}};
@@ -1,4 +1,4 @@
1
- import{api as g}from"../api.js";import{addBannerAction as N}from"../lib/plugin-chrome.js";import{helpPopovers as ee}from"../lib/help-popover.js";import{openMenuAt as V,sidebarCount as te}from"../lib/tool-kit.js";import{depthOfItems as ne,locationRows as ae,locationsPayload as se,locationsSnapshot as w}from"../lib/menu-editor-arrange.js";import{fmtDateTime as oe}from"../lib/dates.js";const K=/^\/menu-locations\/?(\?.*)?$/,v="menu-locations.draft",le=`
1
+ import{api as y}from"../api.js";import{addBannerAction as N}from"../lib/plugin-chrome.js";import{helpPopovers as ee}from"../lib/help-popover.js";import{openMenuAt as V,sidebarCount as te}from"../lib/tool-kit.js";import{depthOfItems as ne,locationRows as ae,locationsPayload as se,locationsSnapshot as w}from"../lib/menu-editor-arrange.js";import{fmtDateTime as oe}from"../lib/dates.js";const K=/^\/menu-locations\/?(\?.*)?$/,h="menu-locations.draft",le=`
2
2
  <div class="mlx-row" data-slot="{{slot}}" tabindex="0" data-bind-class="(changed ? 'is-changed ' : '') + (tooDeep || missing ? 'is-bad' : '')">
3
3
  <span class="mlx-main">
4
4
  <strong>{{label}}</strong>
@@ -12,5 +12,6 @@ import{api as g}from"../api.js";import{addBannerAction as N}from"../lib/plugin-c
12
12
  <span class="mlx-chip is-changed" data-bind-hidden="!changed">Changed</span>
13
13
  </span>
14
14
  <select class="form-input mlx-select" aria-label="Menu for {{label}}"></select>
15
+ <span data-help-title="Menu for this location" data-help="Choose the menu shown in this slot across the site, then Save to apply the mapping. None clears the mapping; the slot description says what happens when empty. A menu with its own page or project binding can override this default on matching pages. This changes placement only, not the menu items. Edit the selected menu to change its links or appearance."></span>
15
16
  <button type="button" class="mlx-kebab" aria-label="Actions for {{label}}" title="Actions">\u22EE</button>
16
- </div>`,ie=o=>String(o??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");let u=null,W=!1;function re(){W||!window.R?.use||(W=!0,R.use((o,p,s)=>!(K.test(p?.path||"")&&!K.test(o?.path||""))||!u?.dirty()?s():E.confirm("You have unsaved menu locations. Leave anyway? They are kept as a draft for your next visit.").then(d=>{d?s():history.replaceState(null,"","#/menu-locations")})),M.subscribe("router:afterChange",({to:o}={})=>{if(u&&!K.test(o?.path||"")){const p=u;u=null,p.dispose()}}))}export const menuLocationsView={templateUrl:"/admin/js/templates/menu-locations.html",async onMount(o){if(re(),u){const e=u;u=null,e.dispose()}const p=[],s=e=>(p.push(e),e);let k=!1;const d=o.get(0).querySelector(".mlx")||o.get(0),f=()=>!k&&d.isConnected,x=M.observable([]),L=M.observable([]),O=M.observable({}),l=M.observable({}),a=M.observable({}),C=M.observable(!0),$=M.observable(""),y=M.observable(!1),h=M.computed(()=>w(a.value)!==w(l.value)),b=M.observable([]);s(M.effect(()=>{b.value=ae(x.value,a.value,l.value,L.value,O.value)}));const H=M.computed(()=>{const e=b.value;return`${e.filter(n=>!n.empty).length} of ${e.length} slot${e.length===1?"":"s"} have a menu`});async function _(){try{const[e,t,n]=await Promise.all([g.menuLocations.registry(),g.menuLocations.get().catch(()=>({})),g.menus.list().catch(()=>[])]),r=await Promise.all((n||[]).map(c=>g.menus.get(c.slug).catch(()=>null)));if(!f())return;x.value=Array.isArray(e)?e:[],L.value=n||[],O.value=Object.fromEntries(r.filter(Boolean).map(c=>[c.slug,ne(c.items||[])])),l.value={...t||{}},a.value={...t||{}},$.value=""}catch(e){$.value=`Could not load the locations: ${e.message||e}`}finally{C.value=!1}}const A=(e,t)=>{a.value={...a.peek(),[e]:t||""}};async function D(){if(!(y.peek()||C.peek())){y.value=!0;try{const e=se(a.peek(),x.peek());if(await g.menuLocations.save(e),!f())return;l.value={...e},a.value={...e};try{S.remove?S.remove(v):S.set(v,null)}catch{}te("#/menus"),E.toast("Locations saved.",{type:"success"})}catch(e){E.toast(`Save failed: ${e.message||e}`,{type:"error"})}finally{y.value=!1}}}async function z(){!h.peek()||!await E.confirm("Throw away the changes you have not saved?")||(a.value={...l.peek()})}const i=d.querySelector(".mlx-list");i.innerHTML=le;const G=M.applyBindings({rows:b,loading:C,loadError:$,saving:y,dirty:h,summary:H},d,{methods:{save:()=>D(),discard:()=>z()}});s(()=>G.dispose()),s(ee(d,{position:"bottom"}));const J=()=>{const e=[["","- none -"],...L.peek().map(t=>[t.slug,t.name||t.slug])];for(const t of i.querySelectorAll(".mlx-select")){const n=t.closest(".mlx-row")?.dataset.slot;if(!t.options.length)for(const[c,Z]of e)t.add(new Option(Z,c));const r=a.peek()[n]||"";r&&![...t.options].some(c=>c.value===r)&&t.add(new Option(`${r} (missing)`,r)),t.value!==r&&(t.value=r)}};s(M.effect(()=>{b.value,requestAnimationFrame(()=>{f()&&J()})}));const P=e=>{e.target.matches(".mlx-select")&&A(e.target.closest(".mlx-row").dataset.slot,e.target.value)},T=e=>{const t=a.peek()[e];t&&(location.hash=`#/menus/edit/${encodeURIComponent(t)}`)},q=(e,t)=>navigator.clipboard?.writeText(e).then(()=>E.toast(`${t} copied.`,{type:"success"}),()=>E.toast(e,{type:"info",duration:8e3})),Q=e=>{const t=b.peek().find(n=>n.slot===e);return t?[{header:t.label},{label:t.menu?`Edit "${t.menuName}"`:"Edit its menu",icon:"edit",disabled:!t.menu||t.missing,action:()=>T(e)},{label:"Clear this slot",icon:"x-circle",disabled:!t.menu,action:()=>A(e,"")},{label:"Undo the change",icon:"rotate-ccw",disabled:!t.changed,action:()=>A(e,l.peek()[e]||"")},{type:"divider"},{label:"Copy slot name",icon:"copy",action:()=>q(e,"Slot name")},{label:"Copy [menu location] shortcode",icon:"code",action:()=>q(`[menu location="${e}" /]`,"Shortcode")}]:[]},X=E.contextMenu(i,{cursor:"glyph",match:".mlx-row",inherit:!1,density:"compact",items:e=>Q(e.dataset.slot)});s(()=>X?.destroy?.());const U=e=>{const t=e.target.closest(".mlx-row");if(!t)return;const n=e.target.closest(".mlx-kebab");if(n){V(n);return}e.target.closest("select")||T(t.dataset.slot)},Y=e=>{const t=e.target.closest(".mlx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),V(t)):e.key==="Enter"&&e.target===t&&T(t.dataset.slot))};i.addEventListener("change",P),i.addEventListener("click",U),i.addEventListener("keydown",Y),s(()=>{i.removeEventListener("change",P),i.removeEventListener("click",U),i.removeEventListener("keydown",Y)}),N(o,{id:"menu-locations-back",icon:"arrow-left",title:"All menus",onClick:()=>{location.hash="#/menus"}}),N(o,{id:"menu-locations-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>D()});const B=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),D())};document.addEventListener("keydown",B,!0),s(()=>document.removeEventListener("keydown",B,!0));const F=()=>{if(h.peek())try{S.set(v,{map:a.peek(),base:w(l.peek()),at:new Date().toISOString()})}catch{}},j=e=>{h.peek()&&(F(),e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",j),s(()=>window.removeEventListener("beforeunload",j)),u={dirty:()=>h.peek(),dispose(){k=!0,F(),p.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(d),await _(),!f())return;let m=null;try{m=S.get(v)}catch{}if(m?.map){try{S.remove?S.remove(v):S.set(v,null)}catch{}const e=m.base&&m.base!==w(l.peek()),t=m.at?` (${oe(m.at)})`:"",n=e?`You left unsaved menu locations${t}, but the saved locations have changed since. Restore your version anyway?`:`You left unsaved menu locations${t}. Restore them?`;await E.confirm(ie(n))&&f()&&(a.value={...l.peek(),...m.map})}}};
17
+ </div>`,ie=o=>String(o??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");let u=null,W=!1;function re(){W||!window.R?.use||(W=!0,R.use((o,p,s)=>!(K.test(p?.path||"")&&!K.test(o?.path||""))||!u?.dirty()?s():E.confirm("You have unsaved menu locations. Leave anyway? They are kept as a draft for your next visit.").then(d=>{d?s():history.replaceState(null,"","#/menu-locations")})),M.subscribe("router:afterChange",({to:o}={})=>{if(u&&!K.test(o?.path||"")){const p=u;u=null,p.dispose()}}))}export const menuLocationsView={templateUrl:"/admin/js/templates/menu-locations.html",async onMount(o){if(re(),u){const e=u;u=null,e.dispose()}const p=[],s=e=>(p.push(e),e);let k=!1;const d=o.get(0).querySelector(".mlx")||o.get(0),v=()=>!k&&d.isConnected,x=M.observable([]),C=M.observable([]),O=M.observable({}),l=M.observable({}),a=M.observable({}),L=M.observable(!0),A=M.observable(""),b=M.observable(!1),f=M.computed(()=>w(a.value)!==w(l.value)),g=M.observable([]);s(M.effect(()=>{g.value=ae(x.value,a.value,l.value,C.value,O.value)}));const H=M.computed(()=>{const e=g.value;return`${e.filter(n=>!n.empty).length} of ${e.length} slot${e.length===1?"":"s"} have a menu`});async function _(){try{const[e,t,n]=await Promise.all([y.menuLocations.registry(),y.menuLocations.get().catch(()=>({})),y.menus.list().catch(()=>[])]),r=await Promise.all((n||[]).map(c=>y.menus.get(c.slug).catch(()=>null)));if(!v())return;x.value=Array.isArray(e)?e:[],C.value=n||[],O.value=Object.fromEntries(r.filter(Boolean).map(c=>[c.slug,ne(c.items||[])])),l.value={...t||{}},a.value={...t||{}},A.value=""}catch(e){A.value=`Could not load the locations: ${e.message||e}`}finally{L.value=!1}}const $=(e,t)=>{a.value={...a.peek(),[e]:t||""}};async function D(){if(!(b.peek()||L.peek())){b.value=!0;try{const e=se(a.peek(),x.peek());if(await y.menuLocations.save(e),!v())return;l.value={...e},a.value={...e};try{S.remove?S.remove(h):S.set(h,null)}catch{}te("#/menus"),E.toast("Locations saved.",{type:"success"})}catch(e){E.toast(`Save failed: ${e.message||e}`,{type:"error"})}finally{b.value=!1}}}async function z(){!f.peek()||!await E.confirm("Throw away the changes you have not saved?")||(a.value={...l.peek()})}const i=d.querySelector(".mlx-list");i.innerHTML=le;const G=M.applyBindings({rows:g,loading:L,loadError:A,saving:b,dirty:f,summary:H},d,{methods:{save:()=>D(),discard:()=>z()}});s(()=>G.dispose()),s(ee(d,{position:"bottom"}));const J=()=>{const e=[["","- none -"],...C.peek().map(t=>[t.slug,t.name||t.slug])];for(const t of i.querySelectorAll(".mlx-select")){const n=t.closest(".mlx-row")?.dataset.slot;if(!t.options.length)for(const[c,Z]of e)t.add(new Option(Z,c));const r=a.peek()[n]||"";r&&![...t.options].some(c=>c.value===r)&&t.add(new Option(`${r} (missing)`,r)),t.value!==r&&(t.value=r)}};s(M.effect(()=>{g.value,requestAnimationFrame(()=>{v()&&J()})}));const P=e=>{e.target.matches(".mlx-select")&&$(e.target.closest(".mlx-row").dataset.slot,e.target.value)},T=e=>{const t=a.peek()[e];t&&(location.hash=`#/menus/edit/${encodeURIComponent(t)}`)},q=(e,t)=>navigator.clipboard?.writeText(e).then(()=>E.toast(`${t} copied.`,{type:"success"}),()=>E.toast(e,{type:"info",duration:8e3})),Q=e=>{const t=g.peek().find(n=>n.slot===e);return t?[{header:t.label},{label:t.menu?`Edit "${t.menuName}"`:"Edit its menu",icon:"edit",disabled:!t.menu||t.missing,action:()=>T(e)},{label:"Clear this slot",icon:"x-circle",disabled:!t.menu,action:()=>$(e,"")},{label:"Undo the change",icon:"rotate-ccw",disabled:!t.changed,action:()=>$(e,l.peek()[e]||"")},{type:"divider"},{label:"Copy slot name",icon:"copy",action:()=>q(e,"Slot name")},{label:"Copy [menu location] shortcode",icon:"code",action:()=>q(`[menu location="${e}" /]`,"Shortcode")}]:[]},X=E.contextMenu(i,{cursor:"glyph",match:".mlx-row",inherit:!1,density:"compact",items:e=>Q(e.dataset.slot)});s(()=>X?.destroy?.());const U=e=>{const t=e.target.closest(".mlx-row");if(!t)return;const n=e.target.closest(".mlx-kebab");if(n){V(n);return}e.target.closest("select")||T(t.dataset.slot)},Y=e=>{const t=e.target.closest(".mlx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),V(t)):e.key==="Enter"&&e.target===t&&T(t.dataset.slot))};i.addEventListener("change",P),i.addEventListener("click",U),i.addEventListener("keydown",Y),s(()=>{i.removeEventListener("change",P),i.removeEventListener("click",U),i.removeEventListener("keydown",Y)}),N(o,{id:"menu-locations-back",icon:"arrow-left",title:"All menus",onClick:()=>{location.hash="#/menus"}}),N(o,{id:"menu-locations-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>D()});const j=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),D())};document.addEventListener("keydown",j,!0),s(()=>document.removeEventListener("keydown",j,!0));const B=()=>{if(f.peek())try{S.set(h,{map:a.peek(),base:w(l.peek()),at:new Date().toISOString()})}catch{}},F=e=>{f.peek()&&(B(),e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",F),s(()=>window.removeEventListener("beforeunload",F)),u={dirty:()=>f.peek(),dispose(){k=!0,B(),p.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(d),await _(),!v())return;let m=null;try{m=S.get(h)}catch{}if(m?.map){try{S.remove?S.remove(h):S.set(h,null)}catch{}const e=m.base&&m.base!==w(l.peek()),t=m.at?` (${oe(m.at)})`:"",n=e?`You left unsaved menu locations${t}, but the saved locations have changed since. Restore your version anyway?`:`You left unsaved menu locations${t}. Restore them?`;await E.confirm(ie(n))&&v()&&(a.value={...l.peek(),...m.map})}}};
@@ -350,6 +350,7 @@ headings (Layouts → Page contents settings, and `[contents]` per page).
350
350
  - **Menus as data** - any number of menus in `config/menus/<slug>.json`, mapped to slots (`navbar`, `footer-primary`, `footer-legal`, plugin slots) in `config/menu-locations.json`. Items can be links, separators, spacers or the theme switcher, with icons, badges and per-item `visibility` (same ladder and `=role` as pages) or `hidden`.
351
351
  - Where: **Content > Menus**, **Menu locations**
352
352
  - **Drag to move and nest** - in the Menus editor, drag an item by its grip: a line shows where it will land (above or below a row), and an outline shows it will go inside a row (the middle half of the row). The item follows the pointer and the rows glide to their new places when you let go. Esc cancels. Works with a mouse, pen or finger. Built on Domma's `E.sortable` (domma-js 0.45.0); on an older Domma core the right-click menu, Alt+arrows and buttons still move items.
353
+ - **Safe saving** - the editor sends back the menu's `revision` (a fingerprint of the stored file from `GET /api/menus/:slug`); if the menu changed since it was opened - another tab, another person, a hand edit on the server - `PUT` answers 409 and the editor asks: overwrite, or load the latest with your changes kept as a draft. `menuRevision()` in `server/services/menus.js`.
353
354
  - **Bindings** - a menu can take over a slot only on chosen pages or projects.
354
355
  - **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
356
  - **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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.16.0",
3
+ "version": "1.16.2",
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",