domma-cms 1.10.27 → 1.10.30

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.10.30] - 2026-10-26
4
+
5
+ - Admin navigation keeps My Profile in the top account menu, removing its redundant System entry. Search defaults to Content, and existing System entries move there once while retaining saved settings and renamed folders.
6
+
7
+ ## [1.10.29] - 2026-10-26
8
+
9
+ - **Pro Tools open again on sites behind the Manager.** The admin's versioned-file check ran before the Manager handed a request to its site, so a site's request for a Tool's screen (Analytics Pro's dashboard, for one) was answered from the Manager's own copy of that plugin; where the two builds differ the fingerprint never matched and the screen 404'd. The check now runs after the plugins' hooks, so the site answers its own files.
10
+
11
+ ## [1.10.28] - 2026-10-26
12
+
13
+ - Admin pages and Tools have clearer section boundaries: stronger outlines, coloured header bands, icon tiles and a shaded canvas, all using the active theme palette. Settings and editor sections have distinct identities, and optional generated CSS and template-variable references fold away with keyboard-accessible controls.
14
+
3
15
  ## [1.10.27] - 2026-10-26
4
16
 
5
17
  - **Built against domma-js 1.0.0** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v1.0.0).
@@ -0,0 +1 @@
1
+ .dashboard-layout{--dm-admin-section-accent: var(--dm-info, var(--dm-primary));--dm-admin-section-border: color-mix(in srgb, var(--dm-text) 30%, var(--dm-border))}.dashboard-layout :is(.acx-card,.aex-card,.apx-card,.atx-card,.bex-card,.blx-card,.cax-card,.cex-card,.clx-card,.cmx-card,.cnx-card,.cpx-card,.dbx-card,.epx-card,.fsx-card,.fxx-card,.lyx-card,.mdx-card,.mlx-card,.mnx-card,.mpx-card,.ntx-card,.pcx-card,.pdx-card,.pgx-card,.pjx-card,.plx-card,.psx-card,.rex-card,.rlx-card,.sox-card,.ssx-card,.stx-card,.thx-card,.uex-card,.usx-card,.vex-card,.vpx-card,.vwx-card,.wpx-card,.bkp-card,.calx-card,.dtx-card,.ecx-card,.gx-card,.jbx-card,.obx-card,.scx-card,.smx-card,.trx-card,.bk-card,.shp-card,.wp-card,.wk-card,.gma-card,.cpp-card,.sp-card,.tfx-card,.msx-card,.an-card,.dm-admin-section){--dm-admin-section-pad-x: .9rem;--dm-admin-section-pad-y: .75rem;min-width:0;background-color:var(--dm-surface);border-block:2px solid var(--dm-admin-section-border);border-inline-end:2px solid var(--dm-admin-section-border);border-inline-start:5px solid var(--dm-admin-section-accent)}.dashboard-layout :is(.acx-card,.aex-card,.apx-card,.atx-card,.bex-card,.blx-card,.cax-card,.cex-card,.clx-card,.cmx-card,.cnx-card,.cpx-card,.dbx-card,.epx-card,.fsx-card,.fxx-card,.lyx-card,.mdx-card,.mlx-card,.mnx-card,.mpx-card,.ntx-card,.pcx-card,.pdx-card,.pgx-card,.pjx-card,.plx-card,.psx-card,.rex-card,.rlx-card,.sox-card,.ssx-card,.stx-card,.thx-card,.uex-card,.usx-card,.vex-card,.vpx-card,.vwx-card,.wpx-card,.bkp-card,.calx-card,.dtx-card,.ecx-card,.gx-card,.jbx-card,.obx-card,.scx-card,.smx-card,.trx-card,.bk-card,.shp-card,.wp-card,.wk-card,.gma-card,.cpp-card,.sp-card,.tfx-card,.msx-card,.an-card,.dm-admin-section)>:is(.acx-card-title,.aex-card-title,.apx-card-title,.atx-card-title,.bex-card-title,.blx-card-title,.cax-card-title,.cex-card-title,.clx-card-title,.cmx-card-title,.cnx-card-title,.cpx-card-title,.dbx-card-title,.epx-card-title,.fsx-card-title,.fxx-card-title,.lyx-card-title,.mdx-card-title,.mlx-card-title,.mnx-card-title,.mpx-card-title,.ntx-card-title,.pcx-card-title,.pdx-card-title,.pgx-card-title,.pjx-card-title,.plx-card-title,.psx-card-title,.rex-card-title,.rlx-card-title,.sox-card-title,.ssx-card-title,.stx-card-title,.thx-card-title,.uex-card-title,.usx-card-title,.vex-card-title,.vpx-card-title,.vwx-card-title,.wpx-card-title,.bkp-card-title,.calx-card-title,.dtx-card-title,.ecx-card-title,.gx-card-title,.jbx-card-title,.obx-card-title,.scx-card-title,.smx-card-title,.trx-card-title,.wp-card-head,.wk-card-head,.gma-h,.dm-admin-section-title){margin:.25rem calc(-1 * var(--dm-admin-section-pad-x)) 0;padding:.8rem var(--dm-admin-section-pad-x);font-weight:700;border-bottom:1px solid var(--dm-border);background:color-mix(in srgb,var(--dm-admin-section-accent) 20%,var(--dm-surface));color:var(--dm-text);line-height:1.5;flex-wrap:wrap;overflow-wrap:anywhere}.dashboard-layout :is(.acx-card,.aex-card,.apx-card,.atx-card,.bex-card,.blx-card,.cax-card,.cex-card,.clx-card,.cmx-card,.cnx-card,.cpx-card,.dbx-card,.epx-card,.fsx-card,.fxx-card,.lyx-card,.mdx-card,.mlx-card,.mnx-card,.mpx-card,.ntx-card,.pcx-card,.pdx-card,.pgx-card,.pjx-card,.plx-card,.psx-card,.rex-card,.rlx-card,.sox-card,.ssx-card,.stx-card,.thx-card,.uex-card,.usx-card,.vex-card,.vpx-card,.vwx-card,.wpx-card,.bkp-card,.calx-card,.dtx-card,.ecx-card,.gx-card,.jbx-card,.obx-card,.scx-card,.smx-card,.trx-card,.bk-card,.shp-card,.wp-card,.wk-card,.gma-card,.cpp-card,.sp-card,.tfx-card,.msx-card,.an-card,.dm-admin-section)>:is(.acx-card-title,.aex-card-title,.apx-card-title,.atx-card-title,.bex-card-title,.blx-card-title,.cax-card-title,.cex-card-title,.clx-card-title,.cmx-card-title,.cnx-card-title,.cpx-card-title,.dbx-card-title,.epx-card-title,.fsx-card-title,.fxx-card-title,.lyx-card-title,.mdx-card-title,.mlx-card-title,.mnx-card-title,.mpx-card-title,.ntx-card-title,.pcx-card-title,.pdx-card-title,.pgx-card-title,.pjx-card-title,.plx-card-title,.psx-card-title,.rex-card-title,.rlx-card-title,.sox-card-title,.ssx-card-title,.stx-card-title,.thx-card-title,.uex-card-title,.usx-card-title,.vex-card-title,.vpx-card-title,.vwx-card-title,.wpx-card-title,.bkp-card-title,.calx-card-title,.dtx-card-title,.ecx-card-title,.gx-card-title,.jbx-card-title,.obx-card-title,.scx-card-title,.smx-card-title,.trx-card-title,.wp-card-head,.wk-card-head,.gma-h,.dm-admin-section-title):first-child{margin-top:calc(-1 * var(--dm-admin-section-pad-y));border-start-start-radius:calc(var(--dm-radius-md) - 1px);border-start-end-radius:calc(var(--dm-radius-md) - 1px)}.dashboard-layout :is(.acx-card-title,.aex-card-title,.apx-card-title,.atx-card-title,.bex-card-title,.blx-card-title,.cax-card-title,.cex-card-title,.clx-card-title,.cmx-card-title,.cnx-card-title,.cpx-card-title,.dbx-card-title,.epx-card-title,.fsx-card-title,.fxx-card-title,.lyx-card-title,.mdx-card-title,.mlx-card-title,.mnx-card-title,.mpx-card-title,.ntx-card-title,.pcx-card-title,.pdx-card-title,.pgx-card-title,.pjx-card-title,.plx-card-title,.psx-card-title,.rex-card-title,.rlx-card-title,.sox-card-title,.ssx-card-title,.stx-card-title,.thx-card-title,.uex-card-title,.usx-card-title,.vex-card-title,.vpx-card-title,.vwx-card-title,.wpx-card-title,.bkp-card-title,.calx-card-title,.dtx-card-title,.ecx-card-title,.gx-card-title,.jbx-card-title,.obx-card-title,.scx-card-title,.smx-card-title,.trx-card-title,.wp-card-head,.wk-card-head,.gma-h,.dm-admin-section-title)>[data-icon]{color:var(--dm-admin-section-accent);background:color-mix(in srgb,var(--dm-admin-section-accent) 18%,var(--dm-surface));padding:.25rem;border-radius:var(--dm-radius-sm);flex-shrink:0}.dashboard-layout :is(.mpx-card,.uex-card){--dm-admin-section-pad-y: .8rem}.dashboard-layout :is(.dbx-card,.pdx-card,.psx-card,.wk-card){--dm-admin-section-pad-x: .95rem;--dm-admin-section-pad-y: .8rem}.dashboard-layout :is(.bk-card,.shp-card,.wp-card){--dm-admin-section-pad-x: 1rem;--dm-admin-section-pad-y: .9rem}.dashboard-layout :is(.cpp-card,.cpx-card,.ecx-card,.obx-card,.gma-card){--dm-admin-section-pad-x: 1rem;--dm-admin-section-pad-y: 1rem}.dashboard-layout .pcx-card{--dm-admin-section-pad-x: 0px;--dm-admin-section-pad-y: 0px}.dashboard-layout .pcx-card>.pcx-card-title{padding-inline:.75rem}.dashboard-layout :is(.sox-flush,.scx-flush,.smx-flush){--dm-admin-section-pad-x: 0px;--dm-admin-section-pad-y: 0px}.dashboard-layout :is(.sox-flush,.scx-flush,.smx-flush)>:is(.acx-card-title,.aex-card-title,.apx-card-title,.atx-card-title,.bex-card-title,.blx-card-title,.cax-card-title,.cex-card-title,.clx-card-title,.cmx-card-title,.cnx-card-title,.cpx-card-title,.dbx-card-title,.epx-card-title,.fsx-card-title,.fxx-card-title,.lyx-card-title,.mdx-card-title,.mlx-card-title,.mnx-card-title,.mpx-card-title,.ntx-card-title,.pcx-card-title,.pdx-card-title,.pgx-card-title,.pjx-card-title,.plx-card-title,.psx-card-title,.rex-card-title,.rlx-card-title,.sox-card-title,.ssx-card-title,.stx-card-title,.thx-card-title,.uex-card-title,.usx-card-title,.vex-card-title,.vpx-card-title,.vwx-card-title,.wpx-card-title,.bkp-card-title,.calx-card-title,.dtx-card-title,.ecx-card-title,.gx-card-title,.jbx-card-title,.obx-card-title,.scx-card-title,.smx-card-title,.trx-card-title,.wp-card-head,.wk-card-head,.gma-h,.dm-admin-section-title){padding-inline:.9rem}.dashboard-layout .card:not(:where(.dm-plugin-unwrap,.card-primary,.card-success,.card-warning,.card-danger,.card-info,.card-accent,.card-accent-success,.card-accent-warning,.card-accent-danger,.card-accent-info)){border-block:2px solid var(--dm-admin-section-border);border-inline-end:2px solid var(--dm-admin-section-border);border-inline-start:5px solid var(--dm-admin-section-accent)}.dashboard-layout .card:not(:where(.card-primary,.card-success,.card-warning,.card-danger,.card-info))>.card-header{background:color-mix(in srgb,var(--dm-admin-section-accent) 20%,var(--dm-surface));border-bottom-color:var(--dm-border);flex-wrap:wrap;row-gap:.4rem}.dashboard-layout .card>.card-header :is(h2,h3,h4){overflow-wrap:anywhere}.dashboard-layout .card>.card-header [data-icon]{color:var(--dm-admin-section-accent)}.dashboard-layout :is(.card-primary,.card-success,.card-warning,.card-danger,.card-info)>.card-header [data-icon]{color:inherit}.dashboard-layout .card-success{--dm-admin-section-accent: var(--dm-success)}.dashboard-layout .card-warning{--dm-admin-section-accent: var(--dm-warning)}.dashboard-layout .card-danger{--dm-admin-section-accent: var(--dm-danger)}.dashboard-layout .card-info{--dm-admin-section-accent: var(--dm-info, var(--dm-primary))}.dashboard-layout :is(.dm-section--info,:has(>.dm-section--info)){--dm-admin-section-accent: var(--dm-info, var(--dm-primary))}.dashboard-layout :is(.dm-section--neutral,:has(>.dm-section--neutral)){--dm-admin-section-accent: var(--dm-text-muted)}.dashboard-layout :is(.dm-section--success,:has(>.dm-section--success)){--dm-admin-section-accent: var(--dm-success)}.dashboard-layout :is(.dm-section--danger,:has(>.dm-section--danger)){--dm-admin-section-accent: var(--dm-danger)}.dashboard-layout :is(.acx-card,.aex-card,.apx-card,.atx-card,.bex-card,.blx-card,.cax-card,.cex-card,.clx-card,.cmx-card,.cnx-card,.cpx-card,.dbx-card,.epx-card,.fsx-card,.fxx-card,.lyx-card,.mdx-card,.mlx-card,.mnx-card,.mpx-card,.ntx-card,.pcx-card,.pdx-card,.pgx-card,.pjx-card,.plx-card,.psx-card,.rex-card,.rlx-card,.sox-card,.ssx-card,.stx-card,.thx-card,.uex-card,.usx-card,.vex-card,.vpx-card,.vwx-card,.wpx-card,.bkp-card,.calx-card,.dtx-card,.ecx-card,.gx-card,.jbx-card,.obx-card,.scx-card,.smx-card,.trx-card,.bk-card,.shp-card,.wp-card,.wk-card,.gma-card,.cpp-card,.sp-card,.tfx-card,.msx-card,.an-card,.dm-admin-section)>:is(.acx-bar,.apx-bar,.atx-bar,.blx-bar,.cax-bar,.cex-bar,.clx-bar,.cmx-bar,.cpx-bar,.dbx-bar,.epx-bar,.fsx-bar,.gx-bar,.jbx-bar,.lyx-bar,.mdx-bar,.mlx-bar,.mnx-bar,.ntx-bar,.pdx-bar,.pgx-bar,.pjx-bar,.plx-bar,.rlx-bar,.sox-bar,.ssx-bar,.stx-bar,.thx-bar,.usx-bar,.vpx-bar,.vwx-bar,.wpx-bar,.bkp-bar,.calx-bar,.dtx-bar,.ecx-bar,.obx-bar,.scx-bar,.smx-bar,.trx-bar,.bk-bar,.wp-bar):first-child{padding-bottom:.75rem;border-bottom:1px solid var(--dm-border)}.dashboard-layout :is(.ct-bar,.notes-bar,.todo-bar,.an-bar,.sx-home__bar,.gma-screen>.gma-bar){padding-bottom:.75rem;border-bottom:1px solid var(--dm-border)}.dashboard-layout .ct-chips-row{padding-bottom:.75rem;border-bottom:1px solid var(--dm-border)}.dashboard-layout .an-chart-wrap{border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);background:var(--dm-surface)}.dashboard-layout :is(.mail-section-heading,.bk-sub,.me-form-section){padding-block:.55rem .35rem;border-top:1px solid var(--dm-border);color:var(--dm-text)}.dashboard-layout :is(.pe-ex-head,.pe-dock-tabs,.pcx-toolbar){background:color-mix(in srgb,var(--dm-primary) 7%,var(--dm-surface))}.dashboard-layout .pe-ex-section+.pe-ex-section{margin-top:.6rem;border-top:1px solid var(--dm-border);padding-top:.6rem}.dashboard-layout :is(.stx-card,.thx-card,.ssx-card,.cex-card,.aex-card,.rex-card,.vex-card,.fxx-card,.clx-card,.dtx-card){margin-top:1rem}.dashboard-layout .stx-card-columns>.stx-card{margin-top:1rem}.dashboard-layout details.dm-admin-section{display:block;padding:0;border:2px solid var(--dm-admin-section-border);border-inline-start:5px solid var(--dm-admin-section-accent);border-radius:var(--dm-radius-md)}.dashboard-layout details.dm-admin-section>summary.dm-admin-section-title{display:flex;align-items:center;gap:.45rem;margin:0;padding:.65rem .9rem;font-weight:600;cursor:pointer;list-style:none;border-bottom:0;border-radius:calc(var(--dm-radius-md) - 1px)}.dashboard-layout details.dm-admin-section>summary::-webkit-details-marker{display:none}.dashboard-layout details.dm-admin-section>summary:after{content:"";width:.45rem;height:.45rem;margin-inline-start:auto;border-right:2px solid var(--dm-text-muted);border-bottom:2px solid var(--dm-text-muted);transform:rotate(45deg) translateY(-2px);flex-shrink:0}.dashboard-layout details.dm-admin-section[open]>summary.dm-admin-section-title{border-bottom:1px solid var(--dm-border);border-end-start-radius:0;border-end-end-radius:0}.dashboard-layout details.dm-admin-section[open]>summary:after{transform:rotate(225deg) translate(-2px,-1px)}.dashboard-layout details.dm-admin-section>summary:focus-visible{outline:2px solid var(--dm-primary);outline-offset:3px}.dashboard-layout .dm-admin-section-content{display:flex;flex-direction:column;gap:.75rem;padding:.9rem;min-width:0}.dashboard-layout :is(.sox-tile,.scx-tile,.apx-tile,.dbx-kpi,.gma-stat){border:1px solid var(--dm-border)}.dashboard-layout .svx-card{border-block:2px solid var(--dm-admin-section-border);border-inline-end:2px solid var(--dm-admin-section-border)}.dashboard-layout .dm-plugin-body{background:color-mix(in srgb,var(--dm-text) 3%,var(--dm-surface))}.dashboard-layout :is(.stx-card-columns,.svx-editor){gap:1.25rem}.dashboard-layout :is(.stx-card,.thx-card,.ssx-card,.cex-card,.aex-card,.rex-card,.vex-card,.fxx-card,.clx-card,.dtx-card){margin-top:1.25rem}.dashboard-layout .stx-card-columns>.stx-card{margin-top:1.25rem}.dashboard-layout .svx-card{border-inline-start:5px solid var(--svx-accent)}.dashboard-layout .svx-card>.svx-card-title{background:color-mix(in srgb,var(--svx-accent) 20%,var(--dm-surface));font-weight:700}
package/admin/index.html CHANGED
@@ -20,6 +20,7 @@
20
20
  <!-- Admin CSS -->
