domma-cms 0.52.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -0
- package/admin/index.html +4 -0
- package/admin/js/lib/sidebar-renderer.js +3 -3
- package/admin/js/templates/menu-editor.html +47 -0
- package/admin/js/views/menu-editor.js +14 -14
- package/package.json +1 -1
- package/public/css/menu-highlight.css +1 -0
- package/public/css/site.css +1 -1
- package/public/js/menu-decor.mjs +1 -1
- package/public/js/site.js +1 -1
- package/server/server.js +24 -1
- package/server/services/markdown.js +8 -4
- package/server/services/menuRender.js +38 -8
- package/server/services/menus.js +42 -0
- package/server/services/renderer.js +16 -3
- package/server/templates/page.html +4 -1
package/CLAUDE.md
CHANGED
|
@@ -152,6 +152,8 @@ Menus live in `config/menus/<slug>.json`. The slot → menu map lives in `config
|
|
|
152
152
|
|
|
153
153
|
**Bindings.** A menu can carry `binding: {slot, projects: [], pages: []}` and take that slot over on matching pages instead of being mapped globally. `resolveLocation(slot, user, {urlPath, project})` checks bindings first (exact page > `/glob/*` > project, longest pattern wins) and falls back to `menu-locations.json`; **callers that know the page must pass the ctx** or bindings silently never fire - `renderer.js` (both render paths) and `parseMarkdown({urlPath})` do. All menu files are cached in-process via `getAllMenus()`; anything writing a menu file outside `menus.js` must call `invalidateMenuIndex()` (the two boot migrations do).
|
|
154
154
|
|
|
155
|
+
**Item highlighting.** `highlight: {hover, active, accent}` on a menu picks the pointer effect and the current-page marker; the whole implementation is `public/css/menu-highlight.css` keyed off `data-menu-hover` / `data-menu-active` (+ inline `--dm-menu-highlight`), stamped by `applyHighlight` (client) and `buildMenuNav` (server). Unlike the float helpers, `highlightAttrs`/`isCurrentItem` live ONCE in `menu-decor.mjs` and the server imports them - do not duplicate them into `menus.js`. Marking the current item needs the page's URL: `buildMenuNav({currentPath})` (renderer + markdown pass it), `location.pathname` in site.js. The rules carry `!important` because Domma's `.navbar-dark .navbar-link:hover` and site.css's blanket `.page-body a` rule both outrank a class selector - and site.css's rule additionally excludes `:not([data-menu-hover] a):not([data-menu-active] a)`, so a highlighted `[menu]` in the body is not underlined at rest.
|
|
156
|
+
|
|
155
157
|
**Orientation.** `orientation: horizontal|vertical` + `side: left|right`. Vertical navbar = CSS rail over Domma's normal navbar markup (`#site-navbar.dm-nav-vertical` + server-stamped `body.dm-vnav-*` gutter, ≥993px only; below that the mobile drawer already applies). The `[menu]` shortcode emits `dm-menu--vertical|horizontal` **only when set** - no class means the historic plain-list rendering.
|
|
156
158
|
|
|
157
159
|
**Floating panel surface.** `float` also takes `radius`, `shadow` (`none|sm|md|lg|xl` → `--dm-shadow-*`), `accent` (preset key or hex → border tint + leading-edge stripe via `--dm-menu-accent`) and `opacity` (20-100 → `--dm-menu-panel-opacity`, blurred translucent surface). `floatToCss` exists twice - `server/services/menus.js` and `public/js/menu-decor.mjs` - and `tests/services/menus-float.test.js` asserts the copies agree.
|
package/admin/index.html
CHANGED
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
|
|
22
22
|
<!-- Forms CSS - needed for conditional logic preview (.fb-field-hidden, etc.) -->
|
|
23
23
|
<link rel="stylesheet" href="/public/css/forms.css?v=20260817-multi-chooser">
|
|
24
|
+
|
|
25
|
+
<!-- Menu item hover / current-page effects - the admin sidebar is a menu
|
|
26
|
+
like any other, so it honours the same `highlight` block. -->
|
|
27
|
+
<link rel="stylesheet" href="/public/css/menu-highlight.css?v=20260905-menu-highlight">
|
|
24
28
|
</head>
|
|
25
29
|
<body class="dm-cloaked dm-theme-charcoal-dark dashboard-layout">
|
|
26
30
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as c}from"../api.js";import{colourToCss as R,applyFloat as
|
|
2
|
-
<summary${
|
|
1
|
+
import{api as c}from"../api.js";import{colourToCss as R,applyFloat as K,applyHighlight as G}from"/public/js/menu-decor.mjs?v=20260905-menu-highlight";import{groupPluginItems as O,stripItemByUrl as V,insertFoldersBeforeSystem as Y,pruneEmptySynthesisedFolders as J,MANAGE_PLUGINS_URL as Q}from"./sidebar-grouping.js";function h(e){return String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}const Z=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu"}];function B(e,a){return a?!e||!e.length?!1:e.includes(a)?!0:["read","create","update","delete"].some(t=>e.includes(`${a}.${t}`)):!0}function I(e,a){const t=[];for(const n of e){if(n.hidden||n.permission&&!B(a,n.permission))continue;const i=Array.isArray(n.items)&&n.items.length?I(n.items,a):[];t.push({...n,items:i})}return t}function ee(e,a){if(!a||!a.length)return e;const t=(n,i)=>{for(const d of n){if((d.text||"").toLowerCase()===i.toLowerCase())return d;if(Array.isArray(d.items)){const r=t(d.items,i);if(r)return r}}return null};for(const n of a)if(n.parent){const i=t(e,n.parent);i?(i.items=Array.isArray(i.items)?i.items:[],i.items.push(n.item)):e.push(n.item)}else e.push(n.item);return e}async function te(e){if(!B(e,"projects"))return[];let a=[];try{a=await c.projects.list()}catch{return[]}const t={};await Promise.all(a.map(async i=>{try{const d=await c.projects.artefacts(i.slug);t[i.slug]=d}catch{t[i.slug]={}}}));const n=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return a.map(i=>{const r="#/projects/"+encodeURIComponent(i.slug),m=t[i.slug]||{},f=[{text:"Overview",url:r,icon:i.icon||"folder"}];for(const p of n){const y=m[p.key],g=Array.isArray(y)?y.length:0;g<=0||f.push({text:`${p.text} (${g})`,url:r+p.path,icon:p.icon})}f.push({text:"Settings",url:r+"/settings",icon:"settings"});const u=i.enabled===!1;return{text:i.name||i.slug,icon:i.icon||"folder",items:f,...u&&{colour:"neutral",badge:{text:"off"}}}})}function ne(e,a){for(const t of e)if((t.text||"").toLowerCase()==="projects"){t.items=Array.isArray(t.items)?t.items.slice():[],t.items.push(...a);return}}function se(e,a){if(!e.url||!a)return"";const t=a[e.url];return t==null||t<=0?"":`<span class="sidebar-badge">${h(String(t))}</span>`}function ie(e){const a=(e.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),t=(e.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${t?`project-${decodeURIComponent(t[1])}.`:""}${a}`}function N(e,a){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="spacer"){const u=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${h(u)}" aria-hidden="true"></div>`}const t=e.icon?`<span data-icon="${h(e.icon)}"></span>`:"",n=h(e.text||""),i=e.url?h(e.url):"",d=Array.isArray(e.items)&&e.items.length>0,r=e.colour?R(e.colour):"",m=r?` style="color:${h(r)}"`:"";let f="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const u=e.badge.variant?R(e.badge.variant):"";f=`<span class="dm-menu-badge"${u?` style="background:${h(u)};color:var(--dm-text-inverse,#fff)"`:""}>${h(String(e.badge.text))}</span>`}if(d){const u=ie(e),p=S.get(u)!==!1,y=e.items.map(g=>N(g,a)).join("");return`<details data-state-key="${h(u)}"${p?" open":""}>
|
|
2
|
+
<summary${m}>${t} <span class="sidebar-text">${n}</span>${f}</summary>
|
|
3
3
|
<div class="sidebar-children">${y}</div>
|
|
4
|
-
</details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${
|
|
4
|
+
</details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${m}>${t} <span class="sidebar-text">${n}</span>${se(e,a)}${f}</a>`}async function ae(){const e=g=>g.then(b=>Array.isArray(b)?b.length:Array.isArray(b?.entries)?b.entries.length:0).catch(()=>0),[a,t,n,i,d,r,m,f,u,p,y]=await Promise.all([e(c.pages.list()),e(c.media.list()),e(c.collections.list()),e(c.forms.list()),e(c.views.list()),e(c.actions.list()),e(c.blocks.list()),e(c.components.list()),e(c.users.list()),e(c.plugins.list()),c.system?.notifications?.unreadCount?.().then(g=>g?.count??0).catch(()=>0)??Promise.resolve(0)]);return{"#/pages":a,"#/media":t,"#/collections":n,"#/forms":i,"#/views":d,"#/actions":r,"#/blocks":m,"#/components":f,"#/users":u,"#/plugins":p,"#/system/notifications":y}}async function re(){try{const e=await c.settings.get();return e?.adminBrand?.title||e?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:a}){const t=$(e).get(0);if(!t)return;let n,i=null,d=null,r=null,m=null,f=null;try{const s=await c.menus.get("admin-sidebar");n=Array.isArray(s?.items)?s.items:null,i=s?.variant||null,d=s?.position||null,r=s?.style||null,m=s?.float||null,f=s?.highlight||null}catch{n=null}(!n||!n.length)&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),n=Z.slice());let u=[];try{u=await H.get("/api/sidebar/registered-items")||[]}catch{}n=V(n,Q);const{toolsFolder:p,pluginsFolder:y,parented:g}=O(u);n=ee(n,g),n=Y(n,[p,y]);const b=await te(a);b.length&&ne(n,b),n=I(n,a),n=J(n);const[T,U]=await Promise.all([ae().catch(()=>({})),re()]),q=i?` dm-admin-sidebar--${h(i)}`:"";function j(s,l="px"){if(s==null)return s;const o=String(s).trim();return/^\d+(\.\d+)?$/.test(o)?`${o}${l}`:o}let E="";if(r){const s=[];r.fontFamily&&s.push(`font-family: ${r.fontFamily}, sans-serif`),r.fontSize&&s.push(`font-size: ${j(r.fontSize)}`),r.fontWeight&&s.push(`font-weight: ${r.fontWeight}`),r.letterSpacing&&s.push(`letter-spacing: ${j(r.letterSpacing,"em")}`);const l=[];if(s.length&&l.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${s.join("; ")} }`),r.iconSize){const o=j(r.iconSize);l.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${o} !important; height: ${o} !important; }`)}l.length&&(E=`<style data-admin-sidebar-style>${l.join(" ")}</style>`)}const X=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${h(U)}</div>`,W=n.some(s=>s&&s.type==="spacer"&&s.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",D=`${E}<nav class="dm-admin-sidebar${q}${W}">${X}${n.map(s=>N(s,T)).join("")}</nav>`,L=document.createRange();L.selectNodeContents(t);const _=L.createContextualFragment(D);t.replaceChildren(_),Domma.icons.scan(t);const v=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(v)&&v>=180&&v<=480&&(t.style.width=v+"px"),t.removeAttribute("data-menu-hover"),t.removeAttribute("data-menu-active"),t.style.removeProperty("--dm-menu-highlight"),G(t,{highlight:f});const x=document.querySelector(".dashboard-wrapper");if(["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(s=>{t.style[s]=""}),t.classList.remove("dm-menu-floating"),x&&(x.style.paddingLeft="",x.style.paddingRight=""),d==="floating"){K(t,{position:"floating",float:m||{}});const l=m&&m.anchor||"TL",o=Number.isFinite(Number(m?.offsetX))?Number(m.offsetX):16,A=t.getBoundingClientRect().width||v||260;if(x){const C=Math.round(A+o*2)+"px";l==="R"||l==="TR"||l==="BR"?x.style.paddingRight=C:x.style.paddingLeft=C}}const w=document.createElement("div");w.className="dm-admin-sidebar-handle",w.setAttribute("aria-label","Resize sidebar"),t.appendChild(w),t.querySelectorAll("details").forEach(s=>{s.addEventListener("toggle",function(){const l=this.getAttribute("data-state-key");l&&S.set(l,this.open);const o=this.querySelector(":scope > .sidebar-children");if(o)if(this.open){const A=o.scrollHeight;o.style.maxHeight="0",requestAnimationFrame(()=>{o.style.maxHeight=A+"px",o.addEventListener("transitionend",function C(){o.style.maxHeight="none",o.removeEventListener("transitionend",C)})})}else{const A=o.scrollHeight;o.style.maxHeight=A+"px",requestAnimationFrame(()=>{o.style.maxHeight="0"})}})});let k=!1,P=0,F=0;w.addEventListener("mousedown",s=>{k=!0,P=s.clientX,F=t.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",s.preventDefault()}),document.addEventListener("mousemove",s=>{if(!k)return;const l=Math.max(180,Math.min(480,F+(s.clientX-P)));t.style.width=l+"px"}),document.addEventListener("mouseup",()=>{k&&(k=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(t.style.width,10)))});function z(){const s=location.hash||"#/";$(t).find(".sidebar-link").removeClass("active"),$(t).find(`.sidebar-link[data-url="${s}"]`).addClass("active")}z(),M.subscribe("router:afterChange",z)}
|
|
@@ -159,6 +159,53 @@
|
|
|
159
159
|
Reveal dropdowns on hover (desktop navbar; click still works)
|
|
160
160
|
</label>
|
|
161
161
|
</div>
|
|
162
|
+
<div class="form-group">
|
|
163
|
+
<label for="me-highlight-hover">Hover effect</label>
|
|
164
|
+
<select id="me-highlight-hover" name="highlightHover" class="form-input">
|
|
165
|
+
<option value="">- theme default -</option>
|
|
166
|
+
<option value="none">None - no change on hover</option>
|
|
167
|
+
<option value="colour">Colour change</option>
|
|
168
|
+
<option value="underline">Underline</option>
|
|
169
|
+
<option value="overline">Overline</option>
|
|
170
|
+
<option value="background">Background tint</option>
|
|
171
|
+
<option value="uppercase">Uppercase</option>
|
|
172
|
+
<option value="grow">Grow</option>
|
|
173
|
+
<option value="breathe">Breathe</option>
|
|
174
|
+
<option value="glow">Glow</option>
|
|
175
|
+
</select>
|
|
176
|
+
<small class="form-hint">What an item does under the pointer. Applies wherever this menu is drawn - navbar, footer, <code>[menu]</code> and the admin sidebar.</small>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="form-group">
|
|
179
|
+
<label for="me-highlight-active">Current page</label>
|
|
180
|
+
<select id="me-highlight-active" name="highlightActive" class="form-input">
|
|
181
|
+
<option value="">- theme default -</option>
|
|
182
|
+
<option value="none">None - not marked at all</option>
|
|
183
|
+
<option value="bold">Bold</option>
|
|
184
|
+
<option value="underline">Underline</option>
|
|
185
|
+
<option value="overline">Overline</option>
|
|
186
|
+
<option value="background">Background tint</option>
|
|
187
|
+
<option value="darken">Darkened</option>
|
|
188
|
+
<option value="pill">Filled pill</option>
|
|
189
|
+
</select>
|
|
190
|
+
<small class="form-hint">How the item pointing at the page you are on is marked out.</small>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="form-group">
|
|
193
|
+
<label for="me-highlight-accent">Highlight colour</label>
|
|
194
|
+
<div class="me-colour-control">
|
|
195
|
+
<select id="me-highlight-accent" name="highlightAccent" class="form-input">
|
|
196
|
+
<option value="">- theme primary -</option>
|
|
197
|
+
<option value="primary">primary</option>
|
|
198
|
+
<option value="success">success</option>
|
|
199
|
+
<option value="danger">danger</option>
|
|
200
|
+
<option value="warning">warning</option>
|
|
201
|
+
<option value="info">info</option>
|
|
202
|
+
<option value="neutral">neutral</option>
|
|
203
|
+
<option value="__custom__">Custom…</option>
|
|
204
|
+
</select>
|
|
205
|
+
<input type="color" id="me-highlight-accent-hex" class="form-input me-colour-hex" value="#000000" style="display:none">
|
|
206
|
+
</div>
|
|
207
|
+
<small class="form-hint">Used by the colour, underline, overline, tint, glow and pill effects. The underline and overline effects fall back to the item's own text colour when this is unset.</small>
|
|
208
|
+
</div>
|
|
162
209
|
<div class="form-group">
|
|
163
210
|
<label for="me-fontFamily">Font family</label>
|
|
164
211
|
<input type="text" id="me-fontFamily" name="fontFamily" class="form-input" placeholder="Inter">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
<div class="menu-tree-row menu-tree-row--separator" data-id="${
|
|
1
|
+
import{api as _}from"../api.js";import{colourToCss as bt}from"/public/js/menu-decor.mjs?v=20260905-menu-highlight";import{openIconPicker as gt}from"../lib/card-builder.js";import{applyDrop as vt}from"../lib/menu-tree.js";let yt=1;const D=()=>"i_"+yt++,wt=760;function lt(t,d=null,s=[]){for(const i of t||[]){const n=D();if(i&&i.type==="separator"){s.push({id:n,parentId:d,type:"separator"});continue}if(i&&i.type==="spacer"){s.push({id:n,parentId:d,type:"spacer",size:i.size||"md"});continue}s.push({id:n,parentId:d,text:i.text||"",url:i.url||"",icon:i.icon||"",visibility:i.visibility||"",permission:i.permission||"",hidden:!!i.hidden,bundled:!!i.bundled,badge:i.badge?{...i.badge}:null,pill:i.pill?{...i.pill}:null,colour:i.colour||""}),Array.isArray(i.items)&&i.items.length&<(i.items,n,s)}return s}function xt(t){const d=new Map;for(const i of t)d.has(i.parentId)||d.set(i.parentId,[]),d.get(i.parentId).push(i);function s(i){return(d.get(i)||[]).map(n=>{if(n.type==="separator")return{type:"separator"};if(n.type==="spacer")return{type:"spacer",...n.size&&n.size!=="md"&&{size:n.size}};const o=s(n.id);return{text:(n.text||"").trim(),url:(n.url||"").trim(),...n.icon&&{icon:n.icon.trim()},...n.visibility&&{visibility:n.visibility},...n.permission&&{permission:n.permission},...n.hidden&&{hidden:!0},...n.bundled&&{bundled:!0},...n.badge&&(n.badge.text||n.badge.countFrom)?{badge:{...n.badge.text&&{text:n.badge.text},...n.badge.countFrom&&{countFrom:n.badge.countFrom},...n.badge.variant&&{variant:n.badge.variant}}}:{},...n.pill&&n.pill.variant?{pill:{style:n.pill.style||"filled",variant:n.pill.variant}}:{},...n.colour&&{colour:n.colour},...o.length&&{items:o}}})}return s(null)}function h(t){return String(t||"").replace(/&/g,"&").replace(/"/g,""").replace(/</g,"<")}function kt(t,d){let s=0,i=d.find(n=>n.id===t);for(;i&&i.parentId;)s++,i=d.find(n=>n.id===i.parentId);return s}function dt(t,d){const{top:s,height:i}=t.getBoundingClientRect(),n=i?(d-s)/i:.5;return n<.25?"above":n>.75?"below":"into"}const ct=["me-drop-above","me-drop-below","me-drop-into"];function tt(t){t.get(0).querySelectorAll(".menu-tree-row").forEach(d=>d.classList.remove(...ct))}function _t(t,d,s){let i=null;t.get(0).querySelectorAll(".menu-tree-row").forEach(n=>{const o=n.querySelector(".menu-tree-grip");n.draggable=!1,o&&(o.addEventListener("mousedown",()=>{n.draggable=!0}),o.addEventListener("mouseup",()=>{n.draggable=!1})),n.addEventListener("dragstart",r=>{i=n.dataset.id,n.style.opacity="0.45",r.dataTransfer.effectAllowed="move";try{r.dataTransfer.setData("text/plain",i)}catch{}}),n.addEventListener("dragend",()=>{n.style.opacity="",n.draggable=!1,i=null,tt(t)}),n.addEventListener("dragover",r=>{if(!i||i===n.dataset.id)return;r.preventDefault(),r.dataTransfer.dropEffect="move";const y=dt(n,r.clientY);tt(t),n.classList.add(`me-drop-${y}`)}),n.addEventListener("dragleave",()=>n.classList.remove(...ct)),n.addEventListener("drop",r=>{r.preventDefault();const y=n.dataset.id,x=dt(n,r.clientY);if(tt(t),!i)return;const v=vt(d(),i,y,x);i=null,v&&s()})})}function At(t,d,s=()=>""){t.html("");function i(n){for(const o of d.filter(r=>r.parentId===n)){const r=kt(o.id,d);if(o.type==="separator"){t.append(`
|
|
2
|
+
<div class="menu-tree-row menu-tree-row--separator" data-id="${o.id}" style="margin-left:${r*28}px">
|
|
3
3
|
<span class="menu-tree-grip" data-icon="move"></span>
|
|
4
4
|
<span class="me-sep-line"><span class="me-sep-label">Separator</span></span>
|
|
5
5
|
<button class="btn btn-sm btn-ghost me-ins-item" data-tooltip="Add item below"><span data-icon="plus"></span></button>
|
|
@@ -10,8 +10,8 @@ import{api as S}from"../api.js";import{colourToCss as ct}from"/public/js/menu-de
|
|
|
10
10
|
<button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
|
|
11
11
|
<button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
|
|
12
12
|
</div>
|
|
13
|
-
`);continue}if(
|
|
14
|
-
<div class="menu-tree-row menu-tree-row--spacer" data-id="${
|
|
13
|
+
`);continue}if(o.type==="spacer"){const P=["xs","sm","md","lg","xl","flex"].map(z=>`<option value="${z}"${(o.size||"md")===z?" selected":""}>${z==="flex"?"flexible":z}</option>`).join("");t.append(`
|
|
14
|
+
<div class="menu-tree-row menu-tree-row--spacer" data-id="${o.id}" style="margin-left:${r*28}px">
|
|
15
15
|
<span class="menu-tree-grip" data-icon="move"></span>
|
|
16
16
|
<span class="me-spacer-line">
|
|
17
17
|
<span class="me-spacer-label">Spacer</span>
|
|
@@ -25,23 +25,23 @@ import{api as S}from"../api.js";import{colourToCss as ct}from"/public/js/menu-de
|
|
|
25
25
|
<button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
|
|
26
26
|
<button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
|
|
27
27
|
</div>
|
|
28
|
-
`);continue}const
|
|
29
|
-
<div class="menu-tree-row menu-tree-row--slim${
|
|
28
|
+
`);continue}const y=o.badge&&(o.badge.text||o.badge.countFrom)?`<span class="dm-menu-badge">${h(String(o.badge.text||"#"))}</span>`:"",x=bt(o.colour),v=x?` style="color:${h(x)}"`:"";t.append(`
|
|
29
|
+
<div class="menu-tree-row menu-tree-row--slim${o.hidden?" menu-tree-row--hidden":""}" data-id="${o.id}" style="margin-left:${r*28}px">
|
|
30
30
|
<span class="menu-tree-grip" data-icon="move"></span>
|
|
31
|
-
<span class="me-row-label"${
|
|
32
|
-
<span class="me-row-url">${
|
|
31
|
+
<span class="me-row-label"${v}>${h(o.text||"(untitled)")}${y}</span>
|
|
32
|
+
<span class="me-row-url">${h(o.url||"")}</span>
|
|
33
33
|
<button class="btn btn-sm btn-ghost me-ins-item" data-tooltip="Add item below"><span data-icon="plus"></span></button>
|
|
34
34
|
<button class="btn btn-sm btn-ghost me-ins-sep" data-tooltip="Add separator below"><span data-icon="minus"></span></button>
|
|
35
35
|
<button class="btn btn-sm btn-ghost me-outdent" data-tooltip="Outdent"><span data-icon="chevron-left"></span></button>
|
|
36
36
|
<button class="btn btn-sm btn-ghost me-indent" data-tooltip="Indent"><span data-icon="chevron-right"></span></button>
|
|
37
37
|
<button class="btn btn-sm btn-ghost me-up" data-tooltip="Move up"><span data-icon="arrow-up"></span></button>
|
|
38
38
|
<button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
|
|
39
|
-
<button class="btn btn-sm btn-ghost me-hidden ${
|
|
39
|
+
<button class="btn btn-sm btn-ghost me-hidden ${o.hidden?"active":""}" data-tooltip="${o.hidden?"Show":"Hide"}"><span data-icon="eye-off"></span></button>
|
|
40
40
|
<button class="btn btn-sm btn-secondary me-edit" data-tooltip="Edit"><span data-icon="edit"></span></button>
|
|
41
41
|
<button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
|
|
42
42
|
</div>
|
|
43
|
-
`),o(i.id)}}o(null),Domma.icons.scan(t.get(0)),t.get(0).querySelectorAll("[data-tooltip]").forEach(n=>{E.tooltip(n,{content:n.getAttribute("data-tooltip"),position:"top"})})}function j(t,d){t.find(".menu-tree-row").each(function(){const s=$(this).data("id"),o=d.find(y=>y.id===s);if(!o||o.type==="separator")return;if(o.type==="spacer"){const y=$(this).find(".me-spacer-size");y.length&&(o.size=y.val());return}const n=$(this).find(".me-text");n.length&&(o.text=n.val());const i=$(this).find(".me-url");i.length&&(o.url=i.val());const r=$(this).find(".me-icon");r.length&&(o.icon=r.val());const w=$(this).find(".me-vis");w.length&&(o.visibility=w.val());const k=$(this).find(".me-perm");k.length&&(o.permission=k.val())})}function _t(t,d){const s={};for(const o of d){const n=t.querySelector(`[name="${o}"]`);n&&(s[o]=n.type==="checkbox"?n.checked:n.value)}return s}function g(t,d={},...s){const o=document.createElement(t);Object.assign(o,d);for(const n of s)n!=null&&o.append(n);return o}function A(t,d,s){const o=g("label",{className:"form-field"},g("span",{className:"form-label",textContent:t}),d);return s&&o.append(g("small",{className:"form-hint",textContent:s,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),o}function ht(t,d){const s=[["","- Everyone -"],["public","Public - anyone, incl. logged-out"],...(d||[]).map(n=>[n.name,`${n.label||n.name} and above`]),["private","Private - super-admin only"]];t&&!s.some(([n])=>n===t)&&s.push([t,`${t} (custom)`]);const o=M(s,t||"");return o.name="visibility",o}function M(t,d){const s=g("select",{className:"form-select"});return t.forEach(([o,n])=>s.add(new Option(n,o))),s.value=d??"",s}function K(t,d){const s=typeof d=="string"&&d.startsWith("#"),o=M([["","- none -"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],s?"__custom__":d||""),n=g("input",{type:"color",className:"form-input me-colour-hex",value:s?d:"#000000"});n.style.display=s?"":"none",o.addEventListener("change",()=>{n.style.display=o.value==="__custom__"?"":"none"});const i=A(t,o);return i.append(n),i._read=()=>o.value==="__custom__"?n.value:o.value,i}function yt(t,d){const s=g("input",{className:"form-input me-icon",name:"icon",value:d||""}),o=g("span",{className:"me-icon-preview"});d&&o.setAttribute("data-icon",d);const n=g("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),i=r=>{r?o.setAttribute("data-icon",r):o.removeAttribute("data-icon"),Domma.icons.scan(o.parentNode||o)};return n.addEventListener("click",()=>rt(r=>{s.value=r,i(r)})),s.addEventListener("input",()=>i(s.value.trim())),A(t,g("div",{className:"me-icon-field"},s,n,o))}function Z(...t){return g("div",{className:"me-field-grid"},...t)}async function wt(t,d,s,o,n){const i=u=>g("h4",{className:"me-form-section",textContent:u}),r=A("Label",g("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),w=A("URL",g("input",{className:"form-input",name:"url",value:t.url||""}),"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items."),k=yt("Icon",t.icon||""),y=A("Visibility",ht(t.visibility||"",o),"Who can see this item, by role seniority. Picking a role shows it to that role and anyone more senior. Blank = everyone."),P=M([["","- no permission gate -"],...(d||[]).map(u=>[u.key,u.label||u.key])],t.permission||"");P.name="permission";const O=A("Permission",P,"Hide unless the viewer holds this specific permission. Independent of Visibility - both must pass."),J=A("Badge text",g("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),X=M([["","- none -"],...(s||[]).map(u=>[u.slug,u.title||u.slug])],t.badge?.countFrom||"");X.name="badgeCount";const V=A("Badge count from",X,"Show a live count of this collection\u2019s entries as the badge."),U=K("Badge colour",t.badge?.variant||""),l=M([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");l.name="renderAs";const x=A("Render as",l,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),v=A("Pill style",M([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));v.querySelector("select").name="pillStyle";const W=K("Pill colour",t.pill?.variant||""),R=g("div",{className:"me-pill-wrap"},Z(v,W));R.style.display=t.pill?"":"none",l.addEventListener("change",()=>{R.style.display=l.value==="pill"?"":"none"});const T=K("Text colour",t.colour||""),q=g("button",{className:"btn btn-primary",textContent:"Apply"}),z=g("div",{className:"me-item-form"},i("Link"),r,w,k,i("Access"),Z(y,O),i("Badge"),Z(J,U),V,i("Appearance"),x,R,T,q),_=E.slideover({title:"Edit menu item",size:"lg",position:"right"});_.element.appendChild(z),_.open();const B=_.element.closest(".dm-slideover")||_.element.querySelector(".dm-slideover")||_.element;B.style.setProperty("width",ft+"px","important"),B.style.setProperty("max-width","95vw","important"),Domma.icons.scan(z);const I=u=>z.querySelector(`[name="${u}"]`)?.value||"";q.addEventListener("click",()=>{t.text=I("text"),t.url=I("url"),t.icon=I("icon"),t.visibility=I("visibility"),t.permission=I("permission");const u=I("badgeText"),F=I("badgeCount"),N=U._read();if(t.badge=u||F?{...u&&{text:u},...F&&{countFrom:F},...N&&{variant:N}}:null,I("renderAs")==="pill"){const L=W._read();t.pill={style:I("pillStyle")||"filled",...L&&{variant:L}}}else t.pill=null;t.colour=T._read(),_.close(),n()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){const d=location.hash.match(/^#\/menus\/edit\/(.+)$/),s=d?decodeURIComponent(d[1]):null,o=!s,n=E.loader(t.get(0),{type:"dots"});let i,r,w,k,y,P,O;try{if(o)i={slug:"",name:"",description:"",items:[]};else try{i=await S.menus.get(s)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}r=await S.projects.list().catch(()=>[]),w=await H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),k=await S.collections.list().catch(()=>[]),y=await S.get("/collections/roles/entries?limit=100").catch(()=>null),P=await S.menuLocations.registry().catch(()=>[]),O=await S.pages.list().catch(()=>[])}finally{n.destroy()}const X=(Array.isArray(y)?y:y?.entries||[]).map(e=>({name:e.data?.name,label:e.data?.label,level:e.data?.level??99})).filter(e=>e.name).sort((e,a)=>a.level-e.level),V='<option value="">- no permission gate -</option>',U=e=>V+(w.resources||[]).map(a=>{const c=(e||"")===a.key?" selected":"";return`<option value="${a.key}"${c}>${a.label||a.key}</option>`}).join("");t.find("#me-title").text(o?"New menu":`Edit "${i.slug}"`);let l=et(i.items);const x=t.find("#me-items-list");function v(){vt(x,l,U),gt(x,()=>l,v)}v(),t.find("#me-add-item").on("click",()=>{j(x,l),l.push({id:D(),parentId:null,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}),v()}),t.find("#me-add-separator").on("click",()=>{j(x,l),l.push({id:D(),parentId:null,type:"separator"}),v()}),t.find("#me-add-spacer").on("click",()=>{j(x,l),l.push({id:D(),parentId:null,type:"spacer",size:"md"}),v()});function W(e){return function(){j(x,l);const a=$(this).closest(".menu-tree-row").data("id"),c=l.findIndex(h=>h.id===a);if(c===-1)return;const f=l[c],p={separator:{type:"separator"},spacer:{type:"spacer",size:"md"}}[e],m=p?{id:D(),parentId:f.parentId,...p}:{id:D(),parentId:f.parentId,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1};l.splice(c+1,0,m),v()}}t.off("click",".me-ins-item").on("click",".me-ins-item",W("item")),t.off("click",".me-ins-sep").on("click",".me-ins-sep",W("separator")),t.off("click",".me-remove").on("click",".me-remove",function(){const e=$(this).closest(".menu-tree-row").data("id");l=l.filter(a=>a.id!==e&&a.parentId!==e),v()}),t.off("click",".me-indent").on("click",".me-indent",function(){j(x,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(p=>p.id===e);if(!a)return;const c=l.filter(p=>p.parentId===a.parentId),f=c.findIndex(p=>p.id===e);f<=0||["separator","spacer"].includes(c[f-1].type)||(a.parentId=c[f-1].id,v())}),t.off("click",".me-outdent").on("click",".me-outdent",function(){j(x,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(f=>f.id===e);if(!a||a.parentId==null)return;const c=l.find(f=>f.id===a.parentId);a.parentId=c?c.parentId:null,v()}),t.off("click",".me-up").on("click",".me-up",function(){j(x,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.filter(m=>m.parentId===l.find(h=>h.id===e)?.parentId),c=a.findIndex(m=>m.id===e);if(c<=0)return;const f=l.indexOf(a[c]),p=l.indexOf(a[c-1]);[l[f],l[p]]=[l[p],l[f]],v()}),t.off("click",".me-down").on("click",".me-down",function(){j(x,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.filter(m=>m.parentId===l.find(h=>h.id===e)?.parentId),c=a.findIndex(m=>m.id===e);if(c===-1||c>=a.length-1)return;const f=l.indexOf(a[c]),p=l.indexOf(a[c+1]);[l[f],l[p]]=[l[p],l[f]],v()}),t.off("click",".me-hidden").on("click",".me-hidden",function(){j(x,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(c=>c.id===e);a&&(a.hidden=!a.hidden),v()}),t.off("click",".me-edit").on("click",".me-edit",function(){const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(c=>c.id===e);!a||["separator","spacer"].includes(a.type)||wt(a,w.resources||[],k,X,()=>v())}),E.tabs(t.find("#me-tabs").get(0)),t.find("#me-variant").val(i.variant||""),t.find("#me-position").val(i.position||"");const R=t.find("#me-appearOnHover").get(0);R&&(R.checked=i.appearOnHover===!0),t.find("#me-float-anchor").val(i.float?.anchor||"TL"),t.find("#me-float-offsetX").val(i.float?.offsetX??""),t.find("#me-float-offsetY").val(i.float?.offsetY??""),t.find("#me-float-width").val(i.float?.width??""),t.find("#me-float-radius").val(i.float?.radius??""),t.find("#me-float-shadow").val(i.float?.shadow||""),t.find("#me-float-opacity").val(i.float?.opacity??""),t.find("#me-float-title").val(i.float?.title||"");for(const[e,a]of[["#me-float-collapsible",i.float?.collapsible],["#me-float-collapsed",i.float?.collapsed],["#me-float-movable",i.float?.movable],["#me-float-resizable",i.float?.resizable]]){const c=t.find(e).get(0);c&&(c.checked=a===!0)}const T=i.float?.accent||"",q=T.startsWith("#"),z=t.find("#me-float-accent"),_=t.find("#me-float-accent-hex");z.val(q?"__custom__":T),q&&_.val(T);const B=()=>{_.get(0).style.display=z.val()==="__custom__"?"":"none"};B(),z.on("change",B);const I=()=>z.val()==="__custom__"?_.val():z.val(),u=()=>{const e=t.find("#me-position").val()==="floating";t.find(".dm-float-field").each(function(){this.style.display=e?"":"none"}),e&&B();const a=t.find("#me-orientation").val()==="vertical";t.find(".dm-vertical-field").each(function(){this.style.display=a?"":"none"});const c=!!(t.find("#me-float-collapsible").get(0)||{}).checked;t.find(".dm-float-collapsed-field").each(function(){this.style.display=e&&c?"":"none"})};t.find("#me-orientation").val(i.orientation||""),t.find("#me-side").val(i.side||"left"),u(),t.find("#me-position").on("change",u),t.find("#me-orientation").on("change",u),t.find("#me-float-collapsible").on("change",u),t.find("#me-fontFamily").val(i.style?.fontFamily||""),t.find("#me-fontSize").val(i.style?.fontSize||""),t.find("#me-fontWeight").val(i.style?.fontWeight||""),t.find("#me-letterSpacing").val(i.style?.letterSpacing||""),t.find("#me-iconSize").val(i.style?.iconSize||"");const F=t.find("#me-bind-slot"),N=t.find("#me-bind-projects"),L=t.find("#me-bind-pages");F.html('<option value="">- not bound -</option>'+(P||[]).map(e=>`<option value="${b(e.slot)}">${b(e.label||e.slot)} (${b(e.slot)})</option>`).join("")),N.html(r.map(e=>`<option value="${b(e.slug)}">${b(e.name||e.slug)}</option>`).join("")),F.val(i.binding?.slot||"");const it=new Set(i.binding?.projects||[]);Array.from(N.get(0).options).forEach(e=>{e.selected=it.has(e.value)}),L.val((i.binding?.pages||[]).join(`
|
|
44
|
-
`)),t.find("#me-bind-page-pick").html('<option value="">- pick a page -</option>'+
|
|
45
|
-
`).map(c=>c.trim()).filter(Boolean);a.includes(e)||a.push(e),
|
|
46
|
-
`)),
|
|
47
|
-
`).map(a=>a.trim()).filter(Boolean)}:null}function
|
|
43
|
+
`),i(o.id)}}i(null),Domma.icons.scan(t.get(0)),t.get(0).querySelectorAll("[data-tooltip]").forEach(n=>{E.tooltip(n,{content:n.getAttribute("data-tooltip"),position:"top"})})}function j(t,d){t.find(".menu-tree-row").each(function(){const s=$(this).data("id"),i=d.find(v=>v.id===s);if(!i||i.type==="separator")return;if(i.type==="spacer"){const v=$(this).find(".me-spacer-size");v.length&&(i.size=v.val());return}const n=$(this).find(".me-text");n.length&&(i.text=n.val());const o=$(this).find(".me-url");o.length&&(i.url=o.val());const r=$(this).find(".me-icon");r.length&&(i.icon=r.val());const y=$(this).find(".me-vis");y.length&&(i.visibility=y.val());const x=$(this).find(".me-perm");x.length&&(i.permission=x.val())})}function Lt(t,d){const s={};for(const i of d){const n=t.querySelector(`[name="${i}"]`);n&&(s[i]=n.type==="checkbox"?n.checked:n.value)}return s}function b(t,d={},...s){const i=document.createElement(t);Object.assign(i,d);for(const n of s)n!=null&&i.append(n);return i}function O(t,d,s){const i=b("label",{className:"form-field"},b("span",{className:"form-label",textContent:t}),d);return s&&i.append(b("small",{className:"form-hint",textContent:s,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),i}function St(t,d){const s=[["","- Everyone -"],["public","Public - anyone, incl. logged-out"],...(d||[]).map(n=>[n.name,`${n.label||n.name} and above`]),["private","Private - super-admin only"]];t&&!s.some(([n])=>n===t)&&s.push([t,`${t} (custom)`]);const i=W(s,t||"");return i.name="visibility",i}function W(t,d){const s=b("select",{className:"form-select"});return t.forEach(([i,n])=>s.add(new Option(n,i))),s.value=d??"",s}function et(t,d){const s=typeof d=="string"&&d.startsWith("#"),i=W([["","- none -"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],s?"__custom__":d||""),n=b("input",{type:"color",className:"form-input me-colour-hex",value:s?d:"#000000"});n.style.display=s?"":"none",i.addEventListener("change",()=>{n.style.display=i.value==="__custom__"?"":"none"});const o=O(t,i);return o.append(n),o._read=()=>i.value==="__custom__"?n.value:i.value,o}function It(t,d){const s=b("input",{className:"form-input me-icon",name:"icon",value:d||""}),i=b("span",{className:"me-icon-preview"});d&&i.setAttribute("data-icon",d);const n=b("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),o=r=>{r?i.setAttribute("data-icon",r):i.removeAttribute("data-icon"),Domma.icons.scan(i.parentNode||i)};return n.addEventListener("click",()=>gt(r=>{s.value=r,o(r)})),s.addEventListener("input",()=>o(s.value.trim())),O(t,b("div",{className:"me-icon-field"},s,n,i))}function nt(...t){return b("div",{className:"me-field-grid"},...t)}async function jt(t,d,s,i,n){const o=u=>b("h4",{className:"me-form-section",textContent:u}),r=O("Label",b("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),y=O("URL",b("input",{className:"form-input",name:"url",value:t.url||""}),"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items."),x=It("Icon",t.icon||""),v=O("Visibility",St(t.visibility||"",i),"Who can see this item, by role seniority. Picking a role shows it to that role and anyone more senior. Blank = everyone."),P=W([["","- no permission gate -"],...(d||[]).map(u=>[u.key,u.label||u.key])],t.permission||"");P.name="permission";const z=O("Permission",P,"Hide unless the viewer holds this specific permission. Independent of Visibility - both must pass."),it=O("Badge text",b("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),U=W([["","- none -"],...(s||[]).map(u=>[u.slug,u.title||u.slug])],t.badge?.countFrom||"");U.name="badgeCount";const J=O("Badge count from",U,"Show a live count of this collection\u2019s entries as the badge."),G=et("Badge colour",t.badge?.variant||""),l=W([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");l.name="renderAs";const w=O("Render as",l,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),g=O("Pill style",W([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));g.querySelector("select").name="pillStyle";const M=et("Pill colour",t.pill?.variant||""),R=b("div",{className:"me-pill-wrap"},nt(g,M));R.style.display=t.pill?"":"none",l.addEventListener("change",()=>{R.style.display=l.value==="pill"?"":"none"});const T=et("Text colour",t.colour||""),q=b("button",{className:"btn btn-primary",textContent:"Apply"}),C=b("div",{className:"me-item-form"},o("Link"),r,y,x,o("Access"),nt(v,z),o("Badge"),nt(it,G),J,o("Appearance"),w,R,T,q),S=E.slideover({title:"Edit menu item",size:"lg",position:"right"});S.element.appendChild(C),S.open();const B=S.element.closest(".dm-slideover")||S.element.querySelector(".dm-slideover")||S.element;B.style.setProperty("width",wt+"px","important"),B.style.setProperty("max-width","95vw","important"),Domma.icons.scan(C);const A=u=>C.querySelector(`[name="${u}"]`)?.value||"";q.addEventListener("click",()=>{t.text=A("text"),t.url=A("url"),t.icon=A("icon"),t.visibility=A("visibility"),t.permission=A("permission");const u=A("badgeText"),N=A("badgeCount"),Y=G._read();if(t.badge=u||N?{...u&&{text:u},...N&&{countFrom:N},...Y&&{variant:Y}}:null,A("renderAs")==="pill"){const L=M._read();t.pill={style:A("pillStyle")||"filled",...L&&{variant:L}}}else t.pill=null;t.colour=T._read(),S.close(),n()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){const d=location.hash.match(/^#\/menus\/edit\/(.+)$/),s=d?decodeURIComponent(d[1]):null,i=!s,n=E.loader(t.get(0),{type:"dots"});let o,r,y,x,v,P,z;try{if(i)o={slug:"",name:"",description:"",items:[]};else try{o=await _.menus.get(s)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}r=await _.projects.list().catch(()=>[]),y=await H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),x=await _.collections.list().catch(()=>[]),v=await _.get("/collections/roles/entries?limit=100").catch(()=>null),P=await _.menuLocations.registry().catch(()=>[]),z=await _.pages.list().catch(()=>[])}finally{n.destroy()}const U=(Array.isArray(v)?v:v?.entries||[]).map(e=>({name:e.data?.name,label:e.data?.label,level:e.data?.level??99})).filter(e=>e.name).sort((e,a)=>a.level-e.level),J='<option value="">- no permission gate -</option>',G=e=>J+(y.resources||[]).map(a=>{const c=(e||"")===a.key?" selected":"";return`<option value="${a.key}"${c}>${a.label||a.key}</option>`}).join("");t.find("#me-title").text(i?"New menu":`Edit "${o.slug}"`);let l=lt(o.items);const w=t.find("#me-items-list");function g(){At(w,l,G),_t(w,()=>l,g)}g(),t.find("#me-add-item").on("click",()=>{j(w,l),l.push({id:D(),parentId:null,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}),g()}),t.find("#me-add-separator").on("click",()=>{j(w,l),l.push({id:D(),parentId:null,type:"separator"}),g()}),t.find("#me-add-spacer").on("click",()=>{j(w,l),l.push({id:D(),parentId:null,type:"spacer",size:"md"}),g()});function M(e){return function(){j(w,l);const a=$(this).closest(".menu-tree-row").data("id"),c=l.findIndex(k=>k.id===a);if(c===-1)return;const f=l[c],p={separator:{type:"separator"},spacer:{type:"spacer",size:"md"}}[e],m=p?{id:D(),parentId:f.parentId,...p}:{id:D(),parentId:f.parentId,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1};l.splice(c+1,0,m),g()}}t.off("click",".me-ins-item").on("click",".me-ins-item",M("item")),t.off("click",".me-ins-sep").on("click",".me-ins-sep",M("separator")),t.off("click",".me-remove").on("click",".me-remove",function(){const e=$(this).closest(".menu-tree-row").data("id");l=l.filter(a=>a.id!==e&&a.parentId!==e),g()}),t.off("click",".me-indent").on("click",".me-indent",function(){j(w,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(p=>p.id===e);if(!a)return;const c=l.filter(p=>p.parentId===a.parentId),f=c.findIndex(p=>p.id===e);f<=0||["separator","spacer"].includes(c[f-1].type)||(a.parentId=c[f-1].id,g())}),t.off("click",".me-outdent").on("click",".me-outdent",function(){j(w,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(f=>f.id===e);if(!a||a.parentId==null)return;const c=l.find(f=>f.id===a.parentId);a.parentId=c?c.parentId:null,g()}),t.off("click",".me-up").on("click",".me-up",function(){j(w,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.filter(m=>m.parentId===l.find(k=>k.id===e)?.parentId),c=a.findIndex(m=>m.id===e);if(c<=0)return;const f=l.indexOf(a[c]),p=l.indexOf(a[c-1]);[l[f],l[p]]=[l[p],l[f]],g()}),t.off("click",".me-down").on("click",".me-down",function(){j(w,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.filter(m=>m.parentId===l.find(k=>k.id===e)?.parentId),c=a.findIndex(m=>m.id===e);if(c===-1||c>=a.length-1)return;const f=l.indexOf(a[c]),p=l.indexOf(a[c+1]);[l[f],l[p]]=[l[p],l[f]],g()}),t.off("click",".me-hidden").on("click",".me-hidden",function(){j(w,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(c=>c.id===e);a&&(a.hidden=!a.hidden),g()}),t.off("click",".me-edit").on("click",".me-edit",function(){const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(c=>c.id===e);!a||["separator","spacer"].includes(a.type)||jt(a,y.resources||[],x,U,()=>g())}),E.tabs(t.find("#me-tabs").get(0)),t.find("#me-variant").val(o.variant||""),t.find("#me-position").val(o.position||"");const R=t.find("#me-appearOnHover").get(0);R&&(R.checked=o.appearOnHover===!0),t.find("#me-float-anchor").val(o.float?.anchor||"TL"),t.find("#me-float-offsetX").val(o.float?.offsetX??""),t.find("#me-float-offsetY").val(o.float?.offsetY??""),t.find("#me-float-width").val(o.float?.width??""),t.find("#me-float-radius").val(o.float?.radius??""),t.find("#me-float-shadow").val(o.float?.shadow||""),t.find("#me-float-opacity").val(o.float?.opacity??""),t.find("#me-float-title").val(o.float?.title||"");for(const[e,a]of[["#me-float-collapsible",o.float?.collapsible],["#me-float-collapsed",o.float?.collapsed],["#me-float-movable",o.float?.movable],["#me-float-resizable",o.float?.resizable]]){const c=t.find(e).get(0);c&&(c.checked=a===!0)}const T=o.float?.accent||"",q=T.startsWith("#"),C=t.find("#me-float-accent"),S=t.find("#me-float-accent-hex");C.val(q?"__custom__":T),q&&S.val(T);const B=()=>{S.get(0).style.display=C.val()==="__custom__"?"":"none"};B(),C.on("change",B);const A=()=>C.val()==="__custom__"?S.val():C.val(),u=()=>{const e=t.find("#me-position").val()==="floating";t.find(".dm-float-field").each(function(){this.style.display=e?"":"none"}),e&&B();const a=t.find("#me-orientation").val()==="vertical";t.find(".dm-vertical-field").each(function(){this.style.display=a?"":"none"});const c=!!(t.find("#me-float-collapsible").get(0)||{}).checked;t.find(".dm-float-collapsed-field").each(function(){this.style.display=e&&c?"":"none"})};t.find("#me-orientation").val(o.orientation||""),t.find("#me-side").val(o.side||"left"),u(),t.find("#me-position").on("change",u),t.find("#me-orientation").on("change",u),t.find("#me-float-collapsible").on("change",u),t.find("#me-highlight-hover").val(o.highlight?.hover||""),t.find("#me-highlight-active").val(o.highlight?.active||"");const N=o.highlight?.accent||"",Y=N.startsWith("#"),L=t.find("#me-highlight-accent"),Q=t.find("#me-highlight-accent-hex");L.val(Y?"__custom__":N),Y&&Q.val(N);const ot=()=>{Q.get(0).style.display=L.val()==="__custom__"?"":"none"};ot(),L.on("change",ot);const rt=()=>L.val()==="__custom__"?Q.val():L.val();t.find("#me-fontFamily").val(o.style?.fontFamily||""),t.find("#me-fontSize").val(o.style?.fontSize||""),t.find("#me-fontWeight").val(o.style?.fontWeight||""),t.find("#me-letterSpacing").val(o.style?.letterSpacing||""),t.find("#me-iconSize").val(o.style?.iconSize||"");const K=t.find("#me-bind-slot"),Z=t.find("#me-bind-projects"),X=t.find("#me-bind-pages");K.html('<option value="">- not bound -</option>'+(P||[]).map(e=>`<option value="${h(e.slot)}">${h(e.label||e.slot)} (${h(e.slot)})</option>`).join("")),Z.html(r.map(e=>`<option value="${h(e.slug)}">${h(e.name||e.slug)}</option>`).join("")),K.val(o.binding?.slot||"");const pt=new Set(o.binding?.projects||[]);Array.from(Z.get(0).options).forEach(e=>{e.selected=pt.has(e.value)}),X.val((o.binding?.pages||[]).join(`
|
|
44
|
+
`)),t.find("#me-bind-page-pick").html('<option value="">- pick a page -</option>'+z.map(e=>{const a=e.urlPath||e.url||"";return a?`<option value="${h(a)}">${h(e.title||a)} - ${h(a)}</option>`:""}).join("")),t.find("#me-bind-page-add").on("click",()=>{const e=t.find("#me-bind-page-pick").val();if(!e)return;const a=X.val().split(`
|
|
45
|
+
`).map(c=>c.trim()).filter(Boolean);a.includes(e)||a.push(e),X.val(a.join(`
|
|
46
|
+
`)),V()});function at(){const e=K.val();return e?{slot:e,projects:Array.from(Z.get(0).selectedOptions).map(a=>a.value),pages:X.val().split(`
|
|
47
|
+
`).map(a=>a.trim()).filter(Boolean)}:null}function V(){const e=at(),a=t.find("#me-bind-summary");if(!e){a.html('<span class="text-muted">Not bound - this menu renders only where it is mapped or placed by a shortcode.</span>');return}const c=[];e.projects.length&&c.push(`pages in ${e.projects.map(m=>`<code>${h(m)}</code>`).join(", ")}`),e.pages.length&&c.push(`pages matching ${e.pages.map(m=>`<code>${h(m)}</code>`).join(", ")}`);const f=e.pages.filter(m=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(m)),p=f.length?`<div class="text-danger" style="margin-top:.4rem">Not valid page patterns: ${f.map(m=>`<code>${h(m)}</code>`).join(", ")}</div>`:"";a.html(c.length?`Takes over the <code>${h(e.slot)}</code> slot on ${c.join(" and ")}.${p}`:`<span class="text-warning">Slot <code>${h(e.slot)}</code> selected but no project or page chosen - the binding will never match.</span>`)}K.on("change",V),Z.on("change",V),X.on("input",V),V();const{getProjectFromHash:mt}=await import("../lib/project-context.js"),ft=i?mt():null,st=t.find("#me-project");st.html('<option value="">- none -</option>'+r.map(e=>`<option value="${h(e.slug)}">${h(e.name||e.slug)}</option>`).join("")),t.find("#me-slug").val(o.slug||""),t.find("#me-name").val(o.name||""),t.find("#me-description").val(o.description||""),st.val(o.meta?.project||ft||""),Domma.icons.scan(t.get(0)),t.find("#me-save").on("click",async()=>{j(w,l);const e={variant:t.find("#me-variant").val(),position:t.find("#me-position").val(),appearOnHover:!!(t.find("#me-appearOnHover").get(0)||{}).checked,floatAnchor:t.find("#me-float-anchor").val(),floatOffsetX:t.find("#me-float-offsetX").val(),floatOffsetY:t.find("#me-float-offsetY").val(),floatWidth:t.find("#me-float-width").val(),floatRadius:t.find("#me-float-radius").val(),floatShadow:t.find("#me-float-shadow").val(),floatAccent:A(),floatOpacity:t.find("#me-float-opacity").val(),floatTitle:t.find("#me-float-title").val().trim(),floatCollapsible:!!(t.find("#me-float-collapsible").get(0)||{}).checked,floatCollapsed:!!(t.find("#me-float-collapsed").get(0)||{}).checked,floatMovable:!!(t.find("#me-float-movable").get(0)||{}).checked,floatResizable:!!(t.find("#me-float-resizable").get(0)||{}).checked,highlightHover:t.find("#me-highlight-hover").val(),highlightActive:t.find("#me-highlight-active").val(),highlightAccent:rt(),orientation:t.find("#me-orientation").val(),side:t.find("#me-side").val(),fontFamily:t.find("#me-fontFamily").val(),fontSize:t.find("#me-fontSize").val(),fontWeight:t.find("#me-fontWeight").val(),letterSpacing:t.find("#me-letterSpacing").val(),iconSize:t.find("#me-iconSize").val()},a={slug:t.find("#me-slug").val().trim(),name:t.find("#me-name").val().trim(),description:t.find("#me-description").val().trim(),project:t.find("#me-project").val()},c=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"],f=Object.fromEntries(Object.entries(e).filter(([I,F])=>c.includes(I)&&F)),p={};if(e.position==="floating"){e.floatAnchor&&(p.anchor=e.floatAnchor);for(const[I,F]of[["offsetX",e.floatOffsetX],["offsetY",e.floatOffsetY],["width",e.floatWidth],["radius",e.floatRadius],["opacity",e.floatOpacity]])F!==""&&F!=null&&Number.isFinite(Number(F))&&(p[I]=Number(F));e.floatShadow&&(p.shadow=e.floatShadow),e.floatAccent&&(p.accent=e.floatAccent),e.floatTitle&&(p.title=e.floatTitle),e.floatCollapsible&&(p.collapsible=!0),e.floatCollapsible&&e.floatCollapsed&&(p.collapsed=!0),e.floatMovable&&(p.movable=!0),e.floatResizable&&(p.resizable=!0)}const m={};e.highlightHover&&(m.hover=e.highlightHover),e.highlightActive&&(m.active=e.highlightActive),e.highlightAccent&&(m.accent=e.highlightAccent);const k={slug:a.slug,name:a.name,description:a.description,...e.variant&&{variant:e.variant},...e.position&&{position:e.position},...e.orientation&&{orientation:e.orientation},...e.orientation==="vertical"&&e.side&&{side:e.side},...Object.keys(p).length&&{float:p},...Object.keys(m).length&&{highlight:m},...e.appearOnHover&&{appearOnHover:!0},...Object.keys(f).length&&{style:f},binding:at(),items:xt(l),meta:{...o.meta||{},project:a.project||null}};try{if(i)await _.menus.create(k);else if(k.slug!==o.slug){await _.menus.create(k);const I=await _.menuLocations.get();let F=!1;for(const[ut,ht]of Object.entries(I))ht===o.slug&&(I[ut]=k.slug,F=!0);F&&await _.menuLocations.save(I),await _.menus.remove(o.slug)}else await _.menus.update(o.slug,k);E.toast("Saved.",{type:"success"}),location.hash="#/menus/edit/"+encodeURIComponent(k.slug)}catch(I){E.toast(`Save failed: ${I.message||I}`,{type:"error"})}})}};
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[data-menu-hover] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a),[data-menu-active] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a){transition:color .18s ease,background-color .18s ease,text-shadow .18s ease,transform .18s ease,letter-spacing .18s ease,text-decoration-color .18s ease}.page-body :is([data-menu-hover],[data-menu-active]).dm-menu li>a{color:var(--dm-primary, inherit);text-decoration:none}[data-menu-hover=none] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a),[data-menu-hover=none] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):hover{color:inherit!important;background-color:transparent!important;text-decoration:none!important;text-shadow:none!important;transform:none!important}[data-menu-hover=colour] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):hover{color:var(--dm-menu-highlight, var(--dm-primary))!important;background-color:transparent!important;text-decoration:none!important}[data-menu-hover=underline] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):hover{background-color:transparent!important;text-decoration:underline!important;text-decoration-thickness:2px;text-underline-offset:4px;text-decoration-color:var(--dm-menu-highlight, currentColor)!important}[data-menu-hover=overline] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):hover{background-color:transparent!important;text-decoration:overline!important;text-decoration-thickness:2px;text-decoration-color:var(--dm-menu-highlight, currentColor)!important}[data-menu-hover=background] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):hover{background-color:color-mix(in srgb,var(--dm-menu-highlight, var(--dm-primary)) 18%,transparent)!important;border-radius:var(--dm-radius-md, 6px);text-decoration:none!important}[data-menu-hover=uppercase] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):hover{text-transform:uppercase!important;letter-spacing:.04em;background-color:transparent!important;text-decoration:none!important}:is([data-menu-hover=grow],[data-menu-hover=breathe]) :is(.dm-menu,.footer-nav) li>a{display:inline-block}[data-menu-hover=grow] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):hover{transform:scale(1.08);background-color:transparent!important;text-decoration:none!important}@keyframes dm-menu-breathe{0%,to{opacity:1;transform:scale(1)}50%{opacity:.72;transform:scale(1.05)}}[data-menu-hover=breathe] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):hover{animation:dm-menu-breathe 1.4s ease-in-out infinite;background-color:transparent!important;text-decoration:none!important}[data-menu-hover=glow] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):hover{text-shadow:0 0 8px color-mix(in srgb,var(--dm-menu-highlight, var(--dm-primary)) 70%,transparent);background-color:transparent!important;text-decoration:none!important}[data-menu-active] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):is(.active,.is-active,[aria-current=page]){box-shadow:none!important;text-decoration:none!important}[data-menu-active=none] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):is(.active,.is-active,[aria-current=page]){font-weight:inherit!important;background-color:transparent!important}[data-menu-active=bold] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):is(.active,.is-active,[aria-current=page]){font-weight:700!important;background-color:transparent!important}[data-menu-active=underline] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):is(.active,.is-active,[aria-current=page]){background-color:transparent!important;text-decoration:underline!important;text-decoration-thickness:2px;text-underline-offset:4px;text-decoration-color:var(--dm-menu-highlight, currentColor)!important}[data-menu-active=overline] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):is(.active,.is-active,[aria-current=page]){background-color:transparent!important;text-decoration:overline!important;text-decoration-thickness:2px;text-decoration-color:var(--dm-menu-highlight, currentColor)!important}[data-menu-active=background] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):is(.active,.is-active,[aria-current=page]){background-color:color-mix(in srgb,var(--dm-menu-highlight, var(--dm-primary)) 18%,transparent)!important;border-radius:var(--dm-radius-md, 6px)}[data-menu-active=darken] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):is(.active,.is-active,[aria-current=page]){background-color:#00000038!important;border-radius:var(--dm-radius-md, 6px)}[data-menu-active=pill] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):is(.active,.is-active,[aria-current=page]){background-color:var(--dm-menu-highlight, var(--dm-primary))!important;color:#fff!important;border-radius:999px;padding-inline:.9em}@media(prefers-reduced-motion:reduce){[data-menu-hover] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a),[data-menu-hover] :is(.navbar-link,.navbar-dropdown-item,.navbar-dropdown-toggle,.sidebar-link,li>a):hover{transition:none;animation:none!important;transform:none!important}}
|