domma-cms 0.54.1 → 0.55.1
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 +23 -77
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/templates/menu-editor.html +19 -25
- package/admin/js/templates/settings.html +11 -0
- package/admin/js/views/menu-editor.js +13 -13
- package/admin/js/views/settings.js +3 -3
- package/bin/cli.js +9 -2
- package/bin/lib/smtp-defaults.js +53 -0
- package/package.json +1 -1
- package/public/css/menu-highlight.css +1 -1
- package/public/css/site.css +1 -1
- package/public/js/menu-decor.mjs +1 -1
- package/public/js/site.js +1 -1
- package/scripts/setup.js +8 -0
- package/server/services/email.js +29 -3
- package/server/services/health.js +23 -2
- package/server/services/menuRender.js +28 -4
- package/server/services/menus.js +33 -19
- package/server/services/renderer.js +4 -0
package/CLAUDE.md
CHANGED
|
@@ -144,61 +144,24 @@ file structure, and `docs/plugin-development.md` for the full API, hooks, and in
|
|
|
144
144
|
|
|
145
145
|
See `docs/configuration.md` for full schema reference.
|
|
146
146
|
|
|
147
|
-
## Multi-menu system
|
|
148
|
-
|
|
149
|
-
Menus live in `config/menus/<slug>.json
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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.
|
|
158
|
-
|
|
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.
|
|
160
|
-
|
|
161
|
-
**Menus render on TWO paths - test both.** `buildMenuNav`/`buildFloatChrome` (server) draw the `[menu]` shortcode and overlay panels; the **public navbar is built in the browser** by Domma from `window.__CMS_NAV__` and never touches that code, so anything added to the server markup is simply absent there. `applyFloat` in `menu-decor.mjs` is the only channel to the client path - it stamps `data-float-*` attributes and `menu-float.js` builds the chrome from them. Fixed in 0.49.1 after `collapsible`/`movable` shipped dead on a floating navbar. Navbar CSS also needs `#site-navbar`-level specificity: `#site-navbar.dm-nav-vertical .navbar-container` outranks any class-only rule.
|
|
162
|
-
|
|
163
|
-
**Floating panel behaviour.** `float` also takes `collapsible`, `collapsed`, `movable`, `resizable` and `title`. Any of the first two or `movable` makes `buildMenuNav` draw a chrome bar (grip + title + hamburger) via `buildFloatChrome`; `public/js/menu-float.js` (loaded from `page.html`, and called by `site.js` for the floating navbar) wires collapse/drag/resize. **Visitor state beats config**: position, size and collapsed state live in the visitor's own `S` storage under `dm_menu_float_<slug>` and are restored over `float` on load; double-clicking the bar clears them. A panel only stops tracking its `anchor` once actually moved or resized (`unanchor()` converts right/bottom/transform pinning to left/top on first interaction), so untouched panels stay responsive. `floatBehaviour` is the third function duplicated across `menus.js` / `menu-decor.mjs`, parity-tested alongside `floatToCss`. Below 768px site.css returns panels to the flow, so only collapse applies there.
|
|
164
|
-
|
|
165
|
-
## Admin sidebar (menu-data-driven)
|
|
166
|
-
|
|
167
|
-
The admin sidebar is now menu-data-driven - no more hardcoded `sidebar-config.js`. The menu lives at `config/menus/admin-sidebar.json` and is auto-seeded on first boot (idempotent, file-existence guarded) with the standard tree: Overview, Projects, Content, Data, System, Documentation. Renderer is at `admin/js/lib/sidebar-renderer.js`; it reads the menu, merges in plugin-registered items (via `hooks.registerSidebarItem({parent, item})`), filters by each item's optional `permission` field, then mounts. The **Menus** admin can edit the sidebar visually like any other menu - the item editor exposes the new `permission` field on each row (resource key from `permissionRegistry`). Plugin-injected items are render-time only and not shown in the Menus editor; admins hide them by adding a same-`text` item with `hidden: true` to the persisted menu.
|
|
168
|
-
|
|
147
|
+
## Multi-menu system, overlays & admin sidebar
|
|
148
|
+
|
|
149
|
+
Menus live in `config/menus/<slug>.json`, mapped to slots by `config/menu-locations.json` (built-in
|
|
150
|
+
slots: `navbar`, `footer-primary`, `footer-legal`, `admin-sidebar`, plus the multi-menu `overlay`
|
|
151
|
+
slot). Menus render on TWO paths - `server/services/menuRender.js` for the `[menu]` shortcode and
|
|
152
|
+
overlays, and Domma in the browser for the public navbar - so a change to one is absent from the
|
|
153
|
+
other. Bindings, highlighting, orientation, floating panels, structural items, and the
|
|
154
|
+
menu-data-driven admin sidebar all have non-obvious contracts: full detail in the
|
|
155
|
+
**`domma-menus` skill** (`.claude/skills/domma-menus/SKILL.md`) - load it before touching any of that.
|
|
169
156
|
## Collection displays & public filtering
|
|
170
157
|
|
|
171
158
|
`[collection]` and `[view]` render in any of table, cards, list, block, accordion, timeline,
|
|
172
|
-
carousel or listgroup (`display="…"`)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
right-clicking inside one opens a menu of filters derived from the clicked entry's fields, plus
|
|
179
|
-
free-text search, sort, group-by, export, copy (CSV/JSON/Markdown to clipboard), print and
|
|
180
|
-
"copy link to this view". Active filters appear as removable chips above the display. Sort,
|
|
181
|
-
filters and grouping all persist in the URL hash (`dmf=`), so a filtered/sorted/grouped view can
|
|
182
|
-
be bookmarked or shared. Each display type filters differently - timeline via the Progression
|
|
183
|
-
instance's `.setItems()`, carousel by rebuilding the track from pristine slide copies and
|
|
184
|
-
re-initialising, table by re-creating the Domma table with the row subset, everything else by
|
|
185
|
-
toggling a hidden class on `[data-entry-id]`. Grouping needs entries rendered as direct DOM
|
|
186
|
-
siblings, so it's only offered on the five toggle-based displays (cards, list, listgroup,
|
|
187
|
-
accordion, block) - table/timeline/carousel manage their own rendering and hide the "Group by…"
|
|
188
|
-
row. See `docs/markdown-shortcodes.md` for the full capability table per display type.
|
|
189
|
-
|
|
190
|
-
**Export permission** lives in the collection schema's `export` block (`enabled`, `access`,
|
|
191
|
-
`formats`) - see `docs/configuration.md`. It is a convenience gate, not an access boundary: entry
|
|
192
|
-
data is already in the page's `data-ctx` attribute, so it never stops a determined reader of page
|
|
193
|
-
source, only the download button shown to everyone else.
|
|
194
|
-
|
|
195
|
-
**Shared modules**: `public/js/collection-export.mjs` (CSV/JSON/Markdown serialisation dialect -
|
|
196
|
-
CSV formula-injection sanitising, BOM handling) is imported by BOTH the browser
|
|
197
|
-
(`collection-context.js`) and the server (`server/services/collections.js`), so admin exports and
|
|
198
|
-
public right-click exports are byte-identical for the same values. `public/js/collection-sort.mjs`
|
|
199
|
-
holds `compareValues`/`sortEntries` - the same comparison ladder backs both the sort menu and the
|
|
200
|
-
`gt`/`lt` right-click filters.
|
|
201
|
-
|
|
159
|
+
carousel or listgroup (`display="…"`), server-side in `markdown.js`, with a public right-click
|
|
160
|
+
menu (`public/js/collection-context.js`) adding filter, search, sort, group-by, export, print and
|
|
161
|
+
shareable `dmf=` hash state on top. Each display type filters differently and export has its own
|
|
162
|
+
permission block: full detail in the **`domma-collection-displays` skill**
|
|
163
|
+
(`.claude/skills/domma-collection-displays/SKILL.md`) - load it before changing a display, the
|
|
164
|
+
context menu, or export serialisation.
|
|
202
165
|
## Key Gotchas
|
|
203
166
|
|
|
204
167
|
1. **Structural menu items**: `{type:'separator'}` and `{type:'spacer', size}` carry no text/url - every renderer (menuRender, site.js navbar+footer, sidebar-renderer, menu-editor flatten/nest) needs a branch, and anything zipping items against rendered DOM must skip them or the indices shift. Domma's navbar has no spacer concept, so site.js strips them pre-render and re-inserts `<li>`s afterwards **against a snapshot** of the children, not the live HTMLCollection. `size: 'flex'` needs a flex parent, so the renderers stamp `dm-menu--has-flex-spacer` / `dm-admin-sidebar--has-flex-spacer` rather than making every list flex.
|
|
@@ -240,31 +203,14 @@ holds `compareValues`/`sortEntries` - the same comparison ladder backs both the
|
|
|
240
203
|
|
|
241
204
|
## Projects
|
|
242
205
|
|
|
243
|
-
Projects group related artefacts under a named slug - sidebar navigation, per-user access scope,
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
**
|
|
250
|
-
|
|
251
|
-
- `project: '<slug>'` → explicit override.
|
|
252
|
-
- `project: null` → resolves to `core` (legacy opt-out, deprecated).
|
|
253
|
-
- Field missing → resolver picks the project whose `rootUrl` is the longest prefix match of the page's URL; no match means `core`.
|
|
254
|
-
|
|
255
|
-
The single source of truth is `getProjectForPage(urlPath, frontmatterProject)` in `server/services/projects.js`; every page-load path (renderer, sidebar, API list endpoints) calls it.
|
|
256
|
-
|
|
257
|
-
**User access scope.** Users gain `projects: ['<slug>', ...]` - the **access scope**, distinct from `user.meta.project` (the **administrative ownership** that decides where the user appears in the sidebar). Two fields, two jobs:
|
|
258
|
-
|
|
259
|
-
- `user.meta.project` - sidebar placement under a Project section.
|
|
260
|
-
- `user.projects: []` - what artefacts the user can see; **empty array = no restriction**; non-empty restricts to listed projects + core artefacts. Super-admins bypass entirely.
|
|
261
|
-
|
|
262
|
-
**Permissions.** New family `projects.{read, create, update, delete}` in `permissionRegistry`. Existing roles are not back-filled - admin grants `projects.read` via the role editor (same gotcha as Menus).
|
|
263
|
-
|
|
264
|
-
**Scaffolder integration.** A recipe with a `project: {...}` block creates the project record on apply (idempotent - re-apply doesn't clobber edits) using `tokens.namespace` as the slug, then stamps `meta.project: <namespace>` on every produced artefact. Seeded users also get `projects: ['<namespace>']` so they can log in and immediately see their project.
|
|
265
|
-
|
|
266
|
-
**Filtering on list endpoints.** Every artefact list path runs `list.filter(item => canSeeArtefact(user, item))`. Get/write endpoints return 403 when the artefact's project isn't in the user's scope. The Projects section in the admin sidebar always renders - `listProjectsForUser(user)` always includes core, so every site shows at least the Core project.
|
|
267
|
-
|
|
206
|
+
Projects group related artefacts under a named slug - sidebar navigation, per-user access scope,
|
|
207
|
+
and a tag the scaffolder stamps on recipe-produced artefacts. Records live in the preset collection
|
|
208
|
+
`content/collections/projects/`; artefacts carry an optional `meta.project`, and anything untagged
|
|
209
|
+
belongs to the protected `core` project by resolution fallback, never by stamping. Note the two
|
|
210
|
+
distinct user fields: `user.meta.project` (administrative ownership, sidebar placement) vs
|
|
211
|
+
`user.projects: []` (access scope, empty = unrestricted). Full detail in the **`domma-projects`
|
|
212
|
+
skill** (`.claude/skills/domma-projects/SKILL.md`) - load it before touching project records,
|
|
213
|
+
artefact tagging, or per-project access checks.
|
|
268
214
|
## External API & API tokens, custom API endpoints
|
|
269
215
|
|
|
270
216
|
The external surface - `/api/v1/:slug` collection access, project-scoped API tokens, and API Builder
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
<summary${
|
|
3
|
-
<div class="sidebar-children">${
|
|
4
|
-
</details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${
|
|
1
|
+
import{api as u}from"../api.js";import{colourToCss as I,applyFloat as K,applyHighlight as G,itemHighlightAttrs as O}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{groupPluginItems as V,stripItemByUrl as Y,insertFoldersBeforeSystem as J,pruneEmptySynthesisedFolders as Q,MANAGE_PLUGINS_URL as Z}from"./sidebar-grouping.js";function f(e){return String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}const ee=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu"}];function N(e,a){return a?!e||!e.length?!1:e.includes(a)?!0:["read","create","update","delete"].some(t=>e.includes(`${a}.${t}`)):!0}function T(e,a){const t=[];for(const n of e){if(n.hidden||n.permission&&!N(a,n.permission))continue;const i=Array.isArray(n.items)&&n.items.length?T(n.items,a):[];t.push({...n,items:i})}return t}function te(e,a){if(!a||!a.length)return e;const t=(n,i)=>{for(const m of n){if((m.text||"").toLowerCase()===i.toLowerCase())return m;if(Array.isArray(m.items)){const r=t(m.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 ne(e){if(!N(e,"projects"))return[];let a=[];try{a=await u.projects.list()}catch{return[]}const t={};await Promise.all(a.map(async i=>{try{const m=await u.projects.artefacts(i.slug);t[i.slug]=m}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),l=t[i.slug]||{},p=[{text:"Overview",url:r,icon:i.icon||"folder"}];for(const h of n){const g=l[h.key],c=Array.isArray(g)?g.length:0;c<=0||p.push({text:`${h.text} (${c})`,url:r+h.path,icon:h.icon})}p.push({text:"Settings",url:r+"/settings",icon:"settings"});const b=i.enabled===!1;return{text:i.name||i.slug,icon:i.icon||"folder",items:p,...b&&{colour:"neutral",badge:{text:"off"}}}})}function se(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 ie(e,a){if(!e.url||!a)return"";const t=a[e.url];return t==null||t<=0?"":`<span class="sidebar-badge">${f(String(t))}</span>`}function ae(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 U(e,a){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="spacer"){const c=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${f(c)}" aria-hidden="true"></div>`}const t=e.icon?`<span data-icon="${f(e.icon)}"></span>`:"",n=f(e.text||""),i=e.url?f(e.url):"",m=Array.isArray(e.items)&&e.items.length>0,r=e.colour?I(e.colour):"",l=O(e),p=[l.hover&&` data-menu-item-hover="${f(l.hover)}"`,l.active&&` data-menu-item-active="${f(l.active)}"`].filter(Boolean).join(""),b=[r&&`color:${f(r)}`,l.accent&&`--dm-menu-highlight:${f(l.accent)}`].filter(Boolean).join(";"),h=b?` style="${b}"`:"";let g="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const c=e.badge.variant?I(e.badge.variant):"";g=`<span class="dm-menu-badge"${c?` style="background:${f(c)};color:var(--dm-text-inverse,#fff)"`:""}>${f(String(e.badge.text))}</span>`}if(m){const c=ae(e),y=S.get(c)!==!1,j=e.items.map(E=>U(E,a)).join("");return`<details data-state-key="${f(c)}"${y?" open":""}>
|
|
2
|
+
<summary${h}>${t} <span class="sidebar-text">${n}</span>${g}</summary>
|
|
3
|
+
<div class="sidebar-children">${j}</div>
|
|
4
|
+
</details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${h}${p}>${t} <span class="sidebar-text">${n}</span>${ie(e,a)}${g}</a>`}async function re(){const e=c=>c.then(y=>Array.isArray(y)?y.length:Array.isArray(y?.entries)?y.entries.length:0).catch(()=>0),[a,t,n,i,m,r,l,p,b,h,g]=await Promise.all([e(u.pages.list()),e(u.media.list()),e(u.collections.list()),e(u.forms.list()),e(u.views.list()),e(u.actions.list()),e(u.blocks.list()),e(u.components.list()),e(u.users.list()),e(u.plugins.list()),u.system?.notifications?.unreadCount?.().then(c=>c?.count??0).catch(()=>0)??Promise.resolve(0)]);return{"#/pages":a,"#/media":t,"#/collections":n,"#/forms":i,"#/views":m,"#/actions":r,"#/blocks":l,"#/components":p,"#/users":b,"#/plugins":h,"#/system/notifications":g}}async function oe(){try{const e=await u.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,m=null,r=null,l=null,p=null;try{const s=await u.menus.get("admin-sidebar");n=Array.isArray(s?.items)?s.items:null,i=s?.variant||null,m=s?.position||null,r=s?.style||null,l=s?.float||null,p=s?.highlight||null}catch{n=null}(!n||!n.length)&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),n=ee.slice());let b=[];try{b=await H.get("/api/sidebar/registered-items")||[]}catch{}n=Y(n,Z);const{toolsFolder:h,pluginsFolder:g,parented:c}=V(b);n=te(n,c),n=J(n,[h,g]);const y=await ne(a);y.length&&se(n,y),n=T(n,a),n=Q(n);const[j,E]=await Promise.all([re().catch(()=>({})),oe()]),q=i?` dm-admin-sidebar--${f(i)}`:"";function L(s,d="px"){if(s==null)return s;const o=String(s).trim();return/^\d+(\.\d+)?$/.test(o)?`${o}${d}`:o}let P="";if(r){const s=[];r.fontFamily&&s.push(`font-family: ${r.fontFamily}, sans-serif`),r.fontSize&&s.push(`font-size: ${L(r.fontSize)}`),r.fontWeight&&s.push(`font-weight: ${r.fontWeight}`),r.letterSpacing&&s.push(`letter-spacing: ${L(r.letterSpacing,"em")}`);const d=[];if(s.length&&d.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=L(r.iconSize);d.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${o} !important; height: ${o} !important; }`)}d.length&&(P=`<style data-admin-sidebar-style>${d.join(" ")}</style>`)}const X=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${f(E)}</div>`,W=n.some(s=>s&&s.type==="spacer"&&s.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",D=`${P}<nav class="dm-admin-sidebar${q}${W}">${X}${n.map(s=>U(s,j)).join("")}</nav>`,F=document.createRange();F.selectNodeContents(t);const _=F.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:p});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=""),m==="floating"){K(t,{position:"floating",float:l||{}});const d=l&&l.anchor||"TL",o=Number.isFinite(Number(l?.offsetX))?Number(l.offsetX):16,A=t.getBoundingClientRect().width||v||260;if(x){const C=Math.round(A+o*2)+"px";d==="R"||d==="TR"||d==="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 d=this.getAttribute("data-state-key");d&&S.set(d,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,z=0,B=0;w.addEventListener("mousedown",s=>{k=!0,z=s.clientX,B=t.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",s.preventDefault()}),document.addEventListener("mousemove",s=>{if(!k)return;const d=Math.max(180,Math.min(480,B+(s.clientX-z)));t.style.width=d+"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 R(){const s=location.hash||"#/";$(t).find(".sidebar-link").removeClass("active"),$(t).find(`.sidebar-link[data-url="${s}"]`).addClass("active")}R(),M.subscribe("router:afterChange",R)}
|
|
@@ -59,6 +59,17 @@
|
|
|
59
59
|
</select>
|
|
60
60
|
<small class="form-hint">Vertical stacks the items into a column - a docked side rail in the navbar slot, an indented list for <code>[menu]</code> and floating panels. Below 993px the navbar always falls back to the mobile drawer.</small>
|
|
61
61
|
</div>
|
|
62
|
+
<div class="form-group">
|
|
63
|
+
<label for="me-align">Alignment</label>
|
|
64
|
+
<select id="me-align" name="align" class="form-input">
|
|
65
|
+
<option value="">- default -</option>
|
|
66
|
+
<option value="left">Left</option>
|
|
67
|
+
<option value="centre">Centre</option>
|
|
68
|
+
<option value="right">Right</option>
|
|
69
|
+
<option value="spread">Spread evenly</option>
|
|
70
|
+
</select>
|
|
71
|
+
<small class="form-hint">Where the items sit along a horizontal menu. On the public navbar the default is <strong>centre</strong>, measured on the bar itself rather than on the room left beside the brand. A <code>[menu]</code> in the page body packs left unless you choose otherwise. Vertical menus and the mobile drawer ignore this.</small>
|
|
72
|
+
</div>
|
|
62
73
|
<div class="form-group dm-vertical-field" style="display:none">
|
|
63
74
|
<label for="me-side">Rail side</label>
|
|
64
75
|
<select id="me-side" name="side" class="form-input">
|
|
@@ -161,33 +172,16 @@
|
|
|
161
172
|
</div>
|
|
162
173
|
<div class="form-group">
|
|
163
174
|
<label for="me-highlight-hover">Hover effect</label>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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>
|
|
175
|
+
<!-- Options are filled from HIGHLIGHT_HOVER in menu-decor.mjs on mount,
|
|
176
|
+
so this list and the per-item editor's always match. -->
|
|
177
|
+
<select id="me-highlight-hover" name="highlightHover" class="form-input"></select>
|
|
178
|
+
<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. Any single item can override this in its own editor, under Highlighting.</small>
|
|
177
179
|
</div>
|
|
178
180
|
<div class="form-group">
|
|
179
|
-
<label for="me-highlight-active">
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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>
|
|
181
|
+
<label for="me-highlight-active">When selected</label>
|
|
182
|
+
<!-- Filled from HIGHLIGHT_ACTIVE in menu-decor.mjs on mount. -->
|
|
183
|
+
<select id="me-highlight-active" name="highlightActive" class="form-input"></select>
|
|
184
|
+
<small class="form-hint">How the item pointing at the page you are on is marked out. Any single item can override this in its own editor, under Highlighting.</small>
|
|
191
185
|
</div>
|
|
192
186
|
<div class="form-group">
|
|
193
187
|
<label for="me-highlight-accent">Highlight colour</label>
|
|
@@ -214,6 +214,17 @@
|
|
|
214
214
|
|
|
215
215
|
<!-- Email / SMTP -->
|
|
216
216
|
<div class="tab-panel">
|
|
217
|
+
<!-- Shown by settings.js when smtp.host is empty. Without a host the CMS
|
|
218
|
+
falls back to an Ethereal test inbox, which ACCEPTS the mail and
|
|
219
|
+
reports success - so nothing anywhere else looks wrong. -->
|
|
220
|
+
<div id="smtp-unconfigured-notice" class="card mb-3" style="display:none;">
|
|
221
|
+
<div class="card-body" style="color:var(--dm-warning,#f59e0b);display:flex;align-items:flex-start;gap:.6rem;">
|
|
222
|
+
<span data-icon="warning"></span>
|
|
223
|
+
<span><strong>No SMTP host configured.</strong> Mail is going to a throwaway test inbox and will
|
|
224
|
+
<strong>not</strong> reach anyone - password resets and form notifications will look like they
|
|
225
|
+
sent. Set a host below and save.</span>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
217
228
|
<div class="row mb-3">
|
|
218
229
|
<div class="col-8">
|
|
219
230
|
<label class="form-label">SMTP Host</label>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
<div class="menu-tree-row menu-tree-row--separator" data-id="${o.id}" style="margin-left:${
|
|
1
|
+
import{api as k}from"../api.js";import{colourToCss as xt,HIGHLIGHT_HOVER as rt,HIGHLIGHT_ACTIVE as pt,HIGHLIGHT_HOVER_LABELS as mt,HIGHLIGHT_ACTIVE_LABELS as ft}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{openIconPicker as At}from"../lib/card-builder.js";import{applyDrop as _t}from"../lib/menu-tree.js";let kt=1;const G=()=>"i_"+kt++,It=760;function ut(t,d=null,s=[]){for(const i of t||[]){const n=G();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||"",highlight:i.highlight?{...i.highlight}:null}),Array.isArray(i.items)&&i.items.length&&ut(i.items,n,s)}return s}function Ht(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},...n.highlight&&(n.highlight.hover||n.highlight.active||n.highlight.accent)?{highlight:{...n.highlight.hover&&{hover:n.highlight.hover},...n.highlight.active&&{active:n.highlight.active},...n.highlight.accent&&{accent:n.highlight.accent}}}:{},...o.length&&{items:o}}})}return s(null)}function h(t){return String(t||"").replace(/&/g,"&").replace(/"/g,""").replace(/</g,"<")}function St(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 ht(t,d){const{top:s,height:i}=t.getBoundingClientRect(),n=i?(d-s)/i:.5;return n<.25?"above":n>.75?"below":"into"}const gt=["me-drop-above","me-drop-below","me-drop-into"];function st(t){t.get(0).querySelectorAll(".menu-tree-row").forEach(d=>d.classList.remove(...gt))}function Ot(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",p=>{i=n.dataset.id,n.style.opacity="0.45",p.dataTransfer.effectAllowed="move";try{p.dataTransfer.setData("text/plain",i)}catch{}}),n.addEventListener("dragend",()=>{n.style.opacity="",n.draggable=!1,i=null,st(t)}),n.addEventListener("dragover",p=>{if(!i||i===n.dataset.id)return;p.preventDefault(),p.dataTransfer.dropEffect="move";const w=ht(n,p.clientY);st(t),n.classList.add(`me-drop-${w}`)}),n.addEventListener("dragleave",()=>n.classList.remove(...gt)),n.addEventListener("drop",p=>{p.preventDefault();const w=n.dataset.id,A=ht(n,p.clientY);if(st(t),!i)return;const y=_t(d(),i,w,A);i=null,y&&s()})})}function jt(t,d,s=()=>""){t.html("");function i(n){for(const o of d.filter(p=>p.parentId===n)){const p=St(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:${p*28}px">
|
|
3
3
|
<span class="menu-tree-grip" data-icon="move"></span>
|
|
4
4
|
<span class="me-sep-line"><span class="me-sep-label">Separator</span></span>
|
|
5
5
|
<button class="btn btn-sm btn-ghost me-ins-item" data-tooltip="Add item below"><span data-icon="plus"></span></button>
|
|
@@ -10,12 +10,12 @@ import{api as _}from"../api.js";import{colourToCss as bt}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(o.type==="spacer"){const
|
|
14
|
-
<div class="menu-tree-row menu-tree-row--spacer" data-id="${o.id}" style="margin-left:${
|
|
13
|
+
`);continue}if(o.type==="spacer"){const N=["xs","sm","md","lg","xl","flex"].map(L=>`<option value="${L}"${(o.size||"md")===L?" selected":""}>${L==="flex"?"flexible":L}</option>`).join("");t.append(`
|
|
14
|
+
<div class="menu-tree-row menu-tree-row--spacer" data-id="${o.id}" style="margin-left:${p*28}px">
|
|
15
15
|
<span class="menu-tree-grip" data-icon="move"></span>
|
|
16
16
|
<span class="me-spacer-line">
|
|
17
17
|
<span class="me-spacer-label">Spacer</span>
|
|
18
|
-
<select class="form-input me-spacer-size" data-tooltip="Spacer size - flexible fills the leftover room">${
|
|
18
|
+
<select class="form-input me-spacer-size" data-tooltip="Spacer size - flexible fills the leftover room">${N}</select>
|
|
19
19
|
</span>
|
|
20
20
|
<button class="btn btn-sm btn-ghost me-ins-item" data-tooltip="Add item below"><span data-icon="plus"></span></button>
|
|
21
21
|
<button class="btn btn-sm btn-ghost me-ins-sep" data-tooltip="Add separator below"><span data-icon="minus"></span></button>
|
|
@@ -25,10 +25,10 @@ import{api as _}from"../api.js";import{colourToCss as bt}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${o.hidden?" menu-tree-row--hidden":""}" data-id="${o.id}" style="margin-left:${
|
|
28
|
+
`);continue}const w=o.badge&&(o.badge.text||o.badge.countFrom)?`<span class="dm-menu-badge">${h(String(o.badge.text||"#"))}</span>`:"",A=xt(o.colour),y=A?` style="color:${h(A)}"`:"";t.append(`
|
|
29
|
+
<div class="menu-tree-row menu-tree-row--slim${o.hidden?" menu-tree-row--hidden":""}" data-id="${o.id}" style="margin-left:${p*28}px">
|
|
30
30
|
<span class="menu-tree-grip" data-icon="move"></span>
|
|
31
|
-
<span class="me-row-label"${
|
|
31
|
+
<span class="me-row-label"${y}>${h(o.text||"(untitled)")}${w}</span>
|
|
32
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>
|
|
@@ -40,8 +40,8 @@ import{api as _}from"../api.js";import{colourToCss as bt}from"/public/js/menu-de
|
|
|
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
|
-
`),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>'+
|
|
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(y=>y.id===s);if(!i||i.type==="separator")return;if(i.type==="spacer"){const y=$(this).find(".me-spacer-size");y.length&&(i.size=y.val());return}const n=$(this).find(".me-text");n.length&&(i.text=n.val());const o=$(this).find(".me-url");o.length&&(i.url=o.val());const p=$(this).find(".me-icon");p.length&&(i.icon=p.val());const w=$(this).find(".me-vis");w.length&&(i.visibility=w.val());const A=$(this).find(".me-perm");A.length&&(i.permission=A.val())})}function Nt(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 I(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 Lt(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=P(s,t||"");return i.name="visibility",i}function P(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 it(t,d){const s=typeof d=="string"&&d.startsWith("#"),i=P([["","- 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=I(t,i);return o.append(n),o._read=()=>i.value==="__custom__"?n.value:i.value,o}function zt(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=p=>{p?i.setAttribute("data-icon",p):i.removeAttribute("data-icon"),Domma.icons.scan(i.parentNode||i)};return n.addEventListener("click",()=>At(p=>{s.value=p,o(p)})),s.addEventListener("input",()=>o(s.value.trim())),I(t,b("div",{className:"me-icon-field"},s,n,i))}function ot(...t){return b("div",{className:"me-field-grid"},...t)}async function Ct(t,d,s,i,n){const o=m=>b("h4",{className:"me-form-section",textContent:m}),p=I("Label",b("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),w=I("URL",b("input",{className:"form-input",name:"url",value:t.url||""}),"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items."),A=zt("Icon",t.icon||""),y=I("Visibility",Lt(t.visibility||"",i),"Who can see this item, by role seniority. Picking a role shows it to that role and anyone more senior. Blank = everyone."),N=P([["","- no permission gate -"],...(d||[]).map(m=>[m.key,m.label||m.key])],t.permission||"");N.name="permission";const L=I("Permission",N,"Hide unless the viewer holds this specific permission. Independent of Visibility - both must pass."),lt=I("Badge text",b("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),J=P([["","- none -"],...(s||[]).map(m=>[m.slug,m.title||m.slug])],t.badge?.countFrom||"");J.name="badgeCount";const at=I("Badge count from",J,"Show a live count of this collection\u2019s entries as the badge."),Q=it("Badge colour",t.badge?.variant||""),l=P([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");l.name="renderAs";const x=I("Render as",l,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),v=I("Pill style",P([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));v.querySelector("select").name="pillStyle";const Y=it("Pill colour",t.pill?.variant||""),W=b("div",{className:"me-pill-wrap"},ot(v,Y));W.style.display=t.pill?"":"none",l.addEventListener("change",()=>{W.style.display=l.value==="pill"?"":"none"});const D=it("Text colour",t.colour||""),X=P([["","- use the menu default -"],...rt.map(m=>[m,mt[m]||m])],t.highlight?.hover||"");X.name="hlHover";const R=I("Hover effect",X,"Overrides the menu-wide hover effect for this item only."),M=P([["","- use the menu default -"],...pt.map(m=>[m,ft[m]||m])],t.highlight?.active||"");M.name="hlActive";const U=I("When selected",M,"How this item is marked when it points at the page being viewed."),tt=it("Highlight colour",t.highlight?.accent||""),B=b("button",{className:"btn btn-primary",textContent:"Apply"}),V=b("div",{className:"me-item-form"},o("Link"),p,w,A,o("Access"),ot(y,L),o("Badge"),ot(lt,Q),at,o("Appearance"),x,W,D,o("Highlighting"),ot(R,U),tt,B),S=E.slideover({title:"Edit menu item",size:"lg",position:"right"});S.element.appendChild(V),S.open();const K=S.element.closest(".dm-slideover")||S.element.querySelector(".dm-slideover")||S.element;K.style.setProperty("width",It+"px","important"),K.style.setProperty("max-width","95vw","important"),Domma.icons.scan(V);const g=m=>V.querySelector(`[name="${m}"]`)?.value||"";B.addEventListener("click",()=>{t.text=g("text"),t.url=g("url"),t.icon=g("icon"),t.visibility=g("visibility"),t.permission=g("permission");const m=g("badgeText"),q=g("badgeCount"),et=Q._read();if(t.badge=m||q?{...m&&{text:m},...q&&{countFrom:q},...et&&{variant:et}}:null,g("renderAs")==="pill"){const nt=Y._read();t.pill={style:g("pillStyle")||"filled",...nt&&{variant:nt}}}else t.pill=null;t.colour=D._read();const F=g("hlHover"),T=g("hlActive"),z=tt._read();t.highlight=F||T||z?{...F&&{hover:F},...T&&{active:T},...z&&{accent:z}}:null,S.close(),n()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){const d=location.hash.match(/^#\/menus\/edit\/(.+)$/),s=d?decodeURIComponent(d[1]):null,i=!s,n=E.loader(t.get(0),{type:"dots"});let o,p,w,A,y,N,L;try{if(i)o={slug:"",name:"",description:"",items:[]};else try{o=await k.menus.get(s)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}p=await k.projects.list().catch(()=>[]),w=await H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),A=await k.collections.list().catch(()=>[]),y=await k.get("/collections/roles/entries?limit=100").catch(()=>null),N=await k.menuLocations.registry().catch(()=>[]),L=await k.pages.list().catch(()=>[])}finally{n.destroy()}const J=(Array.isArray(y)?y:y?.entries||[]).map(e=>({name:e.data?.name,label:e.data?.label,level:e.data?.level??99})).filter(e=>e.name).sort((e,a)=>a.level-e.level),at='<option value="">- no permission gate -</option>',Q=e=>at+(w.resources||[]).map(a=>{const c=(e||"")===a.key?" selected":"";return`<option value="${a.key}"${c}>${a.label||a.key}</option>`}).join("");t.find("#me-title").text(i?"New menu":`Edit "${o.slug}"`);let l=ut(o.items);const x=t.find("#me-items-list");function v(){jt(x,l,Q),Ot(x,()=>l,v)}v(),t.find("#me-add-item").on("click",()=>{j(x,l),l.push({id:G(),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:G(),parentId:null,type:"separator"}),v()}),t.find("#me-add-spacer").on("click",()=>{j(x,l),l.push({id:G(),parentId:null,type:"spacer",size:"md"}),v()});function Y(e){return function(){j(x,l);const a=$(this).closest(".menu-tree-row").data("id"),c=l.findIndex(_=>_.id===a);if(c===-1)return;const u=l[c],r={separator:{type:"separator"},spacer:{type:"spacer",size:"md"}}[e],f=r?{id:G(),parentId:u.parentId,...r}:{id:G(),parentId:u.parentId,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1};l.splice(c+1,0,f),v()}}t.off("click",".me-ins-item").on("click",".me-ins-item",Y("item")),t.off("click",".me-ins-sep").on("click",".me-ins-sep",Y("separator")),t.off("click",".me-remove").on("click",".me-remove",function(){const e=$(this).closest(".menu-tree-row").data("id");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(r=>r.id===e);if(!a)return;const c=l.filter(r=>r.parentId===a.parentId),u=c.findIndex(r=>r.id===e);u<=0||["separator","spacer"].includes(c[u-1].type)||(a.parentId=c[u-1].id,v())}),t.off("click",".me-outdent").on("click",".me-outdent",function(){j(x,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(u=>u.id===e);if(!a||a.parentId==null)return;const c=l.find(u=>u.id===a.parentId);a.parentId=c?c.parentId:null,v()}),t.off("click",".me-up").on("click",".me-up",function(){j(x,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.filter(f=>f.parentId===l.find(_=>_.id===e)?.parentId),c=a.findIndex(f=>f.id===e);if(c<=0)return;const u=l.indexOf(a[c]),r=l.indexOf(a[c-1]);[l[u],l[r]]=[l[r],l[u]],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(f=>f.parentId===l.find(_=>_.id===e)?.parentId),c=a.findIndex(f=>f.id===e);if(c===-1||c>=a.length-1)return;const u=l.indexOf(a[c]),r=l.indexOf(a[c+1]);[l[u],l[r]]=[l[r],l[u]],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)||Ct(a,w.resources||[],A,J,()=>v())}),E.tabs(t.find("#me-tabs").get(0)),t.find("#me-variant").val(o.variant||""),t.find("#me-position").val(o.position||"");const W=t.find("#me-appearOnHover").get(0);W&&(W.checked=o.appearOnHover===!0),t.find("#me-float-anchor").val(o.float?.anchor||"TL"),t.find("#me-float-offsetX").val(o.float?.offsetX??""),t.find("#me-float-offsetY").val(o.float?.offsetY??""),t.find("#me-float-width").val(o.float?.width??""),t.find("#me-float-radius").val(o.float?.radius??""),t.find("#me-float-shadow").val(o.float?.shadow||""),t.find("#me-float-opacity").val(o.float?.opacity??""),t.find("#me-float-title").val(o.float?.title||"");for(const[e,a]of[["#me-float-collapsible",o.float?.collapsible],["#me-float-collapsed",o.float?.collapsed],["#me-float-movable",o.float?.movable],["#me-float-resizable",o.float?.resizable]]){const c=t.find(e).get(0);c&&(c.checked=a===!0)}const D=o.float?.accent||"",X=D.startsWith("#"),R=t.find("#me-float-accent"),M=t.find("#me-float-accent-hex");R.val(X?"__custom__":D),X&&M.val(D);const U=()=>{M.get(0).style.display=R.val()==="__custom__"?"":"none"};U(),R.on("change",U);const tt=()=>R.val()==="__custom__"?M.val():R.val(),B=()=>{const e=t.find("#me-position").val()==="floating";t.find(".dm-float-field").each(function(){this.style.display=e?"":"none"}),e&&U();const a=t.find("#me-orientation").val()==="vertical";t.find(".dm-vertical-field").each(function(){this.style.display=a?"":"none"});const c=!!(t.find("#me-float-collapsible").get(0)||{}).checked;t.find(".dm-float-collapsed-field").each(function(){this.style.display=e&&c?"":"none"})};t.find("#me-orientation").val(o.orientation||""),t.find("#me-align").val(o.align||""),t.find("#me-side").val(o.side||"left"),B(),t.find("#me-position").on("change",B),t.find("#me-orientation").on("change",B),t.find("#me-float-collapsible").on("change",B);const V=(e,a,c,u)=>{const r=t.find(e).get(0);r&&(r.innerHTML="",r.add(new Option(u,"")),a.forEach(f=>r.add(new Option(c[f]||f,f))))};V("#me-highlight-hover",rt,mt,"- theme default -"),V("#me-highlight-active",pt,ft,"- theme default -"),t.find("#me-highlight-hover").val(o.highlight?.hover||""),t.find("#me-highlight-active").val(o.highlight?.active||"");const S=o.highlight?.accent||"",K=S.startsWith("#"),g=t.find("#me-highlight-accent"),m=t.find("#me-highlight-accent-hex");g.val(K?"__custom__":S),K&&m.val(S);const q=()=>{m.get(0).style.display=g.val()==="__custom__"?"":"none"};q(),g.on("change",q);const et=()=>g.val()==="__custom__"?m.val():g.val();t.find("#me-fontFamily").val(o.style?.fontFamily||""),t.find("#me-fontSize").val(o.style?.fontSize||""),t.find("#me-fontWeight").val(o.style?.fontWeight||""),t.find("#me-letterSpacing").val(o.style?.letterSpacing||""),t.find("#me-iconSize").val(o.style?.iconSize||"");const F=t.find("#me-bind-slot"),T=t.find("#me-bind-projects"),z=t.find("#me-bind-pages");F.html('<option value="">- not bound -</option>'+(N||[]).map(e=>`<option value="${h(e.slot)}">${h(e.label||e.slot)} (${h(e.slot)})</option>`).join("")),T.html(p.map(e=>`<option value="${h(e.slug)}">${h(e.name||e.slug)}</option>`).join("")),F.val(o.binding?.slot||"");const nt=new Set(o.binding?.projects||[]);Array.from(T.get(0).options).forEach(e=>{e.selected=nt.has(e.value)}),z.val((o.binding?.pages||[]).join(`
|
|
44
|
+
`)),t.find("#me-bind-page-pick").html('<option value="">- pick a page -</option>'+L.map(e=>{const a=e.urlPath||e.url||"";return a?`<option value="${h(a)}">${h(e.title||a)} - ${h(a)}</option>`:""}).join("")),t.find("#me-bind-page-add").on("click",()=>{const e=t.find("#me-bind-page-pick").val();if(!e)return;const a=z.val().split(`
|
|
45
|
+
`).map(c=>c.trim()).filter(Boolean);a.includes(e)||a.push(e),z.val(a.join(`
|
|
46
|
+
`)),Z()});function dt(){const e=F.val();return e?{slot:e,projects:Array.from(T.get(0).selectedOptions).map(a=>a.value),pages:z.val().split(`
|
|
47
|
+
`).map(a=>a.trim()).filter(Boolean)}:null}function Z(){const e=dt(),a=t.find("#me-bind-summary");if(!e){a.html('<span class="text-muted">Not bound - this menu renders only where it is mapped or placed by a shortcode.</span>');return}const c=[];e.projects.length&&c.push(`pages in ${e.projects.map(f=>`<code>${h(f)}</code>`).join(", ")}`),e.pages.length&&c.push(`pages matching ${e.pages.map(f=>`<code>${h(f)}</code>`).join(", ")}`);const u=e.pages.filter(f=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(f)),r=u.length?`<div class="text-danger" style="margin-top:.4rem">Not valid page patterns: ${u.map(f=>`<code>${h(f)}</code>`).join(", ")}</div>`:"";a.html(c.length?`Takes over the <code>${h(e.slot)}</code> slot on ${c.join(" and ")}.${r}`:`<span class="text-warning">Slot <code>${h(e.slot)}</code> selected but no project or page chosen - the binding will never match.</span>`)}F.on("change",Z),T.on("change",Z),z.on("input",Z),Z();const{getProjectFromHash:bt}=await import("../lib/project-context.js"),vt=i?bt():null,ct=t.find("#me-project");ct.html('<option value="">- none -</option>'+p.map(e=>`<option value="${h(e.slug)}">${h(e.name||e.slug)}</option>`).join("")),t.find("#me-slug").val(o.slug||""),t.find("#me-name").val(o.name||""),t.find("#me-description").val(o.description||""),ct.val(o.meta?.project||vt||""),Domma.icons.scan(t.get(0)),t.find("#me-save").on("click",async()=>{j(x,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:tt(),floatOpacity:t.find("#me-float-opacity").val(),floatTitle:t.find("#me-float-title").val().trim(),floatCollapsible:!!(t.find("#me-float-collapsible").get(0)||{}).checked,floatCollapsed:!!(t.find("#me-float-collapsed").get(0)||{}).checked,floatMovable:!!(t.find("#me-float-movable").get(0)||{}).checked,floatResizable:!!(t.find("#me-float-resizable").get(0)||{}).checked,highlightHover:t.find("#me-highlight-hover").val(),highlightActive:t.find("#me-highlight-active").val(),highlightAccent:et(),orientation:t.find("#me-orientation").val(),align:t.find("#me-align").val(),side:t.find("#me-side").val(),fontFamily:t.find("#me-fontFamily").val(),fontSize:t.find("#me-fontSize").val(),fontWeight:t.find("#me-fontWeight").val(),letterSpacing:t.find("#me-letterSpacing").val(),iconSize:t.find("#me-iconSize").val()},a={slug:t.find("#me-slug").val().trim(),name:t.find("#me-name").val().trim(),description:t.find("#me-description").val().trim(),project:t.find("#me-project").val()},c=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"],u=Object.fromEntries(Object.entries(e).filter(([O,C])=>c.includes(O)&&C)),r={};if(e.position==="floating"){e.floatAnchor&&(r.anchor=e.floatAnchor);for(const[O,C]of[["offsetX",e.floatOffsetX],["offsetY",e.floatOffsetY],["width",e.floatWidth],["radius",e.floatRadius],["opacity",e.floatOpacity]])C!==""&&C!=null&&Number.isFinite(Number(C))&&(r[O]=Number(C));e.floatShadow&&(r.shadow=e.floatShadow),e.floatAccent&&(r.accent=e.floatAccent),e.floatTitle&&(r.title=e.floatTitle),e.floatCollapsible&&(r.collapsible=!0),e.floatCollapsible&&e.floatCollapsed&&(r.collapsed=!0),e.floatMovable&&(r.movable=!0),e.floatResizable&&(r.resizable=!0)}const f={};e.highlightHover&&(f.hover=e.highlightHover),e.highlightActive&&(f.active=e.highlightActive),e.highlightAccent&&(f.accent=e.highlightAccent);const _={slug:a.slug,name:a.name,description:a.description,...e.variant&&{variant:e.variant},...e.position&&{position:e.position},...e.orientation&&{orientation:e.orientation},...e.orientation==="vertical"&&e.side&&{side:e.side},...e.orientation!=="vertical"&&e.align&&{align:e.align},...Object.keys(r).length&&{float:r},...Object.keys(f).length&&{highlight:f},...e.appearOnHover&&{appearOnHover:!0},...Object.keys(u).length&&{style:u},binding:dt(),items:Ht(l),meta:{...o.meta||{},project:a.project||null}};try{if(i)await k.menus.create(_);else if(_.slug!==o.slug){await k.menus.create(_);const O=await k.menuLocations.get();let C=!1;for(const[yt,wt]of Object.entries(O))wt===o.slug&&(O[yt]=_.slug,C=!0);C&&await k.menuLocations.save(O),await k.menus.remove(o.slug)}else await k.menus.update(o.slug,_);E.toast("Saved.",{type:"success"}),location.hash="#/menus/edit/"+encodeURIComponent(_.slug)}catch(O){E.toast(`Save failed: ${O.message||O}`,{type:"error"})}})}};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{api as C,apiRequest as
|
|
2
|
-
`,
|
|
3
|
-
`,
|
|
1
|
+
import{api as C,apiRequest as y}from"../api.js";import{populateThemeSelect as w}from"../lib/themes.js";export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(e){E.tabs(e.find("#settings-tabs").get(0));const p=E.loader(e.get(0),{type:"dots"}),t=await C.settings.get().catch(()=>({}));if(p.destroy(),e.find("#field-site-title").val(t.title||""),e.find("#field-tagline").val(t.tagline||""),e.find("#field-font-family").val(t.fontFamily||"Roboto"),e.find("#field-font-size").val(t.fontSize||16),w(e.find("#field-theme").get(0)),w(e.find("#field-admin-theme").get(0),{includeAdmin:!0}),e.find("#field-theme").val(t.baseTheme||t.theme||"charcoal-dark"),t.baseTheme){e.find("#field-theme").prop("disabled",!0);const l=document.createElement("p");l.className="form-hint",l.style.cssText="margin-top:.4rem;font-size:.8rem;color:var(--dm-info);";const s=document.createElement("a");s.href="#/plugins/theme-roller",s.textContent="Theme Roller",l.appendChild(document.createTextNode(`Custom theme \u201C${t.theme}\u201D is active (based on ${t.baseTheme}). Manage via `)),l.appendChild(s),l.appendChild(document.createTextNode(".")),e.find("#field-theme").get(0).closest(".col-6").appendChild(l)}e.find("#field-admin-theme").val(t.adminTheme||"charcoal-dark");const h=t.autoTheme||{},b=!!h.enabled,u=e.find("#field-auto-theme-enabled"),g=e.find("#auto-theme-fields"),v=e.find("#field-theme"),f=v.get(0).innerHTML;e.find("#field-day-theme").html(f),e.find("#field-night-theme").html(f),t.baseTheme?(u.prop("disabled",!0),e.find("#auto-theme-roller-hint").show()):(u.prop("checked",b),b&&(g.show(),v.prop("disabled",!0)),u.on("change",function(){const l=this.checked;g.toggle(l),v.prop("disabled",l)})),e.find("#field-day-theme").val(h.dayTheme||"charcoal-light"),e.find("#field-night-theme").val(h.nightTheme||"charcoal-dark"),e.find("#field-day-start").val(h.dayStart||"07:00"),e.find("#field-night-start").val(h.nightStart||"19:00"),e.find("#field-spacer-size").val(t.layoutOptions?.spacerSize??40),e.find("#field-seo-title").val(t.seo?.defaultTitle||""),e.find("#field-seo-separator").val(t.seo?.titleSeparator||" | "),e.find("#field-seo-desc").val(t.seo?.defaultDescription||""),e.find("#field-footer-copy").val(t.footer?.copyright||""),e.find("#field-footer-motion-toggle").prop("checked",t.footer?.showMotionToggle!==!1),e.find("#field-social-twitter").val(t.social?.twitter||""),e.find("#field-social-facebook").val(t.social?.facebook||""),e.find("#field-social-instagram").val(t.social?.instagram||""),e.find("#field-social-linkedin").val(t.social?.linkedin||""),e.find("#field-social-github").val(t.social?.github||""),e.find("#field-social-youtube").val(t.social?.youtube||""),e.find("#field-smtp-host").val(t.smtp?.host||""),e.find("#field-smtp-port").val(t.smtp?.port||587),e.find("#field-smtp-user").val(t.smtp?.user||""),e.find("#field-smtp-pass").val(t.smtp?.pass||""),e.find("#field-smtp-secure").prop("checked",t.smtp?.secure||!1),e.find("#field-smtp-from-address").val(t.smtp?.fromAddress||""),e.find("#field-smtp-from-name").val(t.smtp?.fromName||""),S(t.smtp?.host||""),e.find("#field-smtp-host").on("input",function(){S(this.value)});const i=t.backToTop||{};e.find("#field-btt-enabled").prop("checked",i.enabled!==!1),e.find("#field-btt-threshold").val(i.scrollThreshold??300),e.find("#field-btt-position").val(i.position||"bottom-right"),e.find("#field-btt-offset").val(i.offset??32),e.find("#field-btt-bottom-offset").val(i.bottomOffset??i.offset??32),e.find("#field-btt-label").val(i.label||""),e.find("#field-btt-smooth").prop("checked",i.smooth!==!1);const d=t.cookieConsent||{};e.find("#field-cc-enabled").prop("checked",d.enabled!==!1),e.find("#field-cc-message").val(d.message||""),e.find("#field-cc-accept-all").val(d.acceptAllText||"Accept All"),e.find("#field-cc-reject-all").val(d.rejectAllText||"Reject All"),e.find("#field-cc-customize").val(d.customizeText||"Customize"),e.find("#field-cc-save-prefs").val(d.savePreferencesText||"Save Preferences"),e.find("#field-cc-privacy-text").val(d.privacyPolicyText||"Privacy Policy"),e.find("#field-cc-privacy-url").val(d.privacyPolicyUrl||""),e.find("#field-cc-cookie-text").val(d.cookiePolicyText||"Cookie Policy"),e.find("#field-cc-cookie-url").val(d.cookiePolicyUrl||""),e.find("#field-cc-position").val(d.position||"bottom"),e.find("#field-cc-layout").val(d.layout||"bar"),e.find("#field-cc-theme").val(d.theme||"dark"),e.find("#field-cc-show-functional").prop("checked",d.showFunctional!==!1),e.find("#field-cc-show-analytics").prop("checked",d.showAnalytics!==!1),e.find("#field-cc-show-marketing").prop("checked",d.showMarketing!==!1),e.find("#field-cc-version").val(d.consentVersion||"1.0");const m=t.breadcrumbs||{};e.find("#field-breadcrumbs-enabled").prop("checked",m.enabled===!0),e.find("#field-breadcrumbs-home-label").val(m.homeLabel||"Home"),e.find("#field-breadcrumbs-position").val(m.position||"TL"),e.find("#field-bc-offset-x").val(m.offsetX??8),e.find("#field-bc-offset-y").val(m.offsetY??8);function c(l){e.find(".bc-pos-btn").each(function(){const s=this.dataset.pos===l;this.style.background=s?"var(--dm-primary, #5b8cff)":"",this.style.color=s?"var(--dm-primary-text)":""})}c(m.position||"TL"),e.find(".bc-pos-btn").on("click",function(){const l=this.dataset.pos;e.find("#field-breadcrumbs-position").val(l),c(l)});let a=null;try{const{css:l}=await y("/settings/custom-css");e.find("#field-custom-css").val(l||""),E.editor&&(a=E.editor(e.find("#field-custom-css").get(0),{mode:"code",language:"css",lineNumbers:!0,showToolbar:!1,minHeight:420,placeholder:"/* Add your custom CSS here */",characterCount:!0}))}catch{}a&&a._editorEl&&a._editorEl.addEventListener("keydown",l=>{if(!l.ctrlKey&&!l.metaKey)return;if(l.key==="s"){l.preventDefault(),e.find("#save-css-btn").get(0)?.click();return}const s=a._editorEl,n=s.selectionStart!==s.selectionEnd;if((l.key==="c"||l.key==="x")&&!n){l.preventDefault();const o=s.value,k=s.selectionStart,T=o.lastIndexOf(`
|
|
2
|
+
`,k-1)+1,x=o.indexOf(`
|
|
3
|
+
`,k),z=x===-1?o.slice(T):o.slice(T,x+1);if(navigator.clipboard.writeText(z),l.key==="x"){const P=o.slice(0,T),A=x===-1?"":o.slice(x+1);s.value=P+A,s.selectionStart=s.selectionEnd=T,s.dispatchEvent(new Event("input",{bubbles:!0})),E.toast("Line cut",{type:"info",duration:1500})}else E.toast("Line copied",{type:"info",duration:1500})}}),e.find("#save-settings-btn").on("click",async()=>{const l=e.find("#field-admin-theme").val(),s=!t.baseTheme&&e.find("#field-auto-theme-enabled").prop("checked"),n={enabled:s,dayTheme:e.find("#field-day-theme").val()||"charcoal-light",nightTheme:e.find("#field-night-theme").val()||"charcoal-dark",dayStart:e.find("#field-day-start").val()||"07:00",nightStart:e.find("#field-night-start").val()||"19:00"},o={title:e.find("#field-site-title").val().trim(),tagline:e.find("#field-tagline").val().trim(),fontFamily:e.find("#field-font-family").val()||"Roboto",fontSize:parseInt(e.find("#field-font-size").val(),10)||16,theme:t.baseTheme?t.theme:s?n.dayTheme:e.find("#field-theme").val(),...t.baseTheme?{baseTheme:t.baseTheme}:{},autoTheme:n,adminTheme:l,layoutOptions:{spacerSize:parseInt(e.find("#field-spacer-size").val(),10)||40},seo:{defaultTitle:e.find("#field-seo-title").val().trim(),titleSeparator:e.find("#field-seo-separator").val()||" | ",defaultDescription:e.find("#field-seo-desc").val().trim()},footer:{copyright:e.find("#field-footer-copy").val().trim(),showMotionToggle:e.find("#field-footer-motion-toggle").prop("checked"),links:t.footer?.links||[]},social:{twitter:e.find("#field-social-twitter").val().trim(),facebook:e.find("#field-social-facebook").val().trim(),instagram:e.find("#field-social-instagram").val().trim(),linkedin:e.find("#field-social-linkedin").val().trim(),github:e.find("#field-social-github").val().trim(),youtube:e.find("#field-social-youtube").val().trim()},smtp:{host:e.find("#field-smtp-host").val().trim(),port:parseInt(e.find("#field-smtp-port").val(),10)||587,user:e.find("#field-smtp-user").val().trim(),pass:e.find("#field-smtp-pass").val(),secure:e.find("#field-smtp-secure").prop("checked"),fromAddress:e.find("#field-smtp-from-address").val().trim(),fromName:e.find("#field-smtp-from-name").val().trim()},backToTop:{enabled:e.find("#field-btt-enabled").prop("checked"),scrollThreshold:parseInt(e.find("#field-btt-threshold").val(),10)||300,position:e.find("#field-btt-position").val()||"bottom-right",offset:parseInt(e.find("#field-btt-offset").val(),10)||32,bottomOffset:parseInt(e.find("#field-btt-bottom-offset").val(),10)||32,label:e.find("#field-btt-label").val().trim(),smooth:e.find("#field-btt-smooth").prop("checked")},cookieConsent:{enabled:e.find("#field-cc-enabled").prop("checked"),message:e.find("#field-cc-message").val().trim(),acceptAllText:e.find("#field-cc-accept-all").val().trim(),rejectAllText:e.find("#field-cc-reject-all").val().trim(),customizeText:e.find("#field-cc-customize").val().trim(),savePreferencesText:e.find("#field-cc-save-prefs").val().trim(),privacyPolicyText:e.find("#field-cc-privacy-text").val().trim(),privacyPolicyUrl:e.find("#field-cc-privacy-url").val().trim(),cookiePolicyText:e.find("#field-cc-cookie-text").val().trim(),cookiePolicyUrl:e.find("#field-cc-cookie-url").val().trim(),position:e.find("#field-cc-position").val(),layout:e.find("#field-cc-layout").val(),theme:e.find("#field-cc-theme").val(),showFunctional:e.find("#field-cc-show-functional").prop("checked"),showAnalytics:e.find("#field-cc-show-analytics").prop("checked"),showMarketing:e.find("#field-cc-show-marketing").prop("checked"),consentVersion:e.find("#field-cc-version").val().trim()||"1.0"}},k=e.find("#save-settings-btn");k.prop("disabled",!0);try{await C.settings.save(o),Domma.theme.set(l),E.toast("Settings saved.",{type:"success"})}catch{E.toast("Failed to save settings.",{type:"error"})}finally{k.prop("disabled",!1)}}),e.find("#send-test-email-btn").on("click",async()=>{const l=e.find("#field-test-email-to").val().trim(),s=e.find("#test-email-result").get(0),n=e.find("#send-test-email-btn");n.prop("disabled",!0),s&&(s.textContent="Sending\u2026",s.style.color="");try{const o=await y("/settings/test-email",{method:"POST",body:JSON.stringify({to:l||void 0})});s&&(s.textContent=o.message||"Test email sent.",s.style.color="var(--dm-success,#4ade80)")}catch(o){s&&(s.textContent=o.message||"Failed to send test email.",s.style.color="var(--dm-danger,#f87171)")}finally{n.prop("disabled",!1)}}),e.find("#save-breadcrumbs-btn").on("click",async()=>{const l=e.find("#save-breadcrumbs-btn");l.prop("disabled",!0);try{const s={enabled:e.find("#field-breadcrumbs-enabled").prop("checked"),homeLabel:e.find("#field-breadcrumbs-home-label").val().trim()||"Home",position:e.find("#field-breadcrumbs-position").val()||"TL",offsetX:parseInt(e.find("#field-bc-offset-x").val(),10)||8,offsetY:parseInt(e.find("#field-bc-offset-y").val(),10)||8};await C.settings.save({breadcrumbs:s}),t.breadcrumbs=s,E.toast("Breadcrumbs settings saved.",{type:"success"})}catch{E.toast("Failed to save breadcrumbs settings.",{type:"error"})}finally{l.prop("disabled",!1)}}),e.find("#save-css-btn").on("click",async()=>{const l=a?a.getValue():e.find("#field-custom-css").val(),s=e.find("#save-css-btn");s.prop("disabled",!0);try{await y("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:l})}),E.toast("Custom CSS saved.",{type:"success"})}catch(n){E.toast(n.message||"Failed to save CSS.",{type:"error"})}finally{s.prop("disabled",!1)}});async function r(){try{const l=await y("/cache/status"),s=l.enabled?`enabled (${l.driver})`:"disabled",n=l.maxItems?` \xB7 ${l.size} / ${l.maxItems} entries`:"";e.find("#cache-status").text(s+n)}catch{e.find("#cache-status").text("unknown")}try{const{entries:l}=await y("/cache/keys");F(e.find("#cache-keys-table"),l),e.find("#cache-key-count").text(`(${l.length})`)}catch(l){e.find("#cache-keys-table").get(0).textContent="Failed to load keys: "+l.message}}await r(),e.find("#btn-clear-cache").on("click",async()=>{if(!await E.confirm("Clear the entire response cache?"))return;const l=e.find("#btn-clear-cache");l.prop("disabled",!0);try{await y("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"}),await r()}catch(s){E.toast(s.message||"Failed to clear cache.",{type:"error"})}finally{l.prop("disabled",!1)}}),e.find("#btn-refresh-cache-keys").on("click",r)}};function S(e){const p=document.getElementById("smtp-unconfigured-notice");p&&(p.style.display=String(e||"").trim()?"none":"",p.style.display===""&&Domma.icons.scan(p))}function F(e,p){const t=e.get(0);if(!t)return;for(;t.firstChild;)t.removeChild(t.firstChild);if(!p.length){const f=document.createElement("p");f.className="text-muted",f.style.fontSize=".875rem",f.textContent="Cache is empty.",t.appendChild(f);return}const h=[...p].reverse(),b=document.createElement("table");b.className="table table-compact";const u=document.createElement("thead"),g=document.createElement("tr");for(const f of["Key","Tags","Expires"]){const i=document.createElement("th");i.textContent=f,g.appendChild(i)}u.appendChild(g),b.appendChild(u);const v=document.createElement("tbody");for(const f of h){const i=document.createElement("tr"),d=document.createElement("td");d.style.fontFamily="monospace",d.style.fontSize="12px",d.textContent=f.key,i.appendChild(d);const m=document.createElement("td");m.style.fontSize="12px";for(const a of f.tags){const r=document.createElement("span");r.className="badge",r.style.marginRight="4px",r.style.fontSize="11px",r.textContent=a,m.appendChild(r)}i.appendChild(m);const c=document.createElement("td");if(c.style.fontSize="12px",c.style.color="var(--dm-text-muted, #6b7280)",f.expiresAt===null)c.textContent="no expiry";else{const a=f.expiresAt-Date.now();a<=0?c.textContent="expired":a<6e4?c.textContent=`${Math.round(a/1e3)}s`:a<36e5?c.textContent=`${Math.round(a/6e4)}m`:c.textContent=`${Math.round(a/36e5)}h`}i.appendChild(c),v.appendChild(i)}b.appendChild(v),t.appendChild(b)}
|