21
21
  <link rel="stylesheet" href="/admin/css/admin.css">
22
22
  <link rel="stylesheet" href="/admin/css/dashboard.css">
23
+ <link rel="stylesheet" href="/admin/css/sections.css">
23
24
 
24
25
  <!-- Forms CSS - needed for conditional logic preview (.fb-field-hidden, etc.) -->
25
26
  <link rel="stylesheet" href="/public/css/forms.css?v=20261025-dropzone-css">
@@ -1 +1 @@
1
- import{isPathAllowed as F}from"./admin-scope.js";export const TOOLS_FOLDER_TEXT="Tools",MANAGE_PLUGINS_URL="#/plugins",MANAGE_PLUGINS_ITEM={text:"Marketplace",url:MANAGE_PLUGINS_URL,icon:"store",permission:"plugins"};export function stripItemByUrl(e,t){const n=[];for(const r of e||[]){if(!r||r.url===t)continue;const s={...r};Array.isArray(r.items)&&r.items.length&&(s.items=stripItemByUrl(r.items,t)),n.push(s)}return n}export function managePluginsItem(){return{...MANAGE_PLUGINS_ITEM}}export const TOOLS_FOLDER_KEY="tools";const E=["countUrl","core","permission"],c=e=>String(e??"").toLowerCase();export function slugify(e){return c(e).trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}export function groupKeyFor(e,t){return`${c(e)}.${slugify(t)}`}export const GROUP_ICON="folder";const P=e=>e&&(e.type==="separator"||e.type==="spacer"||e.type==="theme-switcher"),A=e=>e&&!P(e)&&!e.url;function w(e){return(e||[]).filter(Boolean).map(t=>Array.isArray(t.items)?{...t,items:w(t.items)}:{...t})}function M(e,t,n=[]){for(const r of e||[]){if(!r)continue;if(t(r))return[...n,r];const s=M(r.items,t,[...n,r]);if(s)return s}return null}function I(e,t,n=[],r=[]){for(const s of e||[])s&&(t(s)&&r.push([...n,s]),I(s.items,t,[...n,s],r));return r}const U=e=>!e.some(t=>t.hidden||t.restricted);function $(e,t){for(const n of e||[]){if(t(n))return n;const r=$(n?.items,t);if(r)return r}return null}export function findFolder(e,t){if(!t)return null;const n=c(t);return $(e,r=>A(r)&&c(r.key)===n)||$(e,r=>A(r)&&!r.key&&c(r.text)===n)}function b(e,t){const n={...t,...e};for(const r of E)t[r]!==void 0&&(n[r]=t[r]);return!(Array.isArray(e.items)&&e.items.length)&&Array.isArray(t.items)&&(n.items=t.items),e.text||(n.text=t.text),!e.icon&&t.icon&&(n.icon=t.icon),delete n.ref,delete n.folder,delete n.group,n}function O(e,{persisted:t,orphan:n=!1,node:r=null}){const s=e?.item||{};return{persisted:t,orphan:n,source:s.source||(s.core?"Built in":""),folder:e?.folder||TOOLS_FOLDER_KEY,defaults:n?{text:null,icon:null}:{text:s.text||"",icon:s.icon||""},...n&&r&&{label:r.text||r.url},...(e?.group||e?.item?.group)&&{group:e.group||e.item.group}}}export function placeTools(e,t,{forEditor:n=!1}={}){const r=w(e),s=(t||[]).filter(o=>o&&o.item&&typeof o.item.text=="string"),m=new Map;for(const o of s)o.item.url&&!m.has(o.item.url)&&m.set(o.item.url,o);const y=new Set,d=o=>{const i=[];for(const u of o){const h=u.url?m.get(u.url):null;if(h&&!y.has(u.url)){y.add(u.url);const g=b(u,h.item);Array.isArray(u.items)&&u.items.length?g.items=d(u.items):n&&(g.items=[]),n&&(g.$tool=O(h,{persisted:!0})),i.push(g);continue}if(u.ref){if(!n)continue;const{ref:g,...f}=u;i.push({...f,items:d(u.items||[]),$tool:O(null,{persisted:!0,orphan:!0,node:u})});continue}Array.isArray(u.items)&&(u.items=d(u.items)),i.push(u)}return i},a=d(r),T=new Set,S=o=>{for(const i of o||[])i.hidden&&i.text&&!i.url&&!(Array.isArray(i.items)&&i.items.length)&&T.add(c(i.text)),S(i.items)};S(a);let x=null;const L=()=>{const o=findFolder(a,TOOLS_FOLDER_KEY);if(o)return o;if(!x){x={text:TOOLS_FOLDER_TEXT,key:TOOLS_FOLDER_KEY,icon:"tool",items:[],synthesised:!0};const i=a.findIndex(u=>["system","documentation"].includes(c(u?.key||u?.text)));a.splice(i===-1?a.length:i,0,x)}return x},l=new Set,k=(o,i)=>{const u=groupKeyFor(o.key||slugify(o.text),i),h=$(a,p=>A(p)&&c(p.key)===u)||(o.items||[]).find(p=>A(p)&&(!p.key||c(p.key)===u)&&c(p.text)===c(i));if(h)return h;const g={text:String(i),key:u,icon:GROUP_ICON,items:[],synthesised:!0},f=o.items.findIndex(p=>l.has(p));return o.items.splice(f===-1?o.items.length:f,0,g),g};for(const o of s){const{item:i}=o;if(i.url&&y.has(i.url))continue;i.url&&y.add(i.url);const u=T.has(c(i.text));if(u&&!n)continue;const h=b({},i);n&&(h.items=[],h.$tool=O(o,{persisted:!1}),u&&(h.hidden=!0));let f=(o.parent?findFolder(a,o.parent)||$(a,_=>!P(_)&&c(_.text)===c(o.parent)):null)||findFolder(a,o.folder||i.folder)||L();f.items=Array.isArray(f.items)?f.items:[];const p=typeof(o.group||i.group)=="string"?(o.group||i.group).trim():"";p&&slugify(p)&&(f=k(f,p),f.items=Array.isArray(f.items)?f.items:[]),f.items.push(h),l.add(h)}return a}export const LOCKED_TOOLS=[{folder:"overview",item:{text:"Dashboard",url:"#/",icon:"home"}},{folder:"content",item:{text:"Site menus",url:"#/menus",icon:"menu",permission:"menus"}},{folder:"system",item:{text:"Users",url:"#/users",icon:"users",permission:"users"}},{folder:"system",item:{text:"My Profile",url:"#/my-profile",icon:"user"}}];export function isLockedUrl(e){return LOCKED_TOOLS.some(t=>t.item.url===e)}export function ensureLockedTools(e,{leaveInHiddenFolders:t=!1}={}){const n=w(e);for(const{folder:r,item:s}of LOCKED_TOOLS){const m=I(n,l=>l?.url===s.url),y=m.find(l=>!l.some(k=>k.restricted));if(!y&&m.length)continue;const d=y?.at(-1);if(d){if(d.hidden&&delete d.hidden,!y.slice(0,-1).some(o=>o.hidden)||t)continue;const k=y.at(-2);k.items=k.items.filter(o=>o!==d)}const a=c(r),T=I(n,l=>A(l)&&c(l.key)===a).filter(U),S=I(n,l=>A(l)&&!l.key&&c(l.text)===a).filter(U),x=(T[0]||S[0])?.at(-1)||null,L=d||{...s};x?(x.items=Array.isArray(x.items)?x.items:[],x.items.push(L)):n.push(L)}return n}export function insertFoldersBeforeSystem(e,t){const n=(t||[]).filter(Boolean);if(!n.length)return e;const r=["system","documentation"],s=e.findIndex(m=>r.includes(c(m?.key||m?.text)));return s===-1?[...e,...n]:[...e.slice(0,s),...n,...e.slice(s)]}export function canAny(e,t){return t?!e||!e.length?!1:e.includes(t)?!0:["read","create","update","delete"].some(n=>e.includes(`${t}.${n}`)):!0}export function filterByPermission(e,t,n=null){const r=[];for(const s of e){if(s.hidden||s.permission&&!canAny(t,s.permission))continue;const m=typeof s.url=="string"?s.url:"";if(n&&m.startsWith("#/")&&!F(n,m.slice(1)))continue;const y=Array.isArray(s.items)&&s.items.length>0,d=y?filterByPermission(s.items,t,n):[];y&&!d.length&&!m||r.push({...s,items:d})}return r}export function sidebarStateKey(e,t=null){const n=String(e?.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),r=String(e?.url||"").match(/^#\/projects\/([^/]+)/);let s="";if(r)try{s=`project-${decodeURIComponent(r[1])}.`}catch{s=`project-${r[1]}.`}return t?e?.key?`sidebar.expanded.key.${c(e.key)}`:`${t}/${s}${n}`:`sidebar.expanded.${s}${n}`}export function pruneEmptyToolsFolder(e){return(e||[]).filter(t=>A(t)?!(c(t.key)===TOOLS_FOLDER_KEY||!t.key&&t.text===TOOLS_FOLDER_TEXT)||Array.isArray(t.items)&&t.items.length>0:!0)}
1
+ import{isPathAllowed as F}from"./admin-scope.js";export const TOOLS_FOLDER_TEXT="Tools",MANAGE_PLUGINS_URL="#/plugins",MANAGE_PLUGINS_ITEM={text:"Marketplace",url:MANAGE_PLUGINS_URL,icon:"store",permission:"plugins"};export function stripItemByUrl(e,t){const n=[];for(const r of e||[]){if(!r||r.url===t)continue;const s={...r};Array.isArray(r.items)&&r.items.length&&(s.items=stripItemByUrl(r.items,t)),n.push(s)}return n}export function managePluginsItem(){return{...MANAGE_PLUGINS_ITEM}}export const TOOLS_FOLDER_KEY="tools";const E=["countUrl","core","permission"],c=e=>String(e??"").toLowerCase();export function slugify(e){return c(e).trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}export function groupKeyFor(e,t){return`${c(e)}.${slugify(t)}`}export const GROUP_ICON="folder";const P=e=>e&&(e.type==="separator"||e.type==="spacer"||e.type==="theme-switcher"),A=e=>e&&!P(e)&&!e.url;function w(e){return(e||[]).filter(Boolean).map(t=>Array.isArray(t.items)?{...t,items:w(t.items)}:{...t})}function N(e,t,n=[]){for(const r of e||[]){if(!r)continue;if(t(r))return[...n,r];const s=N(r.items,t,[...n,r]);if(s)return s}return null}function I(e,t,n=[],r=[]){for(const s of e||[])s&&(t(s)&&r.push([...n,s]),I(s.items,t,[...n,s],r));return r}const U=e=>!e.some(t=>t.hidden||t.restricted);function $(e,t){for(const n of e||[]){if(t(n))return n;const r=$(n?.items,t);if(r)return r}return null}export function findFolder(e,t){if(!t)return null;const n=c(t);return $(e,r=>A(r)&&c(r.key)===n)||$(e,r=>A(r)&&!r.key&&c(r.text)===n)}function b(e,t){const n={...t,...e};for(const r of E)t[r]!==void 0&&(n[r]=t[r]);return!(Array.isArray(e.items)&&e.items.length)&&Array.isArray(t.items)&&(n.items=t.items),e.text||(n.text=t.text),!e.icon&&t.icon&&(n.icon=t.icon),delete n.ref,delete n.folder,delete n.group,n}function O(e,{persisted:t,orphan:n=!1,node:r=null}){const s=e?.item||{};return{persisted:t,orphan:n,source:s.source||(s.core?"Built in":""),folder:e?.folder||TOOLS_FOLDER_KEY,defaults:n?{text:null,icon:null}:{text:s.text||"",icon:s.icon||""},...n&&r&&{label:r.text||r.url},...(e?.group||e?.item?.group)&&{group:e.group||e.item.group}}}export function placeTools(e,t,{forEditor:n=!1}={}){const r=w(e),s=(t||[]).filter(o=>o&&o.item&&typeof o.item.text=="string"),d=new Map;for(const o of s)o.item.url&&!d.has(o.item.url)&&d.set(o.item.url,o);const y=new Set,m=o=>{const i=[];for(const u of o){const h=u.url?d.get(u.url):null;if(h&&!y.has(u.url)){y.add(u.url);const g=b(u,h.item);Array.isArray(u.items)&&u.items.length?g.items=m(u.items):n&&(g.items=[]),n&&(g.$tool=O(h,{persisted:!0})),i.push(g);continue}if(u.ref){if(!n)continue;const{ref:g,...f}=u;i.push({...f,items:m(u.items||[]),$tool:O(null,{persisted:!0,orphan:!0,node:u})});continue}Array.isArray(u.items)&&(u.items=m(u.items)),i.push(u)}return i},a=m(r),T=new Set,S=o=>{for(const i of o||[])i.hidden&&i.text&&!i.url&&!(Array.isArray(i.items)&&i.items.length)&&T.add(c(i.text)),S(i.items)};S(a);let x=null;const L=()=>{const o=findFolder(a,TOOLS_FOLDER_KEY);if(o)return o;if(!x){x={text:TOOLS_FOLDER_TEXT,key:TOOLS_FOLDER_KEY,icon:"tool",items:[],synthesised:!0};const i=a.findIndex(u=>["system","documentation"].includes(c(u?.key||u?.text)));a.splice(i===-1?a.length:i,0,x)}return x},l=new Set,k=(o,i)=>{const u=groupKeyFor(o.key||slugify(o.text),i),h=$(a,p=>A(p)&&c(p.key)===u)||(o.items||[]).find(p=>A(p)&&(!p.key||c(p.key)===u)&&c(p.text)===c(i));if(h)return h;const g={text:String(i),key:u,icon:GROUP_ICON,items:[],synthesised:!0},f=o.items.findIndex(p=>l.has(p));return o.items.splice(f===-1?o.items.length:f,0,g),g};for(const o of s){const{item:i}=o;if(i.url&&y.has(i.url))continue;i.url&&y.add(i.url);const u=T.has(c(i.text));if(u&&!n)continue;const h=b({},i);n&&(h.items=[],h.$tool=O(o,{persisted:!1}),u&&(h.hidden=!0));let f=(o.parent?findFolder(a,o.parent)||$(a,_=>!P(_)&&c(_.text)===c(o.parent)):null)||findFolder(a,o.folder||i.folder)||L();f.items=Array.isArray(f.items)?f.items:[];const p=typeof(o.group||i.group)=="string"?(o.group||i.group).trim():"";p&&slugify(p)&&(f=k(f,p),f.items=Array.isArray(f.items)?f.items:[]),f.items.push(h),l.add(h)}return a}export const LOCKED_TOOLS=[{folder:"overview",item:{text:"Dashboard",url:"#/",icon:"home"}},{folder:"content",item:{text:"Site menus",url:"#/menus",icon:"menu",permission:"menus"}},{folder:"system",item:{text:"Users",url:"#/users",icon:"users",permission:"users"}}];export function isLockedUrl(e){return LOCKED_TOOLS.some(t=>t.item.url===e)}export function ensureLockedTools(e,{leaveInHiddenFolders:t=!1}={}){const n=w(e);for(const{folder:r,item:s}of LOCKED_TOOLS){const d=I(n,l=>l?.url===s.url),y=d.find(l=>!l.some(k=>k.restricted));if(!y&&d.length)continue;const m=y?.at(-1);if(m){if(m.hidden&&delete m.hidden,!y.slice(0,-1).some(o=>o.hidden)||t)continue;const k=y.at(-2);k.items=k.items.filter(o=>o!==m)}const a=c(r),T=I(n,l=>A(l)&&c(l.key)===a).filter(U),S=I(n,l=>A(l)&&!l.key&&c(l.text)===a).filter(U),x=(T[0]||S[0])?.at(-1)||null,L=m||{...s};x?(x.items=Array.isArray(x.items)?x.items:[],x.items.push(L)):n.push(L)}return n}export function insertFoldersBeforeSystem(e,t){const n=(t||[]).filter(Boolean);if(!n.length)return e;const r=["system","documentation"],s=e.findIndex(d=>r.includes(c(d?.key||d?.text)));return s===-1?[...e,...n]:[...e.slice(0,s),...n,...e.slice(s)]}export function canAny(e,t){return t?!e||!e.length?!1:e.includes(t)?!0:["read","create","update","delete"].some(n=>e.includes(`${t}.${n}`)):!0}export function filterByPermission(e,t,n=null){const r=[];for(const s of e){if(s.hidden||s.permission&&!canAny(t,s.permission))continue;const d=typeof s.url=="string"?s.url:"";if(n&&d.startsWith("#/")&&!F(n,d.slice(1)))continue;const y=Array.isArray(s.items)&&s.items.length>0,m=y?filterByPermission(s.items,t,n):[];y&&!m.length&&!d||r.push({...s,items:m})}return r}export function sidebarStateKey(e,t=null){const n=String(e?.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),r=String(e?.url||"").match(/^#\/projects\/([^/]+)/);let s="";if(r)try{s=`project-${decodeURIComponent(r[1])}.`}catch{s=`project-${r[1]}.`}return t?e?.key?`sidebar.expanded.key.${c(e.key)}`:`${t}/${s}${n}`:`sidebar.expanded.${s}${n}`}export function pruneEmptyToolsFolder(e){return(e||[]).filter(t=>A(t)?!(c(t.key)===TOOLS_FOLDER_KEY||!t.key&&t.text===TOOLS_FOLDER_TEXT)||Array.isArray(t.items)&&t.items.length>0:!0)}
@@ -36,7 +36,7 @@
36
36
  <!-- General -->
37
37
  <div class="tab-panel active">
38
38
  <div class="aex-card">
39
- <div class="aex-card-title">What it is</div>
39
+ <div class="aex-card-title"><span data-icon="info" data-icon-size="16" aria-hidden="true"></span>What it is</div>
40
40
  <div class="aex-grid">
41
41
  <div class="aex-wide">
42
42
  <label class="form-label" for="action-title">Title <span class="aex-req">*</span></label>
@@ -80,7 +80,7 @@
80
80
  <!-- Trigger -->
81
81
  <div class="tab-panel">
82
82
  <div class="aex-card">
83
- <div class="aex-card-title">The button
83
+ <div class="aex-card-title"><span data-icon="mouse-pointer" data-icon-size="16" aria-hidden="true"></span>The button
84
84
  <span data-help-title="Trigger" data-help="How the action is started. Manual actions appear as a button on each entry of the collection (and, when they have a transition, on the site in an interactive [collection] with the transitions attribute)."></span></div>
85
85
  <div class="aex-grid">
86
86
  <div>
@@ -139,7 +139,7 @@
139
139
  <!-- Steps -->
140
140
  <div class="tab-panel">
141
141
  <div class="aex-card">
142
- <div class="aex-card-title">Steps
142
+ <div class="aex-card-title"><span data-icon="list" data-icon-size="16" aria-hidden="true"></span>Steps
143
143
  <span data-help-title="Steps" data-help="What the action does, in order. Right-click a step (or ⋮, or Shift+F10 on its header) to move it up or down, duplicate or remove it. Nothing is saved until you Save."></span>
144
144
  <span class="aex-spacer"></span>
145
145
  <select id="add-step-type" class="form-input aex-add-type" aria-label="Step to add" data-model="addType.value">
@@ -156,18 +156,20 @@
156
156
  <p class="aex-note" data-bind-hidden="stepCount.value">No steps yet - add one to say what this action does.</p>
157
157
  <div class="aex-steps" id="action-steps-list"></div>
158
158
  </div>
159
- <div class="aex-card">
160
- <div class="aex-card-title">Template variables
161
- <span data-help="Any text in a step may use these; they are filled in when the action runs, for the entry it runs on and the person running it."></span></div>
162
- <!-- Filled from the view as text, after binding: {{…}} in the page would read as bindings. -->
163
- <table class="aex-vars"></table>
164
- </div>
159
+ <details class="aex-card dm-admin-section">
160
+ <summary class="aex-card-title dm-admin-section-title dm-section--neutral"><span data-icon="code" data-icon-size="16" aria-hidden="true"></span>Template variables
161
+ <span data-help="Any text in a step may use these; they are filled in when the action runs, for the entry it runs on and the person running it."></span></summary>
162
+ <div class="dm-admin-section-content">
163
+ <!-- Filled from the view as text, after binding: {{…}} in the page would read as bindings. -->
164
+ <table class="aex-vars"></table>
165
+ </div>
166
+ </details>
165
167
  </div>
166
168
 
167
169
  <!-- Access -->
168
170
  <div class="tab-panel">
169
171
  <div class="aex-card">
170
- <div class="aex-card-title">Who can run it
172
+ <div class="aex-card-title dm-section--info"><span data-icon="shield" data-icon-size="16" aria-hidden="true"></span>Who can run it
171
173
  <span data-help-title="Allowed roles" data-help="Any role ticked here is enough: its holders may run the action, and so may everyone in a more senior role (the same rule as page and view visibility). Ticking admin and hr lets both in. Roles come from System › Roles; one the action names that no longer exists is kept and flagged, and admits nobody but the level-0 role."></span></div>
172
174
  <div class="aex-roles" data-each="roleRows.value key=name"></div>
173
175
  <small class="aex-err" data-bind-hidden="!err.value.roles" data-bind-text="err.value.roles"></small>
@@ -73,7 +73,7 @@
73
73
 
74
74
  <div class="tab-panel">
75
75
  <div class="epx-card">
76
- <div class="epx-card-title">Filters
76
+ <div class="epx-card-title"><span data-icon="filter" data-icon-size="16" aria-hidden="true"></span>Filters
77
77
  <span data-help-title="Filters" data-help="Every filter must match. A value can be a literal, {{params.name}} (from a :name segment - required) or {{query.name}} (from ?name= - optional: the filter is dropped when the caller leaves it out). Right-click a filter (or ⋮) to move, duplicate or remove it."></span>
78
78
  <span class="epx-spacer"></span>
79
79
  <button type="button" class="btn btn-sm btn-ghost" id="ep-add-filter"><span data-icon="plus" data-icon-size="13"></span> Add filter</button>
@@ -105,7 +105,7 @@
105
105
 
106
106
  <div class="tab-panel">
107
107
  <div class="epx-card">
108
- <div class="epx-card-title">Response fields
108
+ <div class="epx-card-title"><span data-icon="list" data-icon-size="16" aria-hidden="true"></span>Response fields
109
109
  <span data-help-title="Response fields" data-help="Tick the fields callers get back - none ticked returns every field. Anything not ticked cannot be read or filtered on through this endpoint. Right-click the list (or ⋮) to tick all, none, or invert."></span>
110
110
  <span class="epx-spacer"></span>
111
111
  <button type="button" class="epx-kebab" id="ep-fields-more" aria-label="Field actions" title="Actions">⋮</button>
@@ -24,7 +24,7 @@
24
24
  <!-- Settings -->
25
25
  <div class="tab-panel active">
26
26
  <div class="cex-card">
27
- <div class="cex-card-title">Collection
27
+ <div class="cex-card-title"><span data-icon="database" data-icon-size="16" aria-hidden="true"></span>Collection
28
28
  <span data-help-title="Collections" data-help="A collection is a set of entries that share the same fields - products, bookings, team members. Its entries are edited under Data › Collections, shown on pages with the [collection] shortcode, and can be read by the public API if you allow it on the API &amp; Export tab."></span></div>
29
29
  <div class="cex-grid">
30
30
  <div class="cex-wide">
@@ -54,7 +54,7 @@
54
54
  </div>
55
55
 
56
56
  <div class="cex-card">
57
- <div class="cex-card-title">Entry form
57
+ <div class="cex-card-title dm-section--neutral"><span data-icon="list" data-icon-size="16" aria-hidden="true"></span>Entry form
58
58
  <span data-help-title="Entry form" data-help="How the form for adding and editing an entry is laid out. Stacked puts one field per row; Grid places them in columns, and each field can span several columns or the full width (set on the field)."></span></div>
59
59
  <div class="cex-grid">
60
60
  <div>
@@ -81,7 +81,7 @@
81
81
  <!-- Fields -->
82
82
  <div class="tab-panel">
83
83
  <div class="cex-card">
84
- <div class="cex-card-title">Fields
84
+ <div class="cex-card-title"><span data-icon="list" data-icon-size="16" aria-hidden="true"></span>Fields
85
85
  <span data-help-title="Fields" data-help="What every entry holds. Click a field to edit it; drag it by its grip, or right-click it (or ⋮, or Shift+F10) to move it up or down, duplicate it or remove it. Alt+↑ / Alt+↓ move the focused field. Changes are kept here until you Save."></span>
86
86
  <span class="cex-spacer"></span>
87
87
  <button type="button" class="btn btn-sm btn-ghost" data-on-click="addField"><span data-icon="plus" data-icon-size="13"></span> Add field</button>
@@ -94,7 +94,7 @@
94
94
  <!-- API & Export -->
95
95
  <div class="tab-panel">
96
96
  <div class="cex-card">
97
- <div class="cex-card-title">Public API
97
+ <div class="cex-card-title dm-section--info"><span data-icon="link" data-icon-size="16" aria-hidden="true"></span>Public API
98
98
  <span data-help-title="Public API" data-help="What the public API may do with this collection, and who may do it: Public is anyone; Token needs an API token (System › API Tokens); the others need a signed-in user with at least that role. The admin itself is always governed by roles and permissions, not by these switches."></span></div>
99
99
  <p class="cex-note">At <code data-bind-text="apiPath.value"></code></p>
100
100
  <div class="cex-ops">
@@ -121,7 +121,7 @@
121
121
  </div>
122
122
 
123
123
  <div class="cex-card">
124
- <div class="cex-card-title">Export
124
+ <div class="cex-card-title dm-section--neutral"><span data-icon="download" data-icon-size="16" aria-hidden="true"></span>Export
125
125
  <span data-help-title="Export" data-help="Lets visitors download the entries a [collection] display shows, from its right-click menu - as CSV and/or JSON. Who may export is a role: Public is anyone."></span></div>
126
126
  <div class="cex-ops">
127
127
  <div class="cex-op"><strong>Export</strong>
@@ -142,7 +142,7 @@
142
142
  <!-- Storage (Pro) -->
143
143
  <div class="tab-panel">
144
144
  <div class="cex-card">
145
- <div class="cex-card-title">Storage
145
+ <div class="cex-card-title dm-section--info"><span data-icon="database" data-icon-size="16" aria-hidden="true"></span>Storage
146
146
  <span data-help-title="Storage" data-help="Where this collection's entries live: in a file on this server (the default), or in MongoDB (Pro). Switching an existing collection asks whether to move its entries across when you Save - moving copies each entry to the new store. Export the entries first if they matter."></span></div>
147
147
  <div class="cex-grid">
148
148
  <div>
@@ -42,7 +42,7 @@
42
42
  </div>
43
43
 
44
44
  <div class="mpx-card" data-bind-hidden="loading.value || loadError.value">
45
- <div class="mpx-card-title">Account
45
+ <div class="mpx-card-title"><span data-icon="user" data-icon-size="16" aria-hidden="true"></span>Account
46
46
  <span data-help="Your name is how you appear in the admin and on anything you author. Your email is what you sign in with, and where password resets are sent."></span></div>
47
47
  <div class="mpx-grid">
48
48
  <div>
@@ -60,7 +60,7 @@
60
60
  </div>
61
61
 
62
62
  <div class="mpx-card" data-bind-hidden="loading.value || loadError.value">
63
- <div class="mpx-card-title">Password
63
+ <div class="mpx-card-title dm-section--info"><span data-icon="lock" data-icon-size="16" aria-hidden="true"></span>Password
64
64
  <span data-help-title="Changing your password" data-help="Leave both boxes empty to keep your current password. A new one needs at least 8 characters, typed twice. It is never kept as a draft - if you leave the screen, you type it again."></span></div>
65
65
  <div class="mpx-grid">
66
66
  <div>
@@ -79,7 +79,7 @@
79
79
  </div>
80
80
 
81
81
  <div class="mpx-card" data-bind-hidden="loading.value || loadError.value || !hasProfile.value">
82
- <div class="mpx-card-title">Profile
82
+ <div class="mpx-card-title dm-section--neutral"><span data-icon="user" data-icon-size="16" aria-hidden="true"></span>Profile
83
83
  <span data-help="Extra details about you. Which ones are asked for is set by an administrator (the User Profiles collection)."></span></div>
84
84
  <div class="mpx-grid mpx-profile"></div>
85
85
  </div>
@@ -12,7 +12,7 @@
12
12
  </div>
13
13
 
14
14
  <div class="psx-card">
15
- <div class="psx-card-title">Project details
15
+ <div class="psx-card-title"><span data-icon="folder" data-icon-size="16" aria-hidden="true"></span>Project details
16
16
  <span data-help-title="Project details" data-help="A project groups pages, collections, forms and the rest for the sidebar, and can limit what a user sees. Ctrl+S saves; leaving with unsaved changes asks first and keeps them for your next visit."></span>
17
17
  </div>
18
18
  <div class="psx-grid">
@@ -56,7 +56,7 @@
56
56
  </div>
57
57
 
58
58
  <div class="psx-card psx-danger" data-bind-hidden="!dangerShown">
59
- <div class="psx-card-title">Danger zone
59
+ <div class="psx-card-title dm-section--danger"><span data-icon="alert-triangle" data-icon-size="16" aria-hidden="true"></span>Danger zone
60
60
  <span data-help-title="Danger zone" data-help="Untagging takes the project tag off everything filed under it: the artefacts stay and belong to Core. A project can only be deleted once nothing is filed under it. Both ask first."></span>
61
61
  </div>
62
62
  <p class="psx-note" data-bind-text="filedText.value"></p>
@@ -31,13 +31,13 @@
31
31
  <!-- General -->
32
32
  <div class="tab-panel active">
33
33
  <div class="stx-card" data-bind-hidden="!problemsText.value">
34
- <div class="stx-card-title">Needs attention
34
+ <div class="stx-card-title"><span data-icon="alert-circle" data-icon-size="16" aria-hidden="true"></span>Needs attention
35
35
  <span data-help-title="Needs attention" data-help="Settings that are saved but quietly wrong - the same list the badge on Site Settings in the sidebar counts. They go away once fixed and saved."></span></div>
36
36
  <div class="stx-problems" data-each="problems.value key=text"></div>
37
37
  </div>
38
38
  <div class="stx-card-columns">
39
39
  <div class="stx-card">
40
- <div class="stx-card-title">Site</div>
40
+ <div class="stx-card-title dm-section--success"><span data-icon="globe" data-icon-size="16" aria-hidden="true"></span>Site</div>
41
41
  <div class="stx-grid">
42
42
  <div>
43
43
  <label class="form-label" for="stx-title">Site title
@@ -57,7 +57,7 @@
57
57
  </div>
58
58
  </div>
59
59
  <div class="stx-card">
60
- <div class="stx-card-title">Admin</div>
60
+ <div class="stx-card-title dm-section--info"><span data-icon="layout" data-icon-size="16" aria-hidden="true"></span>Admin</div>
61
61
  <div class="stx-grid">
62
62
  <div>
63
63
  <label class="form-label" for="stx-admin-home">Admin home
@@ -90,7 +90,7 @@
90
90
  <!-- Branding -->
91
91
  <div class="tab-panel">
92
92
  <div class="stx-card-columns">
93
- <div class="stx-card"><div class="stx-card-title">Public site</div><div class="stx-grid">
93
+ <div class="stx-card"><div class="stx-card-title"><span data-icon="globe" data-icon-size="16" aria-hidden="true"></span>Public site</div><div class="stx-grid">
94
94
  <div><label class="form-label" for="stx-brandText">Brand name</label>
95
95
  <input id="stx-brandText" type="text" class="form-input" placeholder="My Site" data-model="brandText.value">
96
96
  </div>
@@ -108,7 +108,7 @@
108
108
  </div>
109
109
 
110
110
  </div><p class="text-muted">The logo takes priority over the icon. Set the browser icon in the Favicon tab.</p></div>
111
- <div class="stx-card"><div class="stx-card-title">Admin
111
+ <div class="stx-card"><div class="stx-card-title"><span data-icon="layout" data-icon-size="16" aria-hidden="true"></span>Admin
112
112
  <span data-help-title="Admin name and mark" data-help="The name and logo or icon at the top of the admin - in the top bar, the sidebar and the browser tab - for everyone who signs in. Leave the name empty to use the site title."></span></div><div class="stx-grid">
113
113
  <div><label class="form-label" for="stx-adminBrandTitle">Admin name</label>
114
114
  <input id="stx-adminBrandTitle" type="text" class="form-input" placeholder="Uses the site title when empty" data-model="adminBrandTitle.value">
@@ -129,7 +129,7 @@
129
129
  <!-- Favicon -->
130
130
  <div class="tab-panel">
131
131
  <div class="stx-card-columns">
132
- <div class="stx-card"><div class="stx-card-title">Generate favicon.ico</div>
132
+ <div class="stx-card"><div class="stx-card-title"><span data-icon="image" data-icon-size="16" aria-hidden="true"></span>Generate favicon.ico</div>
133
133
  <p>Choose or upload an image. Generate a single ICO file with 16, 32, 48 and 256 pixel icons. Images keep their proportions with transparent padding.</p>
134
134
  <label class="form-label" for="stx-favicon-source">Source image</label>
135
135
  <input id="stx-favicon-source" class="form-input" type="text" placeholder="/media/logo.png" data-model="faviconSource.value">
@@ -138,7 +138,7 @@
138
138
  <p class="text-danger" data-bind-text="faviconError.value" data-bind-hidden="!faviconError.value"></p>
139
139
  <p data-bind-hidden="!faviconGenerated.value">Generated. Save Site Settings to apply it. <a data-bind-href="faviconGenerated.value" download="favicon.ico">Download favicon.ico</a></p>
140
140
  </div>
141
- <div class="stx-card"><div class="stx-card-title">Browser icon</div>
141
+ <div class="stx-card"><div class="stx-card-title"><span data-icon="image" data-icon-size="16" aria-hidden="true"></span>Browser icon</div>
142
142
  <div><label class="form-label" for="stx-favicon">Browser icon (favicon)</label>
143
143
  <input id="stx-favicon" type="text" class="form-input" placeholder="/media/favicon.png" data-model="favicon.value">
144
144
  <button type="button" class="btn btn-sm btn-ghost" data-on-click="pickBrandImage('favicon')">Choose or upload image</button>
@@ -153,7 +153,7 @@
153
153
  <!-- SEO -->
154
154
  <div class="tab-panel">
155
155
  <div class="stx-card">
156
- <div class="stx-card-title">Search engines
156
+ <div class="stx-card-title"><span data-icon="search" data-icon-size="16" aria-hidden="true"></span>Search engines
157
157
  <span data-help-title="SEO defaults" data-help="Used on every page that does not set its own. A page's title becomes “Page title” + separator + default site title."></span></div>
158
158
  <div class="stx-grid">
159
159
  <div>
@@ -178,7 +178,7 @@
178
178
  <div class="tab-panel">
179
179
  <div class="stx-card-columns">
180
180
  <div class="stx-card">
181
- <div class="stx-card-title">Footer</div>
181
+ <div class="stx-card-title"><span data-icon="align-left" data-icon-size="16" aria-hidden="true"></span>Footer</div>
182
182
  <div class="stx-grid">
183
183
  <div class="stx-wide">
184
184
  <label class="form-label" for="stx-footer-copy">Copyright text</label>
@@ -238,7 +238,7 @@
238
238
  </div>
239
239
  </div>
240
240
  <div class="stx-card">
241
- <div class="stx-card-title">Social media icons
241
+ <div class="stx-card-title"><span data-icon="share-2" data-icon-size="16" aria-hidden="true"></span>Social media icons
242
242
  <span data-help="The full address of each profile - its icon then shows in the footer. Leave one empty to hide its icon."></span></div>
243
243
  <div class="stx-grid">
244
244
  <div><label class="form-label" for="stx-s-tw">X / Twitter</label>
@@ -267,7 +267,7 @@
267
267
  <span><strong>No SMTP host.</strong> Mail is going to a throwaway test inbox and will <strong>not</strong>
268
268
  reach anyone - password resets and form notifications will look like they sent. Set a host and save.</span></p>
269
269
  <div class="stx-card">
270
- <div class="stx-card-title">SMTP server
270
+ <div class="stx-card-title dm-section--info"><span data-icon="server" data-icon-size="16" aria-hidden="true"></span>SMTP server
271
271
  <span data-help-title="SMTP" data-help="The mail server the site sends through - your email provider's (for example smtp.gmail.com, or your host's). Its username and password are the mail account's. The password is never shown again once saved: leave it empty to keep it, type a new one to change it."></span></div>
272
272
  <div class="stx-grid">
273
273
  <div>
@@ -300,7 +300,7 @@
300
300
  </div>
301
301
  </div>
302
302
  <div class="stx-card">
303
- <div class="stx-card-title">Sender
303
+ <div class="stx-card-title dm-section--info"><span data-icon="user" data-icon-size="16" aria-hidden="true"></span>Sender
304
304
  <span data-help="Who the site's mail comes from. Most providers only accept a From address on the account (or domain) you sign in with."></span></div>
305
305
  <div class="stx-grid">
306
306
  <div>
@@ -314,7 +314,7 @@
314
314
  </div>
315
315
  </div>
316
316
  <div class="stx-card">
317
- <div class="stx-card-title">Send a test email
317
+ <div class="stx-card-title dm-section--info"><span data-icon="send" data-icon-size="16" aria-hidden="true"></span>Send a test email
318
318
  <span data-help="Sends through the SAVED settings - Save first if you have changed anything above. Leave the address empty to send to the From address."></span></div>
319
319
  <div class="stx-inline">
320
320
  <input type="email" class="form-input stx-test-to" placeholder="recipient@example.com" aria-label="Send the test to" data-model="testTo.value">
@@ -403,7 +403,7 @@
403
403
  <!-- Cookie consent -->
404
404
  <div class="tab-panel">
405
405
  <div class="stx-card">
406
- <div class="stx-card-title">Cookie consent banner
406
+ <div class="stx-card-title"><span data-icon="shield" data-icon-size="16" aria-hidden="true"></span>Cookie consent banner
407
407
  <span data-help-title="Cookie consent" data-help="The banner that asks visitors which cookies they accept. Necessary cookies are always allowed; the categories below are the ones visitors can turn off. Raise the consent version to ask everyone again - after changing what the site stores, for example."></span></div>
408
408
  <div class="stx-grid">
409
409
  <div class="stx-wide stx-setting"><label class="form-label" for="stx-cc-on">Show the banner</label>
@@ -431,7 +431,7 @@
431
431
  </div>
432
432
  </div>
433
433
  <div class="stx-card">
434
- <div class="stx-card-title">Categories</div>
434
+ <div class="stx-card-title"><span data-icon="list" data-icon-size="16" aria-hidden="true"></span>Categories</div>
435
435
  <div class="stx-grid">
436
436
  <div class="stx-setting"><label class="form-label" for="stx-cc-fn">Functional</label>
437
437
  <input id="stx-cc-fn" type="checkbox" class="form-switch" data-model="ccShowFunctional.value"></div>
@@ -442,7 +442,7 @@
442
442
  </div>
443
443
  </div>
444
444
  <div class="stx-card">
445
- <div class="stx-card-title">Links and button text</div>
445
+ <div class="stx-card-title"><span data-icon="link" data-icon-size="16" aria-hidden="true"></span>Links and button text</div>
446
446
  <div class="stx-grid">
447
447
  <div><label class="form-label" for="stx-cc-pt">Privacy policy text</label>
448
448
  <input id="stx-cc-pt" type="text" class="form-input" placeholder="Privacy Policy" data-model="ccPrivacyText.value"></div>
@@ -468,7 +468,7 @@
468
468
  <!-- Analytics -->
469
469
  <div class="tab-panel">
470
470
  <div class="stx-card">
471
- <div class="stx-card-title">Google Analytics
471
+ <div class="stx-card-title"><span data-icon="bar-chart" data-icon-size="16" aria-hidden="true"></span>Google Analytics
472
472
  <span data-help-title="Google Analytics" data-help="Paste the measurement ID of your Google Analytics 4 web stream (Admin > Data streams). Google Analytics sets cookies, so it loads only for visitors who accept Analytics cookies in the cookie banner - the banner is shown whenever an ID is set. Leave it empty to switch Google Analytics off. The built-in Analytics Tool is separate and needs no cookies."></span></div>
473
473
  <div class="stx-grid">
474
474
  <div class="stx-wide"><label class="form-label" for="stx-ga-id">Measurement ID</label>
@@ -484,7 +484,7 @@
484
484
  <div class="tab-panel">
485
485
  <div class="stx-card-columns">
486
486
  <div class="stx-card">
487
- <div class="stx-card-title">Breadcrumb trail
487
+ <div class="stx-card-title"><span data-icon="chevron-right" data-icon-size="16" aria-hidden="true"></span>Breadcrumb trail
488
488
  <span data-help-title="Breadcrumbs" data-help="A small frosted pill fixed to a corner of every public page, showing where the page sits (Home › Services › Web). A page can switch it off with “Show Breadcrumbs” in the page editor."></span></div>
489
489
  <div class="stx-grid">
490
490
  <div class="stx-wide stx-setting"><label class="form-label" for="stx-bc-on">Show breadcrumbs</label>
@@ -495,7 +495,7 @@
495
495
  <p class="stx-hint">Shows the path from Home to the current page. Individual pages can hide their trail in the page editor.</p>
496
496
  </div>
497
497
  <div class="stx-card">
498
- <div class="stx-card-title">Placement</div>
498
+ <div class="stx-card-title"><span data-icon="layout" data-icon-size="16" aria-hidden="true"></span>Placement</div>
499
499
  <div class="stx-grid">
500
500
  <div class="stx-wide">
501
501
  <span class="form-label">Corner</span>
@@ -520,7 +520,7 @@
520
520
  <!-- Cache -->
521
521
  <div class="tab-panel">
522
522
  <div class="stx-card">
523
- <div class="stx-card-title">Response cache
523
+ <div class="stx-card-title dm-section--neutral"><span data-icon="database" data-icon-size="16" aria-hidden="true"></span>Response cache
524
524
  <span data-help-title="Response cache" data-help="Public pages are kept rendered, per address and per visitor role, so the next visitor gets them at once. Saving a page, collection, form, block, view, menu or these settings clears what it affects automatically; Clear empties it all. Turning it off makes every visit render afresh - useful while debugging, slower for visitors."></span>
525
525
  <span class="stx-spacer"></span>
526
526
  <button type="button" class="btn btn-sm btn-ghost" data-on-click="refreshCache"><span data-icon="refresh-cw" data-icon-size="14"></span> Refresh</button>
@@ -534,7 +534,7 @@
534
534
  </div>
535
535
  </div>
536
536
  <div class="stx-card">
537
- <div class="stx-card-title">Cached pages <span class="stx-count" data-bind-text="cacheRows.value.length"></span>
537
+ <div class="stx-card-title dm-section--neutral"><span data-icon="layers" data-icon-size="16" aria-hidden="true"></span>Cached pages <span class="stx-count" data-bind-text="cacheRows.value.length"></span>
538
538
  <span data-help="What is in the cache now, newest first: the page, the visitor role it was rendered for, and when it expires. Right-click one (or ⋮) to open the page or copy its key."></span>
539
539
  <span class="stx-spacer"></span>
540
540
  <input type="search" class="form-input stx-search" placeholder="Filter…" aria-label="Filter cached pages" data-model="cacheQuery.value">
@@ -24,7 +24,7 @@
24
24
  <!-- Themes -->
25
25
  <div class="tab-panel active">
26
26
  <div class="thx-card">
27
- <div class="thx-card-title">Live now
27
+ <div class="thx-card-title dm-section--success"><span data-icon="activity" data-icon-size="16" aria-hidden="true"></span>Live now
28
28
  <span data-help-title="Live now" data-help="What the public site and this admin show at the moment - the saved settings, not what you are editing. With day/night switching on, the site changes theme at the times below, on each visitor's own clock."></span></div>
29
29
  <div class="thx-chips">
30
30
  <span class="thx-chip is-ok" data-bind-text="liveSite.value"></span>
@@ -35,7 +35,7 @@
35
35
  </div>
36
36
 
37
37
  <div class="thx-card">
38
- <div class="thx-card-title">Site &amp; admin</div>
38
+ <div class="thx-card-title"><span data-icon="layout" data-icon-size="16" aria-hidden="true"></span>Site &amp; admin</div>
39
39
  <div class="thx-grid">
40
40
  <div>
41
41
  <label class="form-label" for="thx-site">Site theme
@@ -75,7 +75,7 @@
75
75
  </div>
76
76
 
77
77
  <div class="thx-card">
78
- <div class="thx-card-title">Theme switcher</div>
78
+ <div class="thx-card-title"><span data-icon="droplet" data-icon-size="16" aria-hidden="true"></span>Theme switcher</div>
79
79
  <div class="thx-setting">
80
80
  <label class="form-label" for="thx-sw">Show a theme switcher on the public site
81
81
  <span data-help-title="Theme switcher" data-help="A floating disc of theme dots that lets a visitor pick their own theme, remembered in their browser. It offers the built-in themes only - custom themes cannot be switched to from the page."></span></label>
@@ -105,7 +105,7 @@
105
105
  </div>
106
106
 
107
107
  <div class="thx-card">
108
- <div class="thx-card-title">Typography</div>
108
+ <div class="thx-card-title dm-section--neutral"><span data-icon="type" data-icon-size="16" aria-hidden="true"></span>Typography</div>
109
109
  <div class="thx-grid">
110
110
  <div>
111
111
  <label class="form-label" for="thx-font">Font family
@@ -160,7 +160,7 @@
160
160
  <!-- Element overrides -->
161
161
  <div class="tab-panel">
162
162
  <div class="thx-card">
163
- <div class="thx-card-title">Element overrides
163
+ <div class="thx-card-title dm-section--neutral"><span data-icon="sliders" data-icon-size="16" aria-hidden="true"></span>Element overrides
164
164
  <span data-help-title="Element overrides" data-help="An override re-themes one region of the page: give it a CSS selector, then adopt another theme there wholesale, or set individual --dm-* tokens. Everything inside the region follows, including components drawn later, because a Domma theme is nothing but a block of --dm-* values. Click an override to edit it; right-click it (or ⋮, or Shift+F10) to switch it off, duplicate, move or remove it. Order matters: later overrides win where they overlap."></span>
165
165
  <span class="thx-spacer"></span>
166
166
  <button type="button" class="btn btn-sm btn-primary" data-on-click="addOverride">
@@ -170,15 +170,17 @@
170
170
  <div class="thx-list" data-each="rows.value key=id"></div>
171
171
  </div>
172
172
 
173
- <div class="thx-card">
174
- <div class="thx-card-title">Generated CSS
173
+ <details class="thx-card dm-admin-section">
174
+ <summary class="thx-card-title dm-admin-section-title dm-section--neutral"><span data-icon="code" data-icon-size="16" aria-hidden="true"></span>Generated CSS
175
175
  <span data-help="Exactly what is injected into every public page for the overrides, ahead of your custom CSS - so hand-written CSS still wins. It follows your edits, before you save."></span>
176
176
  <span class="thx-spacer"></span>
177
177
  <button type="button" class="btn btn-sm btn-ghost" data-on-click="refreshCss" title="Compile again">
178
178
  <span data-icon="refresh-cw" data-icon-size="14"></span> Refresh</button>
179
+ </summary>
180
+ <div class="dm-admin-section-content">
181
+ <pre class="thx-css" data-bind-class="cssError.value && 'is-bad'" data-bind-text="cssText.value">(nothing yet)</pre>
179
182
  </div>
180
- <pre class="thx-css" data-bind-class="cssError.value && 'is-bad'" data-bind-text="cssText.value">(nothing yet)</pre>
181
- </div>
183
+ </details>
182
184
  </div>
183
185
  </div>
184
186
  </div>
@@ -36,7 +36,7 @@
36
36
 
37
37
  <fieldset class="uex-fields" data-bind-disabled="locked.value || saving.value" data-bind-hidden="loading.value || loadError.value">
38
38
  <div class="uex-card">
39
- <div class="uex-card-title">Account
39
+ <div class="uex-card-title"><span data-icon="user" data-icon-size="16" aria-hidden="true"></span>Account
40
40
  <span data-help="The name shown in the admin and on anything they author. The email is what they sign in with, and where password resets go - no two accounts may share one."></span></div>
41
41
  <div class="uex-grid">
42
42
  <div>
@@ -74,7 +74,7 @@
74
74
  </div>
75
75
 
76
76
  <div class="uex-card">
77
- <div class="uex-card-title">Roles
77
+ <div class="uex-card-title dm-section--info"><span data-icon="shield" data-icon-size="16" aria-hidden="true"></span>Roles
78
78
  <span data-help-title="Roles" data-help="The primary role is the badge shown beside their name, and what seniority is compared on. Additional roles add their permissions on top - someone can be, say, a recruiter AND a content editor; the most senior role they hold decides who may manage them. You can only give roles below your own; a role they already hold can always be kept."></span></div>
79
79
  <div class="uex-grid">
80
80
  <div>
@@ -92,7 +92,7 @@
92
92
  </div>
93
93
 
94
94
  <div class="uex-card" data-bind-hidden="!hasProjects.value">
95
- <div class="uex-card-title">Projects
95
+ <div class="uex-card-title dm-section--info"><span data-icon="folder" data-icon-size="16" aria-hidden="true"></span>Projects
96
96
  <span data-help-title="Projects" data-help="Owned by: where this user is filed in the sidebar's Projects section - administrative only. Access: which projects' pages, collections and forms they can see and edit - leave it all unticked for no restriction. Core is always visible."></span></div>
97
97
  <div class="uex-grid">
98
98
  <div>
@@ -108,7 +108,7 @@
108
108
  </div>
109
109
 
110
110
  <div class="uex-card" data-bind-hidden="!hasProfile.value">
111
- <div class="uex-card-title">Profile
111
+ <div class="uex-card-title dm-section--neutral"><span data-icon="user" data-icon-size="16" aria-hidden="true"></span>Profile
112
112
  <span data-help="Extra details about this user. Which ones are asked for is set in the User Profiles collection."></span></div>
113
113
  <div class="uex-grid uex-profile"></div>
114
114
  </div>
@@ -24,7 +24,7 @@
24
24
  <!-- Source -->
25
25
  <div class="tab-panel active">
26
26
  <div class="vex-card">
27
- <div class="vex-card-title">What it is, and what it reads</div>
27
+ <div class="vex-card-title"><span data-icon="database" data-icon-size="16" aria-hidden="true"></span>What it is, and what it reads</div>
28
28
  <div class="vex-grid">
29
29
  <div class="vex-wide">
30
30
  <label class="form-label" for="view-title">Title <span class="vex-req">*</span></label>
@@ -67,7 +67,7 @@
67
67
  <!-- Pipeline -->
68
68
  <div class="tab-panel">
69
69
  <div class="vex-card">
70
- <div class="vex-card-title">Stages <small class="vex-muted">run top to bottom</small>
70
+ <div class="vex-card-title"><span data-icon="list" data-icon-size="16" aria-hidden="true"></span>Stages <small class="vex-muted">run top to bottom</small>
71
71
  <span data-help-title="Pipeline" data-help="Each stage takes the rows from the one above and passes on its result: filter them ($match), order them ($sort), join another collection ($lookup), reshape or compute fields, group and total, keep or skip some, or count them. $match and $sort have a builder; the rest take their settings as JSON - only the inside, e.g. { &quot;from&quot;: …, &quot;as&quot;: … } for $lookup. Right-click a stage (or ⋮, or Shift+F10) to move, duplicate or delete it, or to see the results up to that stage."></span>
72
72
  <span class="vex-spacer"></span>
73
73
  <select id="add-stage-type" class="form-input vex-add-type" aria-label="Stage to add"></select>
@@ -81,7 +81,7 @@
81
81
  <!-- Display -->
82
82
  <div class="tab-panel">
83
83
  <div class="vex-card">
84
- <div class="vex-card-title">How it is shown</div>
84
+ <div class="vex-card-title dm-section--neutral"><span data-icon="layout" data-icon-size="16" aria-hidden="true"></span>How it is shown</div>
85
85
  <div class="vex-grid">
86
86
  <div>
87
87
  <label class="form-label" for="view-display-mode">Display mode
@@ -105,7 +105,7 @@
105
105
  </div>
106
106
  </div>
107
107
  <div class="vex-card" id="view-columns-section">
108
- <div class="vex-card-title">Columns
108
+ <div class="vex-card-title dm-section--neutral"><span data-icon="columns" data-icon-size="16" aria-hidden="true"></span>Columns
109
109
  <span data-help-title="Columns" data-help="Which fields the table shows, in order, and what each heading says. The field lists come from the source collection. With none, the first six fields of the first result are shown. Right-click a column (or ⋮) to move or remove it."></span></div>
110
110
  <div id="view-columns-builder" class="vex-cols"></div>
111
111
  </div>
@@ -114,7 +114,7 @@
114
114
  <!-- Access -->
115
115
  <div class="tab-panel">
116
116
  <div class="vex-card">
117
- <div class="vex-card-title">Who can see it
117
+ <div class="vex-card-title dm-section--info"><span data-icon="shield" data-icon-size="16" aria-hidden="true"></span>Who can see it
118
118
  <span data-help-title="Access" data-help="Signed-in users holding one of these roles can see the view's results - on pages ([view]) and at /api/views/&lt;slug&gt;/public. Public lets anyone, signed in or not. Admins always can."></span></div>
119
119
  <div id="view-roles-checkboxes" class="vex-roles"></div>
120
120
  <div class="vex-setting">
@@ -123,7 +123,7 @@
123
123
  </div>
124
124
  </div>
125
125
  <div class="vex-card">
126
- <div class="vex-card-title">Row-level access
126
+ <div class="vex-card-title dm-section--info"><span data-icon="lock" data-icon-size="16" aria-hidden="true"></span>Row-level access
127
127
  <span data-help-title="Row-level access" data-help="Each user sees only the results that are theirs. Owner: entries they created. Field Match: entries whose chosen field equals something about them (their id, email, name or role). Admins always see every result."></span></div>
128
128
  <div class="vex-setting">
129
129
  <label class="form-label" for="view-rowlevel-enabled">Show each user only their own results</label>
@@ -157,7 +157,7 @@
157
157
  <!-- Results -->
158
158
  <div class="tab-panel">
159
159
  <div class="vex-card">
160
- <div class="vex-card-title">Results <small class="vex-muted" data-bind-text="previewMeta.value"></small>
160
+ <div class="vex-card-title"><span data-icon="bar-chart" data-icon-size="16" aria-hidden="true"></span>Results <small class="vex-muted" data-bind-text="previewMeta.value"></small>
161
161
  <span data-help-title="Results" data-help="The pipeline as it stands now - saved or not - run against the source, first rows only. It follows your edits. Row-level access is not applied here: this is the admin's view."></span>
162
162
  <span class="vex-spacer"></span>
163
163
  <button type="button" class="btn btn-sm btn-ghost" data-bind-hidden="!upTo.value" data-on-click="allStages">Run every stage</button>
@@ -90,6 +90,12 @@
90
90
  "url": "#/content-api",
91
91
  "icon": "file-text",
92
92
  "permission": "settings"
93
+ },
94
+ {
95
+ "text": "Search",
96
+ "url": "#/search",
97
+ "icon": "search",
98
+ "permission": "settings"
93
99
  }
94
100
  ],
95
101
  "key": "content"
@@ -173,22 +179,11 @@
173
179
  "icon": "layout",
174
180
  "permission": "layouts"
175
181
  },
176
- {
177
- "text": "My Profile",
178
- "url": "#/my-profile",
179
- "icon": "user"
180
- },
181
182
  {
182
183
  "text": "API Tokens",
183
184
  "url": "#/api-tokens",
184
185
  "icon": "key",
185
186
  "permission": "api-tokens"
186
- },
187
- {
188
- "text": "Search",
189
- "url": "#/search",
190
- "icon": "search",
191
- "permission": "settings"
192
187
  }
193
188
  ],
194
189
  "key": "system"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.10.27",
3
+ "version": "1.10.30",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
package/server/server.js CHANGED
@@ -166,7 +166,7 @@ if (publicCorsPrefixes.length) {
166
166
  await app.register(multipart, { limits: { fileSize: serverConfig.uploads.maxFileSize } });
167
167
  await app.register(rateLimit, rateLimitOptions());
168
168
 
169
- registerAdminAssets(app, {root: ROOT, dommaDist: DOMMA_DIST});
169
+ const adminAssets = registerAdminAssets(app, {root: ROOT, dommaDist: DOMMA_DIST});
170
170
  // Text assets (scripts, stylesheets, SVG, JSON) from memory, compressed once
171
171
  // per encoding - services/assetCache.js. Its onSend hook must be added BEFORE
172
172
  // compress registers its own, so compress finds content-encoding set and
@@ -338,7 +338,7 @@ try {
338
338
  item: {text: 'Theme', url: '#/theme', icon: 'droplet', permission: 'theme'}
339
339
  });
340
340
  await ensureSidebarItem({
341
- groupText: 'System',
341
+ groupText: 'Content',
342
342
  item: {text: 'Search', url: '#/search', icon: 'search', permission: 'settings'}
343
343
  });
344
344
  // Restructure Documentation: Tutorials → submenu, add Components reference set.
@@ -699,11 +699,10 @@ registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'Roles',
699
699
  registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'Users', url: '#/users', icon: 'users', permission: 'users', countUrl: '/api/users/_count', inventory: true}});
700
700
  registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'Site Settings', url: '#/settings', icon: 'settings', permission: 'settings', countUrl: '/api/settings/_count'}});
701
701
  registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'Effects', url: '#/effects', icon: 'sparkles', permission: 'settings', countUrl: '/api/effects/_count', inventory: true}});
702
- registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'My Profile', url: '#/my-profile', icon: 'user', countUrl: '/api/auth/me/_count'}});
703
702
  registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'Layouts', url: '#/layouts', icon: 'layout', permission: 'layouts', countUrl: '/api/layouts/_count'}});
704
703
  registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'Plugins', url: '#/plugins', icon: 'package', permission: 'plugins', countUrl: '/api/plugins/_count', inventory: true}});
705
704
  registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'API Tokens', url: '#/api-tokens', icon: 'key', permission: 'api-tokens', countUrl: '/api/api-tokens/_count'}});
706
- registerSidebarItem({folder: 'system', item: {source: 'Built in', text: 'Search', url: '#/search', icon: 'search', permission: 'settings', countUrl: '/api/search/_count', inventory: true}});
705
+ registerSidebarItem({folder: 'content', item: {source: 'Built in', text: 'Search', url: '#/search', icon: 'search', permission: 'settings', countUrl: '/api/search/_count', inventory: true}});
707
706
  registerSidebarItem({folder: 'content', item: {source: 'Built in', text: 'Content API', url: '#/content-api', icon: 'file-text', permission: 'settings', countUrl: '/api/content-api/_count', inventory: true}});
708
707
  // Documentation pages added after the sidebar menu was seeded (0.93.1). The menu is
709
708
  // seeded once, so new pages reach existing sites only by registering here. The two
@@ -727,6 +726,8 @@ await registerCoreSources();
727
726
  (await import('./services/passwordReset.js')).registerPasswordChangeNotice({log: app.log});
728
727
 
729
728
  await registerPlugins(app);
729
+ // After the plugins' onRequest hooks - see serve() in services/adminAssets.js.
730
+ adminAssets.serve();
730
731
 
731
732
  // ---------------------------------------------------------------------------
732
733
  // Public Plugin Routes (root-level, before catch-all)
@@ -76,7 +76,10 @@ export function registerAdminAssets(app, {root, dommaDist, maxBytes = 16 * 1024
76
76
  app.get(url, async (_request, reply) => reply.header('Cache-Control', 'no-store')
77
77
  .type('text/html; charset=utf-8').send(await index()));
78
78
  }
79
- app.addHook('onRequest', async (request, reply) => {
79
+ // Added by serve(), after the plugins: a plugin's onRequest hook that answers a request
80
+ // itself (site-manager proxying another site's host) must run before this one, or this
81
+ // answers from the manager's own files - a different build of the same plugin 404s.
82
+ const serve = () => app.addHook('onRequest', async (request, reply) => {
80
83
  if (!['GET', 'HEAD'].includes(request.method)) return;
81
84
  let url;
82
85
  try { url = new URL(request.raw.url, 'http://localhost'); } catch { return; }
@@ -113,5 +116,5 @@ export function registerAdminAssets(app, {root, dommaDist, maxBytes = 16 * 1024
113
116
  reply.removeHeader('content-length');
114
117
  return JSON.stringify(config);
115
118
  });
116
- return {version};
119
+ return {version, serve};
117
120
  }
@@ -41,7 +41,8 @@ const SEED_ITEMS = [
41
41
  {text: 'Blocks', url: '#/blocks', icon: 'box', permission: 'pages'},
42
42
  {text: 'Components', url: '#/components', icon: 'component', permission: 'components'},
43
43
  {text: 'Views', url: '#/views', icon: 'eye', permission: 'views'},
44
- {text: 'Content API', url: '#/content-api', icon: 'file-text', permission: 'settings'}
44
+ {text: 'Content API', url: '#/content-api', icon: 'file-text', permission: 'settings'},
45
+ {text: 'Search', url: '#/search', icon: 'search', permission: 'settings'}
45
46
  ]
46
47
  },
47
48
  {
@@ -65,11 +66,9 @@ const SEED_ITEMS = [
65
66
  {text: 'Users', url: '#/users', icon: 'users', permission: 'users'},
66
67
  {text: 'Site Settings', url: '#/settings', icon: 'settings', permission: 'settings'},
67
68
  {text: 'Effects', url: '#/effects', icon: 'sparkles', permission: 'settings'},
68
- {text: 'Search', url: '#/search', icon: 'search', permission: 'settings'},
69
69
  {text: 'Layouts', url: '#/layouts', icon: 'layout', permission: 'layouts'},
70
70
  {text: 'Plugins', url: '#/plugins', icon: 'package', permission: 'plugins'},
71
- {text: 'API Tokens', url: '#/api-tokens', icon: 'key', permission: 'api-tokens'},
72
- {text: 'My Profile', url: '#/my-profile', icon: 'user'}
71
+ {text: 'API Tokens', url: '#/api-tokens', icon: 'key', permission: 'api-tokens'}
73
72
  ]
74
73
  },
75
74
  {
@@ -441,6 +440,35 @@ export async function ensureFolderKeys(opts = {}) {
441
440
  changed = true;
442
441
  }
443
442
 
443
+ // Account access lives in the top menu. Move Search out of the old
444
+ // System default once, preserving saved labels, permissions and folders.
445
+ if (!menu.meta?.accountSearchNavigation && items.length) {
446
+ const moving = [];
447
+ const removeProfile = nodes => nodes.filter(n => n?.url !== '#/my-profile').map(n => {
448
+ if (Array.isArray(n?.items)) n.items = removeProfile(n.items);
449
+ return n;
450
+ });
451
+ for (const folder of items.filter(n => isFolder(n) && n.key === 'system')) {
452
+ moving.push(...(folder.items || []).filter(n => n?.url === '#/search'));
453
+ folder.items = removeProfile((folder.items || []).filter(n => n?.url !== '#/search'));
454
+ }
455
+ const hasSearch = nodes => nodes.some(n => n?.url === '#/search'
456
+ || (Array.isArray(n?.items) && hasSearch(n.items)));
457
+ let content = items.find(n => isFolder(n) && n.key === 'content');
458
+ for (const item of moving) {
459
+ if (hasSearch(items)) break;
460
+ if (!content) {
461
+ content = {text: 'Content', key: 'content', icon: 'edit', items: []};
462
+ const index = items.findIndex(n => isFolder(n) && n.key === 'system');
463
+ items.splice(index === -1 ? items.length : index, 0, content);
464
+ }
465
+ if (!Array.isArray(content.items)) content.items = [];
466
+ content.items.push(item);
467
+ }
468
+ menu.meta = {...(menu.meta || {}), accountSearchNavigation: true};
469
+ changed = true;
470
+ }
471
+
444
472
  // An empty menu is left empty: the renderer's minimal fallback (Dashboard,
445
473
  // Menus) is what an admin needs to repair it, and a lone Tools folder would
446
474
  // stop that fallback from applying.