domma-cms 1.10.9 → 1.10.11

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.10.11] - 2026-10-24
4
+
5
+ - Load admin screens and plugin views only when opened, and let the first screen render while navigation data loads. Sidebar badges refresh in the background, reuse recent account-scoped results and share one authenticated request instead of a request per Tool.
6
+ - Cache content-versioned admin scripts and styles for repeat visits. Revalidate module dependencies with content ETags, rebuild entry URLs when files change, and reject superseded asset versions instead of serving different code under an immutable URL.
7
+ - Keep foreground loading indicators independent of low-priority badge polling. Combined counts preserve each route's authorization checks, staff-only visibility and partial failure handling.
8
+
9
+ ## [1.10.10] - 2026-10-24
10
+
11
+ - **Email keeps its text colours in two more cases.** A colour set on the message's `<body>` (a light-on-dark design) was lost when the reader unwrapped it, and an inline style written in capitals (`COLOR: #000000`, common in older templates) was dropped, so a stylesheet colour won instead - grey-on-grey footers and links. Found by rendering a full mailbox (16,000 messages) before and after.
12
+
3
13
  ## [1.10.9] - 2026-10-24
4
14
 
5
15
  - **Built against domma-js 0.56.0** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.56.0). Seven new building blocks are available to pages and plugins: `E.dropzone` (file drop and upload with progress), `E.rangeSlider` (one or two thumbs, also as `type: 'slider'` form fields), `E.commandPalette` (a Cmd/Ctrl+K launcher), `E.focusTrap`, `E.splitPane`, `E.virtualList` (large and infinite lists) and `Domma.i18n` translations. Nothing existing changes.
package/admin/js/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import{wireNotificationMenu as Y}from"./lib/topbar-notifications.js";import{installSmoothCarets as Q}from"./lib/smooth-caret.js";import{views as X}from"./views/index.js";import{withPluginChrome as V}from"./lib/plugin-chrome.js";import{api as r,getUser as L,isAuthenticated as m,logout as Z,sendToLogin as tt}from"./api.js";import{claimDrafts as et}from"./lib/draft-owner.js";import{isAuthScreen as C,loginHashFor as it}from"./lib/login-arrange.js";import{installHttpInterceptor as at}from"./http-interceptor.js";import{installResizableSlideovers as ot}from"./lib/slideover-resizable.js";import{installActivityThrobber as st}from"./lib/activity-throbber.js";import{installUpdateNotice as nt,renderCmsVersion as rt}from"./lib/update-notice.js";import{renderFrameworkVersions as _,wireAccountMenu as lt}from"./lib/topbar-account.js";import{refreshTopbarProject as B}from"./lib/topbar-project.js";import{isPathAllowed as ct,redirectFor as mt}from"./lib/admin-scope.js";import{deliverDesktop as pt,desktopActive as dt,notificationSettings as ut}from"./lib/desktop-notify.js";import{avatarUrl as ht,installAccountAvatar as wt}from"./lib/avatar.js";import{installCalculator as gt}from"./lib/calculator.js";import{takeOverviewPass as ft,withDashboardStrip as W}from"./lib/dashboards-strip.js";import{installHotkeys as vt}from"./lib/hotkeys-ui.js";import{applyAdminBrand as H}from"./lib/admin-brand.js";import{installRouterCatchUp as bt}from"./lib/router-catch-up.js";import{installSidebarDrawer as St}from"./lib/sidebar-drawer.js";$(()=>{Q(document),at(),gt(),nt(),st(),ot(),St(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");m()?await i(r,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,h=null,c="/",w=!1,g=null;function D(t){if(N[t])return E.toast(`${N[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const i=y[t]||mt(p,t);return i||((t.split("?")[0]||"/")==="/"&&c&&c!=="/"&&!ft()?y[c]||c:null)}const y={},N={};R.use(async(t,i,e)=>{if(C(t.path))return e();if(!m()){tt();return}const a=D(t.path);if(a){R.navigate(a);return}e()});async function f(){try{const t=await r.get("/auth/permissions");p=t.adminScope||null,h=t.adminBrand||null,H(h),c=t.adminHome||"/",_(t.versions);const i=t.permissions||[];return w=ct(p,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),g=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return p=null,c="/",h||H(null),w=!1,g=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{f()});let P=null,A=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(B(r),P){A=!0;return}P=(async()=>{do{A=!1;try{await j(await f())}catch{}}while(A)})().finally(()=>{P=null})});async function j(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:p,brand:h})}M.subscribe("router:afterChange",({to:t})=>{C(t.path)&&(u&&(clearInterval(u),u=null),p=null,c="/",w=!1,g=null)});const q=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/editor",view:"tutorialEditor",title:"The Editor - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Tool - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/usage/search-engines",view:"usageSearchEngines",title:"Search Engines & AI - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"pageEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"pageEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"pageEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"pageEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/wordpress-import",view:"wordpressImport",title:"Import from WordPress - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:i})=>{if(!C(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),C(i?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(o=>o.refreshAdminTheme(r)),await T();const e=await f(),a=D(t.path);if(a&&R.navigate(a),j(e),ut.read().signInToasts)try{const o=await r.system.notifications.list().catch(()=>[]),n=(Array.isArray(o)?o:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const v=d(s.data?.title||""),x=d((s.data?.body||"").slice(0,120));E.toast(`${v} - ${x}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}O()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const G=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function z(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const i=t?String(t.path).split("?")[0]:"";if(!t||!z(i))return;const a=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!a||!/\/new$/.test(i))return;let o=0;const n=60,s=1500,v=()=>{o+=n;for(const x of G){const b=document.querySelector(x);if(!b)continue;if(Array.from(b.options||[]).find(J=>J.value===a)){b.value=a,b.dispatchEvent(new Event("change",{bubbles:!0}));return}}o<s&&setTimeout(v,n)};setTimeout(v,n)});const F="cms_card_states",k=S.get(F)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&k[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const i=$(this).closest(".card");i.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(k[e]=i.hasClass("card-collapsed")?"collapsed":"open",S.set(F,k))}),vt(),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const l={...X},K={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},wordpressImport:{displayName:"Import from WordPress",icon:"download"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Site menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,i]of Object.entries(K))l[t]&&(l[t]=V(l[t],{plugin:t,licence:"Built in",...i}));l.dashboard=W(l.dashboard);const I=[...q];async function T(){if(m())try{const t=await r.plugins.adminConfig(),i=new Set(I.map(e=>e.path));for(const e of t.routes||[])i.has(e.path)||I.push(e);for(const[e,a]of Object.entries(t.views||{}))try{const o=await import(`/plugins/${a.entry}`);l[e]=V(o[a.exportName],(t.meta||{})[e])}catch{}l.dashboard=W(l.dashboard);for(const[e,{route:a}]of Object.entries(t.takeovers||{}))a&&(y[`/${e}`]=a);for(const[e,{enabled:a,displayName:o}]of Object.entries(t.tools||{}))a===!1&&(N[`/${e}`]=o);for(const{id:e,href:a}of t.css||[])if(!document.getElementById(e)){const o=document.createElement("link");o.id=e,o.rel="stylesheet",o.href=a,document.head.appendChild(o)}}catch{}}function O(){const t=L();if(!t)return;if($("#topbar-user-name").length){B(r);return}const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,a=ht(t);$("#topbar-user").get(0).innerHTML=`
1
+ import{wireNotificationMenu as Q}from"./lib/topbar-notifications.js";import{installSmoothCarets as X}from"./lib/smooth-caret.js";import{viewLoaders as Z}from"./views/index.js";import{createLazyViews as tt}from"./lib/lazy-views.js";import{withPluginChrome as _}from"./lib/plugin-chrome.js";import{api as r,getUser as L,isAuthenticated as m,logout as et,sendToLogin as it}from"./api.js";import{claimDrafts as at}from"./lib/draft-owner.js";import{isAuthScreen as C,loginHashFor as ot}from"./lib/login-arrange.js";import{installHttpInterceptor as st}from"./http-interceptor.js";import{installResizableSlideovers as nt}from"./lib/slideover-resizable.js";import{installActivityThrobber as rt}from"./lib/activity-throbber.js";import{installUpdateNotice as lt,renderCmsVersion as ct}from"./lib/update-notice.js";import{renderFrameworkVersions as W,wireAccountMenu as mt}from"./lib/topbar-account.js";import{refreshTopbarProject as B}from"./lib/topbar-project.js";import{isPathAllowed as pt,redirectFor as dt}from"./lib/admin-scope.js";import{deliverDesktop as ut,desktopActive as ht,notificationSettings as wt}from"./lib/desktop-notify.js";import{avatarUrl as gt,installAccountAvatar as ft}from"./lib/avatar.js";import{installCalculator as vt}from"./lib/calculator.js";import{takeOverviewPass as bt,withDashboardStrip as H}from"./lib/dashboards-strip.js";import{installHotkeys as St}from"./lib/hotkeys-ui.js";import{applyAdminBrand as q}from"./lib/admin-brand.js";import{installRouterCatchUp as Ct}from"./lib/router-catch-up.js";import{installSidebarDrawer as Mt}from"./lib/sidebar-drawer.js";$(()=>{X(document),st(),vt(),lt(),rt(),nt(),Mt(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:a}=await import("./lib/admin-theme.js");m()?await a(r,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,h=null,c="/",w=!1,g=null;function D(t){if(P[t])return E.toast(`${P[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const a=y[t]||dt(p,t);return a||((t.split("?")[0]||"/")==="/"&&c&&c!=="/"&&!bt()?y[c]||c:null)}const y={},P={};R.use(async(t,a,e)=>{if(C(t.path))return e();if(!m()){it();return}const i=D(t.path);if(i){R.navigate(i);return}e()});async function f(){try{const t=await r.get("/auth/permissions");p=t.adminScope||null,h=t.adminBrand||null,q(h),c=t.adminHome||"/",W(t.versions);const a=t.permissions||[];return w=pt(p,"/settings")&&a.some(e=>e==="settings"||e.startsWith("settings.")),g=a.some(e=>e==="notifications"||e.startsWith("notifications.")),a}catch{return p=null,c="/",h||q(null),w=!1,g=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{f()});let N=null,A=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(B(r),N){A=!0;return}N=(async()=>{do{A=!1;try{await j(await f())}catch{}}while(A)})().finally(()=>{N=null})});async function j(t){const{renderAdminSidebar:a}=await import("./lib/sidebar-renderer.js");await a({mount:"#admin-sidebar",permissions:t,adminScope:p,brand:h})}M.subscribe("router:afterChange",({to:t})=>{C(t.path)&&(u&&(clearInterval(u),u=null),p=null,c="/",w=!1,g=null)});const z=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/editor",view:"tutorialEditor",title:"The Editor - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Tool - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/usage/search-engines",view:"usageSearchEngines",title:"Search Engines & AI - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"pageEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"pageEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"pageEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"pageEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/wordpress-import",view:"wordpressImport",title:"Import from WordPress - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:a})=>{if(!C(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),C(a?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(o=>o.refreshAdminTheme(r)),await T();const e=await f(),i=D(t.path);if(i&&R.navigate(i),j(e),wt.read().signInToasts)try{const o=await r.system.notifications.list().catch(()=>[]),n=(Array.isArray(o)?o:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const l=d(s.data?.title||""),x=d((s.data?.body||"").slice(0,120));E.toast(`${l} - ${x}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}V()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const G=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function J(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const a=t?String(t.path).split("?")[0]:"";if(!t||!J(a))return;const i=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!i||!/\/new$/.test(a))return;let o=0;const n=60,s=1500,l=()=>{o+=n;for(const x of G){const b=document.querySelector(x);if(!b)continue;if(Array.from(b.options||[]).find(Y=>Y.value===i)){b.value=i,b.dispatchEvent(new Event("change",{bubbles:!0}));return}}o<s&&setTimeout(l,n)};setTimeout(l,n)});const F="cms_card_states",k=S.get(F)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&k[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const a=$(this).closest(".card");a.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(k[e]=a.hasClass("card-collapsed")?"collapsed":"open",S.set(F,k))}),St(),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const a=document.querySelector("#view-container .view-header button.btn-primary");a&&(t.preventDefault(),a.click())});const v=tt(),O=v.views,K={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},wordpressImport:{displayName:"Import from WordPress",icon:"download"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Site menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,a]of Object.entries(Z))v.register(t,async()=>{let e=await a();const i=K[t];return i&&(e=_(e,{plugin:t,licence:"Built in",...i})),t==="dashboard"?H(e):e});R.use(async(t,a,e)=>{try{await v.load(t.view)}catch(i){console.error("[admin] Screen failed to load",i),O[t.view]={template:'<p role="alert">This screen could not load. Reload the page to try again.</p>'}}e()});const I=[...z];async function T(){if(m())try{const t=await r.plugins.adminConfig(),a=new Set(I.map(e=>e.path));for(const e of t.routes||[])a.has(e.path)||I.push(e);for(const[e,i]of Object.entries(t.views||{})){const o=(t.meta||{})[e];v.register(e,async()=>{const s=(await import(`/plugins/${i.entry}`))[i.exportName];if(!s)throw new Error(`Missing plugin screen: ${e}`);const l=_(s,o);return e==="dashboard"?H(l):l},JSON.stringify([i,o]))}for(const[e,{route:i}]of Object.entries(t.takeovers||{}))i&&(y[`/${e}`]=i);for(const[e,{enabled:i,displayName:o}]of Object.entries(t.tools||{}))i===!1&&(P[`/${e}`]=o);for(const{id:e,href:i}of t.css||[])if(!document.getElementById(e)){const o=document.createElement("link");o.id=e,o.rel="stylesheet",o.href=i,document.head.appendChild(o)}}catch{}}function V(){const t=L();if(!t)return;if($("#topbar-user-name").length){B(r);return}const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,i=gt(t);$("#topbar-user").get(0).innerHTML=`
2
2
  <a id="topbar-project" class="topbar-action-link topbar-project" hidden>
3
3
  <span data-icon="folder"></span>
4
4
  <span id="topbar-project-name" class="topbar-project-name"></span>
@@ -32,7 +32,7 @@ import{wireNotificationMenu as Y}from"./lib/topbar-notifications.js";import{inst
32
32
  </button>
33
33
  <div id="topbar-account-menu" class="topbar-account-menu" hidden>
34
34
  <div class="topbar-account-identity">
35
- ${a?`<img id="topbar-avatar" class="topbar-avatar" src="${d(a)}" alt="">`:""}
35
+ ${i?`<img id="topbar-avatar" class="topbar-avatar" src="${d(i)}" alt="">`:""}
36
36
  <span id="topbar-user-name" class="topbar-user-name">${d(t.name)}</span>
37
37
  <span class="topbar-role-badge topbar-role-badge--${d(t.role)}">${d(e)}</span>
38
38
  </div>
@@ -50,4 +50,4 @@ import{wireNotificationMenu as Y}from"./lib/topbar-notifications.js";import{inst
50
50
  </a>
51
51
  </div>
52
52
  </div>
53
- `;const o=$("#topbar-bell").get(0);o&&(o.hidden=!0);const n=$("#topbar-settings").get(0);n&&(n.hidden=!w),$("#topbar-logout-btn").on("click",s=>{s.preventDefault(),Z()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),lt(t),Y({refreshBadge:U}),wt(t),_(),rt(),B(r),U(),u||(u=setInterval(U,6e4))}function d(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let u=null;async function U(){if(!(!m()||g===!1))try{const{count:t}=await r.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const i=$("#topbar-bell-badge");t>0?i.text(t>99?"99+":String(t)).removeAttr("hidden"):i.attr("hidden","").text(""),t>0&&dt()&&pt(await r.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!m()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash.startsWith("#/login")||(window.location.hash=it(window.location.hash));else if(et(L()?.id),await T(),L()){const o=await f(),n=(window.location.hash||"#/").slice(1)||"/",s=D(n);s&&(window.location.hash=`#${s}`),await j(o),O()}const t=R._extractParams.bind(R);R._extractParams=function(a,o){const n=o.indexOf("?");if(n!==-1&&(o=o.slice(0,n)),a.endsWith("/*")){const s=a.slice(0,-2);return o.startsWith(s+"/")?{}:null}return t(a,o)};const i=R._renderNotFound.bind(R);let e="";R._renderNotFound=async function(a){return m()&&e!==a&&(e=a,await T(),R._matchRoute(a).route)?(e="",R._handleRouteChange()):(e="",m()&&$("#admin-sidebar").get(0)?.style.display==="none"?(E.toast("That link goes nowhere - here is your home page.",{type:"warning"}),R.navigate("/")):i(a))},bt(R),R.init({container:"#view-container",routes:I,views:l,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}})})()});
53
+ `;const o=$("#topbar-bell").get(0);o&&(o.hidden=!0);const n=$("#topbar-settings").get(0);n&&(n.hidden=!w),$("#topbar-logout-btn").on("click",s=>{s.preventDefault(),et()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),mt(t),Q({refreshBadge:U}),ft(t),W(),ct(),B(r),U(),u||(u=setInterval(U,6e4))}function d(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let u=null;async function U(){if(!(!m()||g===!1))try{const{count:t}=await r.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const a=$("#topbar-bell-badge");t>0?a.text(t>99?"99+":String(t)).removeAttr("hidden"):a.attr("hidden","").text(""),t>0&&ht()&&ut(await r.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!m()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash.startsWith("#/login")||(window.location.hash=ot(window.location.hash));else{at(L()?.id);const i=L(),[o]=await Promise.all([i?f():Promise.resolve([]),T()]);if(i){const n=(window.location.hash||"#/").slice(1)||"/",s=D(n);s&&(window.location.hash=`#${s}`),j(o).catch(l=>console.error("[admin] Sidebar failed to load",l)),V()}}const t=R._extractParams.bind(R);R._extractParams=function(i,o){const n=o.indexOf("?");if(n!==-1&&(o=o.slice(0,n)),i.endsWith("/*")){const s=i.slice(0,-2);return o.startsWith(s+"/")?{}:null}return t(i,o)};const a=R._renderNotFound.bind(R);let e="";R._renderNotFound=async function(i){return m()&&e!==i&&(e=i,await T(),R._matchRoute(i).route)?(e="",R._handleRouteChange()):(e="",m()&&$("#admin-sidebar").get(0)?.style.display==="none"?(E.toast("That link goes nowhere - here is your home page.",{type:"warning"}),R.navigate("/")):a(i))},Ct(R),R.init({container:"#view-container",routes:I,views:O,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}})})()});
@@ -1 +1 @@
1
- export const SHOW_AFTER_MS=200,MIN_VISIBLE_MS=350;export function isTracked(e,n){let r="";if(typeof e=="string"?r=e:e instanceof URL?r=e.href:e&&typeof e.url=="string"&&(r=e.url),!r)return!1;try{return new URL(r,n).origin===n}catch{return!1}}let i=0,o=null,a=null,u=0,t=null;function m(){return t||(t=document.createElement("span"),t.className="dm-activity",t.setAttribute("role","status"),t.setAttribute("aria-label","Loading"),t.hidden=!0,typeof E<"u"&&E.loader&&E.loader(t,{type:"spinner",size:16,color:"primary",centered:"false"}),t)}function h(e){const n=document.querySelector("#view-container .dm-plugin-banner-identity")||document.getElementById("admin-topbar");if(!n)return!1;if(e.parentNode!==n)if(n.id==="admin-topbar"){const r=n.querySelector(".topbar-brand");n.insertBefore(e,r?r.nextSibling:n.firstChild)}else n.appendChild(e);return!0}function w(){if(o=null,!i)return;const e=m();h(e)&&(e.hidden=!1,u=Date.now())}function y(){a=null,!(i||!t)&&(t.hidden=!0)}function p(){i++,clearTimeout(a),a=null,t&&!t.hidden?h(t):o||(o=setTimeout(w,200))}function b(){if(i=Math.max(0,i-1),i||(clearTimeout(o),o=null,!t||t.hidden))return;const e=350-(Date.now()-u);a=setTimeout(y,Math.max(0,e))}export function installActivityThrobber(){if(typeof window>"u"||window.fetch.__dmActivity)return;const e=window.fetch,n=window.location.origin,r=function(c,f){if(!isTracked(c,n))return e.call(this,c,f);p();let s=!1;const d=()=>{s||(s=!0,b())};try{const l=e.call(this,c,f);return l.then(d,d),l}catch(l){throw d(),l}};r.__dmActivity=!0,window.fetch=r}
1
+ export const SHOW_AFTER_MS=200,MIN_VISIBLE_MS=350;export function isTracked(e,n,{priority:i}={}){if(i==="low")return!1;let r="";if(typeof e=="string"?r=e:e instanceof URL?r=e.href:e&&typeof e.url=="string"&&(r=e.url),!r)return!1;try{return new URL(r,n).origin===n}catch{return!1}}let o=0,l=null,c=null,u=0,t=null;function m(){return t||(t=document.createElement("span"),t.className="dm-activity",t.setAttribute("role","status"),t.setAttribute("aria-label","Loading"),t.hidden=!0,typeof E<"u"&&E.loader&&E.loader(t,{type:"spinner",size:16,color:"primary",centered:"false"}),t)}function h(e){const n=document.querySelector("#view-container .dm-plugin-banner-identity")||document.getElementById("admin-topbar");if(!n)return!1;if(e.parentNode!==n)if(n.id==="admin-topbar"){const i=n.querySelector(".topbar-brand");n.insertBefore(e,i?i.nextSibling:n.firstChild)}else n.appendChild(e);return!0}function w(){if(l=null,!o)return;const e=m();h(e)&&(e.hidden=!1,u=Date.now())}function y(){c=null,!(o||!t)&&(t.hidden=!0)}function p(){o++,clearTimeout(c),c=null,t&&!t.hidden?h(t):l||(l=setTimeout(w,200))}function b(){if(o=Math.max(0,o-1),o||(clearTimeout(l),l=null,!t||t.hidden))return;const e=350-(Date.now()-u);c=setTimeout(y,Math.max(0,e))}export function installActivityThrobber(){if(typeof window>"u"||window.fetch.__dmActivity)return;const e=window.fetch,n=window.location.origin,i=function(r,f){if(!isTracked(r,n,f))return e.call(this,r,f);p();let s=!1;const d=()=>{s||(s=!0,b())};try{const a=e.call(this,r,f);return a.then(d,d),a}catch(a){throw d(),a}};i.__dmActivity=!0,window.fetch=i}
@@ -0,0 +1 @@
1
+ export function createRequestQueue(i=4){const e=[],t=new Map;let r=0;function u(){for(;r<i&&e.length;){const s=e.shift();r++,Promise.resolve().then(s.task).then(s.resolve,s.reject).finally(()=>{r--,u()})}}return(s,{key:n,priority:a=!1}={})=>{if(n&&t.has(n))return t.get(n);const o=new Promise((f,h)=>{const c={task:s,resolve:f,reject:h};a?e.unshift(c):e.push(c),u()});if(!n)return o;const l=o.finally(()=>t.delete(n));return t.set(n,l),l}}export function afterInitialView(i=globalThis.R,e=globalThis.M){return!i||i._currentRoute||!e?.subscribe?Promise.resolve():new Promise(t=>{const r=e.subscribe("router:afterChange",()=>{r(),t()})})}
@@ -0,0 +1 @@
1
+ export function createLazyViews(r={}){const t=new Map;return{views:r,register(e,n,i=n){t.get(e)?.key!==i&&(t.set(e,{loader:n,key:i,pending:null,loaded:!1}),delete r[e])},async load(e){const n=t.get(e);if(!n)throw new Error(`Unknown admin screen: ${e}`);if(n.loaded)return r[e];n.pending||(n.pending=Promise.resolve().then(n.loader).then(o=>{if(!o||!["object","function"].includes(typeof o))throw new Error(`Invalid admin screen: ${e}`);return t.get(e)===n&&(r[e]=o,n.loaded=!0),o}).finally(()=>{n.pending=null}));const i=await n.pending;return t.get(e)===n?i:this.load(e)}}}
@@ -1 +1 @@
1
- import{renderTopbarNavigation as y,updateTopbarCounts as h}from"./topbar-navigation.js";export const COUNT_CACHE_MAX_AGE=5*6e4;const f=new Map,u=new Map;let a;export function countCacheKey(o,r,t,c=[]){const n=[],s=e=>{if(e){e.url&&e.countUrl&&n.push([e.url,e.countUrl,!!e.inventory]);for(const i of[...e.items||[],...e.children||[]])s(i)}};return c.forEach(e=>s(e.item||e)),"admin.counts.v1."+JSON.stringify([o?.id||"",[...r].sort(),t,n.sort()])}export function readCountCache(o,r=Date.now()){let t=f.get(o);if(!t)try{t=JSON.parse(sessionStorage.getItem(o))}catch{}return!t||!Number.isFinite(t.at)||r<t.at||r-t.at>COUNT_CACHE_MAX_AGE||!t.counts||typeof t.counts!="object"||Array.isArray(t.counts)?null:t.counts}export function writeCountCache(o,r,t=Date.now()){const c={};for(const[s,e]of Object.entries(r))!e||!Number.isFinite(e.count)||e.count<0||(c[s]={count:e.count,label:String(e.label||""),tone:e.tone||"info",showZero:e.showZero===!0,inventory:e.inventory===!0});const n={at:t,counts:c};f.set(o,n);try{sessionStorage.setItem(o,JSON.stringify(n))}catch{}}export function loadCountSnapshot(o,r){return u.has(o)||u.set(o,Promise.resolve().then(r).then(t=>(writeCountCache(o,t),t)).finally(()=>u.delete(o))),u.get(o)}export function countSnapshot(o,r){const t=new Map,c=n=>{if(n){n.url&&n.countUrl&&t.set(n.url,!!n.inventory);for(const s of[...n.items||[],...n.children||[]])c(s)}};return r.forEach(n=>c(n.item||n)),Object.fromEntries(Object.entries(o).map(([n,s])=>[n,s?{...s,aggregate:t.has(n)&&!t.get(n)&&!s.inventory}:null]))}export async function beginSidebarCounts({user:o,permissions:r,adminScope:t,registered:c,tree:n,actions:s,fetcher:e}){const i={key:countCacheKey(o,r,t,c),registered:c};a=i;const l=readCountCache(i.key)||{};y(n,{...s,counts:countSnapshot(l,c)});try{const p=await loadCountSnapshot(i.key,e);return a===i&&h(countSnapshot(p,c),{replace:!0}),p}catch{return l}}export function publishSidebarCount(o,r){if(!a)return;h(countSnapshot({[o]:r},a.registered));const t=readCountCache(a.key)||{};r?t[o]=r:delete t[o];const c=f.get(a.key)?.at||Date.now();writeCountCache(a.key,t,c)}
1
+ import{renderTopbarNavigation as d,updateTopbarCounts as p}from"./topbar-navigation.js";import{afterInitialView as g}from"./background-requests.js";export const COUNT_CACHE_MAX_AGE=5*6e4,COUNT_REFRESH_INTERVAL=6e4;const f=new Map,l=new Map;let a;export function countCacheKey(n,r,t,c=[]){const e=[],s=o=>{if(o){o.url&&o.countUrl&&e.push([o.url,o.countUrl,!!o.inventory]);for(const i of[...o.items||[],...o.children||[]])s(i)}};return c.forEach(o=>s(o.item||o)),"admin.counts.v1."+JSON.stringify([n?.id||"",[...r].sort(),t,e.sort()])}export function readCountCache(n,r=Date.now()){let t=f.get(n);if(!t)try{t=JSON.parse(sessionStorage.getItem(n))}catch{}return!t||!Number.isFinite(t.at)||r<t.at||r-t.at>COUNT_CACHE_MAX_AGE||!t.counts||typeof t.counts!="object"||Array.isArray(t.counts)?null:(f.set(n,t),t.counts)}export function hasRecentCountCache(n,r=Date.now()){const t=readCountCache(n,r),c=f.get(n);return!!t&&r-c.at<COUNT_REFRESH_INTERVAL}export function writeCountCache(n,r,t=Date.now()){const c={};for(const[s,o]of Object.entries(r))!o||!Number.isFinite(o.count)||o.count<0||(c[s]={count:o.count,label:String(o.label||""),tone:o.tone||"info",showZero:o.showZero===!0,inventory:o.inventory===!0});const e={at:t,counts:c};f.set(n,e);try{sessionStorage.setItem(n,JSON.stringify(e))}catch{}}export function loadCountSnapshot(n,r){return l.has(n)||l.set(n,Promise.resolve().then(r).then(t=>(writeCountCache(n,t),t)).finally(()=>l.delete(n))),l.get(n)}export function countSnapshot(n,r){const t=new Map,c=e=>{if(e){e.url&&e.countUrl&&t.set(e.url,!!e.inventory);for(const s of[...e.items||[],...e.children||[]])c(s)}};return r.forEach(e=>c(e.item||e)),Object.fromEntries(Object.entries(n).map(([e,s])=>[e,s?{...s,aggregate:t.has(e)&&!t.get(e)&&!s.inventory}:null]))}export function beginSidebarCounts({user:n,permissions:r,adminScope:t,registered:c,tree:e,actions:s,fetcher:o}){const i={key:countCacheKey(n,r,t,c),registered:c};a=i;const u=readCountCache(i.key)||{};d(e,{...s,counts:countSnapshot(u,c)});const y=(hasRecentCountCache(i.key)?Promise.resolve(u):g().then(()=>a!==i?u:loadCountSnapshot(i.key,o))).then(h=>(a===i&&p(countSnapshot(h,c),{replace:!0}),h)).catch(()=>u);return{counts:u,refresh:y,isActive:()=>a===i}}export function publishSidebarCount(n,r){if(!a)return;p(countSnapshot({[n]:r},a.registered));const t=readCountCache(a.key)||{};r?t[n]=r:delete t[n];const c=f.get(a.key)?.at||Date.now();writeCountCache(a.key,t,c)}
@@ -0,0 +1 @@
1
+ import{apiRequest as u}from"../api.js";export async function requestSidebarCounts(r,{request:o=u,legacy:n,today:e}={}){if(!r.length)return{};if(!e){const t=new Date;e=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`}try{const t=await o("/sidebar/counts",{method:"POST",priority:"low",body:JSON.stringify({urls:[...new Set(r.map(s=>s.url))],today:e})});if(!t?.counts||typeof t.counts!="object"||Array.isArray(t.counts))throw new Error("Invalid sidebar count response");return t.counts}catch(t){if([404,405].includes(t.status)&&n)return n(r);throw t}}
@@ -1,7 +1,7 @@
1
- import{api as h,getUser as F}from"../api.js";import{beginSidebarCounts as ye,publishSidebarCount as ve}from"./sidebar-count-cache.js";import{isPathAllowed as xe}from"./admin-scope.js";import{colourToCss as J,applyFloat as $e,applyHighlight as we,itemHighlightAttrs as Se}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{placeTools as ke,ensureLockedTools as Ce,managePluginsItem as qe,stripItemByUrl as Ae,insertFoldersBeforeSystem as Fe,pruneEmptyToolsFolder as Le,sidebarStateKey as Q,canAny as z,filterByPermission as Ee,MANAGE_PLUGINS_URL as ee}from"./sidebar-grouping.js";import{arrangeButtonMarkup as Be,canArrange as Me,startArrange as Te}from"./sidebar-arrange.js";import{collapseButtonMarkup as He,wireSidebarSize as je,COLLAPSED_WIDTH as Ne}from"./sidebar-size.js";import{FREQUENT_MAX as te,addFrequent as Oe,frequentFolder as Pe,moveFrequent as ne,readFrequent as G,readFrequentHidden as Ie,removeFrequent as Re,saveFrequent as _e,syncFrequent as Ue,writeFrequent as ze,writeFrequentHidden as se}from"./sidebar-frequent.js";import{hotkeyFor as oe,hotkeyTitle as We,setHotkeyTree as De}from"./hotkeys-ui.js";import{hotkeyLabel as Ge}from"./hotkeys.js";function f(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const Ke=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Site menus",url:"#/menus",icon:"menu",permission:"menus"}];async function Ve(e){if(!z(e,"projects"))return[];let t=[];try{t=await h.projects.listWithCounts()}catch{return[]}if(!Array.isArray(t))return[];const s=[{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:"Site 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 t.map(o=>{const d="#/projects/"+encodeURIComponent(o.slug),i=o.counts||{},l=[{text:"Overview",url:d,icon:o.icon||"folder"}];for(const a of s){const r=Number(i[a.key])||0;r<=0||l.push({text:`${a.text} (${r})`,url:d+a.path,icon:a.icon})}l.push({text:"Settings",url:d+"/settings",icon:"settings"});const u=o.enabled===!1;return{text:o.name||o.slug,key:`project.${o.slug}`,icon:o.icon||"folder",items:l,...u&&{colour:"neutral",badge:{text:"off"}}}})}function Xe(e,t){for(const s of e)if(!s.hidden&&(s.key==="projects"||!s.key&&(s.text||"").toLowerCase()==="projects")){s.items=Array.isArray(s.items)?s.items.slice():[],s.items.push(...t);return}}function ie(e,t){const s=re.includes(t.tone)?t.tone:"info",o=t.label||String(t.count);t.details?I.set(e,t.details):I.delete(e);const n=!t.inventory&&L.some(d=>d.url===e&&!d.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${s}" data-count-for="${f(e)}"${n} data-badge-label="${f(o)}" aria-label="${f(o)}">${f(String(t.count))}</span>`}function ae(e){for(const t of e.querySelectorAll("details")){const s=t.querySelector(":scope > summary");if(!s)continue;let o=0,n=!1;const d=[];for(const r of t.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const m=Number(r.textContent)||0;m&&(o+=m,n||(n=r.classList.contains("sidebar-badge--danger")),d.push({text:r.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:r.dataset.badgeLabel,tone:r.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const i=`folder:${t.dataset.stateKey||s.textContent.trim()}`;let l=s.querySelector(":scope > .sidebar-badge--sum");if(!o){l?.remove(),I.delete(i);continue}l||(l=document.createElement("span"),s.querySelector(".sidebar-text")?.after(l));const u=s.querySelector(".sidebar-text")?.textContent.trim()||"This folder",a=d.map(r=>`${r.text}: ${r.meta}`).join(" \xB7 ");l.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${n?"danger":"info"}`,l.dataset.countFor=i,l.dataset.badgeLabel=a,l.setAttribute("aria-label",a),l.textContent=String(o),I.set(i,{title:u,items:d})}}const I=new Map;function Ze(e,t){if(!e.url||!t)return"";const s=t[e.url];return!s||!(s.count>0)&&!s.showZero?"":ie(e.url,s)}const re=["info","attention","danger","live"],Ye={"#/pages":["page","pages"],"#/media":["file in the media library","files in the media library"],"#/menus":["menu","menus"],"#/context-menus":["context menu","context menus"],"#/theme":["element override in the theme","element overrides in the theme"],"#/collections":["collection","collections"],"#/forms":["form","forms"],"#/views":["view","views"],"#/actions":["action","actions"],"#/blocks":["block","blocks"],"#/components":["component","components"],"#/users":["user","users"],"#/plugins":["plugin installed","plugins installed"],"#/system/notifications":["unread notification","unread notifications"]};function K(e,{fallbackLabel:t="",tone:s="info"}={}){const o=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(o)||o<0)return null;const n=typeof e?.label=="string"&&e.label||(t?`${o} - ${t}`:String(o)),d={count:Math.round(o),label:n,tone:re.includes(e?.tone)?e.tone:s};return e?.details&&typeof e.details=="object"&&(d.details=e.details),e?.inventory===!0&&(d.inventory=!0),e?.showZero===!0&&(d.showZero=!0),d}function le(e,t,s=null,{inFrequent:o=!1}={}){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="theme-switcher")return"";if(e&&e.type==="spacer"){const b=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${f(b)}" aria-hidden="true"></div>`}const n=String(e.text||"?").trim().charAt(0).toUpperCase()||"?",d=e.icon?`<span data-icon="${f(e.icon)}"></span>`:`<span class="sidebar-initial" aria-hidden="true">${f(n)}</span>`,i=f(e.text||""),l=e.url?f(e.url):"",u=Array.isArray(e.items)&&e.items.length>0,a=e.colour?J(e.colour):"",r=Se(e),m=[r.hover&&` data-menu-item-hover="${f(r.hover)}"`,r.active&&` data-menu-item-active="${f(r.active)}"`].filter(Boolean).join(""),y=[a&&`color:${f(a)}`,r.accent&&`--dm-menu-highlight:${f(r.accent)}`].filter(Boolean).join(";"),v=y?` style="${y}"`:"",w=e.url?We(e.url,e.text||"",{inFrequent:o}):"",k=w?` title="${f(w)}"`:"";let q="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const b=e.badge.variant?J(e.badge.variant):"";q=`<span class="dm-menu-badge"${b?` style="background:${f(b)};color:var(--dm-text-inverse,#fff)"`:""}>${f(String(e.badge.text))}</span>`}if(e.frequent&&e.empty){const b=Q(e,s),A=S.get(b)!==!1;return`<details data-state-key="${f(b)}"${A?" open":""} data-frequent data-frequent-empty>
2
- <summary${v}>${d} <span class="sidebar-text">${i}</span></summary>
1
+ import{api as g,getUser as k,isAuthenticated as Se}from"../api.js";import{createRequestQueue as ke}from"./background-requests.js";import{requestSidebarCounts as qe}from"./sidebar-counts.js";import{beginSidebarCounts as Ce,publishSidebarCount as Ae}from"./sidebar-count-cache.js";import{isPathAllowed as Fe}from"./admin-scope.js";import{colourToCss as te,applyFloat as Le,applyHighlight as Ee,itemHighlightAttrs as je}from"/public/js/menu-decor.mjs?v=20261014-menu-entrance";import{placeTools as Be,ensureLockedTools as Oe,managePluginsItem as Me,stripItemByUrl as Te,insertFoldersBeforeSystem as He,pruneEmptyToolsFolder as Ne,sidebarStateKey as ne,canAny as W,filterByPermission as Re,MANAGE_PLUGINS_URL as se}from"./sidebar-grouping.js";import{arrangeButtonMarkup as Ie,canArrange as Pe,startArrange as _e}from"./sidebar-arrange.js";import{collapseButtonMarkup as Ue,wireSidebarSize as ze,COLLAPSED_WIDTH as We}from"./sidebar-size.js";import{FREQUENT_MAX as oe,addFrequent as De,frequentFolder as Ge,moveFrequent as ie,readFrequent as K,readFrequentHidden as Ke,removeFrequent as Ve,saveFrequent as Xe,syncFrequent as Ze,writeFrequent as Je,writeFrequentHidden as ae}from"./sidebar-frequent.js";import{hotkeyFor as re,hotkeyTitle as Ye,setHotkeyTree as Qe}from"./hotkeys-ui.js";import{hotkeyLabel as et}from"./hotkeys.js";function m(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const tt=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Site menus",url:"#/menus",icon:"menu",permission:"menus"}];async function nt(e){if(!W(e,"projects"))return[];let n=[];try{n=await g.projects.listWithCounts()}catch{return[]}if(!Array.isArray(n))return[];const t=[{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:"Site 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 n.map(o=>{const l="#/projects/"+encodeURIComponent(o.slug),u=o.counts||{},i=[{text:"Overview",url:l,icon:o.icon||"folder"}];for(const d of t){const r=Number(u[d.key])||0;r<=0||i.push({text:`${d.text} (${r})`,url:l+d.path,icon:d.icon})}i.push({text:"Settings",url:l+"/settings",icon:"settings"});const a=o.enabled===!1;return{text:o.name||o.slug,key:`project.${o.slug}`,icon:o.icon||"folder",items:i,...a&&{colour:"neutral",badge:{text:"off"}}}})}function st(e,n){for(const t of e)if(!t.hidden&&(t.key==="projects"||!t.key&&(t.text||"").toLowerCase()==="projects")){t.items=Array.isArray(t.items)?t.items.slice():[],t.items.push(...n);return}}function le(e,n){const t=de.includes(n.tone)?n.tone:"info",o=n.label||String(n.count);n.details?R.set(e,n.details):R.delete(e);const s=!n.inventory&&L.some(l=>l.url===e&&!l.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${t}" data-count-for="${m(e)}"${s} data-badge-label="${m(o)}" aria-label="${m(o)}">${m(String(n.count))}</span>`}function ce(e){for(const n of e.querySelectorAll("details")){const t=n.querySelector(":scope > summary");if(!t)continue;let o=0,s=!1;const l=[];for(const r of n.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const f=Number(r.textContent)||0;f&&(o+=f,s||(s=r.classList.contains("sidebar-badge--danger")),l.push({text:r.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:r.dataset.badgeLabel,tone:r.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const u=`folder:${n.dataset.stateKey||t.textContent.trim()}`;let i=t.querySelector(":scope > .sidebar-badge--sum");if(!o){i?.remove(),R.delete(u);continue}i||(i=document.createElement("span"),t.querySelector(".sidebar-text")?.after(i));const a=t.querySelector(".sidebar-text")?.textContent.trim()||"This folder",d=l.map(r=>`${r.text}: ${r.meta}`).join(" \xB7 ");i.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${s?"danger":"info"}`,i.dataset.countFor=u,i.dataset.badgeLabel=d,i.setAttribute("aria-label",d),i.textContent=String(o),R.set(u,{title:a,items:l})}}const R=new Map;function ot(e,n){if(!e.url||!n)return"";const t=n[e.url];return!t||!(t.count>0)&&!t.showZero?"":le(e.url,t)}const de=["info","attention","danger","live"],it={"#/pages":["page","pages"],"#/media":["file in the media library","files in the media library"],"#/menus":["menu","menus"],"#/context-menus":["context menu","context menus"],"#/theme":["element override in the theme","element overrides in the theme"],"#/collections":["collection","collections"],"#/forms":["form","forms"],"#/views":["view","views"],"#/actions":["action","actions"],"#/blocks":["block","blocks"],"#/components":["component","components"],"#/users":["user","users"],"#/plugins":["plugin installed","plugins installed"],"#/system/notifications":["unread notification","unread notifications"]};function D(e,{fallbackLabel:n="",tone:t="info"}={}){const o=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(o)||o<0)return null;const s=typeof e?.label=="string"&&e.label||(n?`${o} - ${n}`:String(o)),l={count:Math.round(o),label:s,tone:de.includes(e?.tone)?e.tone:t};return e?.details&&typeof e.details=="object"&&(l.details=e.details),e?.inventory===!0&&(l.inventory=!0),e?.showZero===!0&&(l.showZero=!0),l}function ue(e,n,t=null,{inFrequent:o=!1}={}){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="theme-switcher")return"";if(e&&e.type==="spacer"){const b=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${m(b)}" aria-hidden="true"></div>`}const s=String(e.text||"?").trim().charAt(0).toUpperCase()||"?",l=e.icon?`<span data-icon="${m(e.icon)}"></span>`:`<span class="sidebar-initial" aria-hidden="true">${m(s)}</span>`,u=m(e.text||""),i=e.url?m(e.url):"",a=Array.isArray(e.items)&&e.items.length>0,d=e.colour?te(e.colour):"",r=je(e),f=[r.hover&&` data-menu-item-hover="${m(r.hover)}"`,r.active&&` data-menu-item-active="${m(r.active)}"`].filter(Boolean).join(""),h=[d&&`color:${m(d)}`,r.accent&&`--dm-menu-highlight:${m(r.accent)}`].filter(Boolean).join(";"),v=h?` style="${h}"`:"",w=e.url?Ye(e.url,e.text||"",{inFrequent:o}):"",q=w?` title="${m(w)}"`:"";let F="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const b=e.badge.variant?te(e.badge.variant):"";F=`<span class="dm-menu-badge"${b?` style="background:${m(b)};color:var(--dm-text-inverse,#fff)"`:""}>${m(String(e.badge.text))}</span>`}if(e.frequent&&e.empty){const b=ne(e,t),A=S.get(b)!==!1;return`<details data-state-key="${m(b)}"${A?" open":""} data-frequent data-frequent-empty>
2
+ <summary${v}>${l} <span class="sidebar-text">${u}</span></summary>
3
3
  <div class="sidebar-children"><p class="sidebar-frequent-empty">Right-click any Tool and choose <strong>Add to Frequent</strong> to keep it here.</p></div>
4
- </details>`}if(u){const b=Q(e,s),A=S.get(b)!==!1,B=e.items.map(N=>le(N,t,b,{inFrequent:!!e.frequent})).join(""),j=e.frequent?' data-frequent title="Your Frequent Tools - right-click any Tool to add or remove it"':"";return`<details data-state-key="${f(b)}"${A?" open":""}${j}>
5
- <summary${v}${k}>${d} <span class="sidebar-text">${i}</span>${q}</summary>
4
+ </details>`}if(a){const b=ne(e,t),A=S.get(b)!==!1,B=e.items.map(I=>ue(I,n,b,{inFrequent:!!e.frequent})).join(""),j=e.frequent?' data-frequent title="Your Frequent Tools - right-click any Tool to add or remove it"':"";return`<details data-state-key="${m(b)}"${A?" open":""}${j}>
5
+ <summary${v}${q}>${l} <span class="sidebar-text">${u}</span>${F}</summary>
6
6
  <div class="sidebar-children">${B}</div>
7
- </details>`}return`<a href="${l}" class="sidebar-link" data-url="${l}"${v}${m}${k}>${d} <span class="sidebar-text">${i}</span>${Ze(e,t)}${q}</a>`}async function Je(e=[],t=[],s=null){const o=g=>g.then(x=>Array.isArray(x)?x.length:Array.isArray(x?.entries)?x.entries.length:0).catch(()=>0);L=tt(e,s);const n=new Set(L.map(g=>g.url)),d={pages:"#/pages",media:"#/media",menus:"#/menus","context-menus":"#/context-menus",theme:"#/theme",collections:"#/collections",forms:"#/forms",views:"#/views",actions:"#/actions",blocks:"#/blocks",components:"#/components",users:"#/users",plugins:"#/plugins",notifications:"#/system/notifications"},i=(g,x)=>z(t,g)&&!n.has(d[g])?x():Promise.resolve(0),[l,u,a,r,m,y,v,w,k,q,b,A,B,j]=await Promise.all([i("pages",()=>o(h.pages.list())),i("media",()=>o(h.media.list())),i("menus",()=>o(h.menus.list())),i("context-menus",()=>o(h.contextMenus.list())),i("theme",()=>h.theme.get().then(g=>(g?.overrides||[]).length).catch(()=>0)),i("collections",()=>o(h.collections.list())),i("forms",()=>o(h.forms.list())),i("views",()=>o(h.views.list())),i("actions",()=>o(h.actions.list())),i("blocks",()=>o(h.blocks.list())),i("components",()=>o(h.components.list())),i("users",()=>o(h.users.list())),i("plugins",()=>o(h.plugins.list())),i("notifications",()=>h.system?.notifications?.unreadCount?.().then(g=>g?.count??0).catch(()=>0)??Promise.resolve(0))]),N={};await Promise.all(L.map(async g=>{const x=await ce(g);x&&(N[g.url]=x)}));const U={"#/pages":l,"#/media":u,"#/menus":a,"#/context-menus":r,"#/theme":m,"#/collections":y,"#/forms":v,"#/views":w,"#/actions":k,"#/blocks":q,"#/components":b,"#/users":A,"#/plugins":B,"#/system/notifications":j},R={};for(const[g,x]of Object.entries(U)){if(n.has(g))continue;const[W,D]=Ye[g]??["item","items"],T=K(x);T&&(R[g]={...T,label:`${T.count} ${T.count===1?W:D}`})}return{...R,...N}}let L=[];const Qe="dm:sidebar-count",et=6e4;function tt(e=[],t=null){const s=[],o=n=>{if(n&&!(t&&typeof n.url=="string"&&n.url.startsWith("#/")&&!xe(t,n.url.slice(1)))){n.url&&typeof n.countUrl=="string"&&n.countUrl&&s.push({url:n.url,countUrl:n.countUrl,text:n.text||"",countLabel:n.countLabel||"",inventory:n.inventory===!0});for(const d of[...n.children||[],...n.items||[]])o(d)}};for(const n of e??[])o(n?.item??n);return s}async function ce(e){try{const t=new Date,s=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`,o=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${s}`;return K(await H.get(o),{fallbackLabel:e.countLabel||e.text})}catch{return null}}function de(e,t,s){ve(t,s);const o=[...e.querySelectorAll("a.sidebar-link")].filter(n=>n.dataset.url===t);if(o.length){for(const n of o){const d=n.querySelector(".sidebar-badge[data-count-for]");if(!s||!(s.count>0)&&!s.showZero)d?.remove();else{const i=document.createRange();i.selectNodeContents(n);const l=i.createContextualFragment(ie(t,s)).firstElementChild;d?d.replaceWith(l):n.querySelector(".sidebar-text")?.after(l)}}V.set(t,s),ae(e)}}const V=new Map;async function X(e,t=null){const s=t?L.filter(o=>o.url===t):L;await Promise.all(s.map(async o=>{const n=await ce(o),d=V.get(o.url);JSON.stringify(d??null)!==JSON.stringify(n)&&de(e,o.url,n)}))}function nt(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener(Qe,t=>{const s=t.detail||{};if(s.url)if(s.count!==void 0){const o=L.find(n=>n.url===s.url);de(e,s.url,K(s,{fallbackLabel:o?.countLabel||o?.text||""}))}else X(e,s.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&X(e)},et),document.addEventListener("visibilitychange",()=>{document.hidden||X(e)}),st(e))}function ue(e,t,s){const o=(i,l,u)=>{const a=document.createElement(i);return l&&(a.className=l),u!=null&&(a.textContent=String(u)),a};if(!s){e.textContent=t;return}e.appendChild(o("strong","dm-badge-pop-title",s.title||t)),(s.rows?.length||!s.items?.length)&&e.appendChild(o("p","dm-badge-pop-sub",t));const n=Array.isArray(s.rows)?s.rows.slice(0,6):[];if(n.length){const i=o("dl","dm-badge-pop-rows");for(const l of n)i.appendChild(o("dt",null,l.label)),i.appendChild(o("dd",l.tone==="danger"?"is-danger":null,l.value));e.appendChild(i)}const d=Array.isArray(s.items)?s.items.slice(0,6):[];if(d.length){const i=o("ul","dm-badge-pop-items");for(const l of d){const u=o("li",l.tone==="danger"?"is-danger":null);u.appendChild(o("span","dm-badge-pop-text",l.text)),l.meta&&u.appendChild(o("span","dm-badge-pop-meta",l.meta)),i.appendChild(u)}e.appendChild(i)}else s.empty&&e.appendChild(o("p","dm-badge-pop-empty",s.empty))}function st(e){let t=null,s=null;const o=()=>e.classList.contains("is-collapsed"),n=()=>{clearTimeout(s),t?.remove(),t=null},d=(a,r)=>{n(),s=setTimeout(()=>{t=document.createElement("div"),t.className="dm-help-pop dm-badge-pop",t.setAttribute("role","tooltip"),r(t),document.body.appendChild(t);const m=a.getBoundingClientRect(),y=t.getBoundingClientRect(),v=Math.min(m.right+10,innerWidth-y.width-8);t.style.left=`${Math.max(8,v)}px`,t.style.top=`${Math.max(8,Math.min(m.top+m.height/2-y.height/2,innerHeight-y.height-8))}px`},150)},i=a=>{const r=a?.dataset.badgeLabel;if(!r)return n();d(a,m=>ue(m,r,I.get(a.dataset.countFor)))},l=a=>{const r=a.querySelector(":scope > .sidebar-text")?.textContent.trim()||"",m=a.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");d(a,y=>{const v=m&&I.get(m.dataset.countFor);if(v)return ue(y,m.dataset.badgeLabel,{...v,title:v.title||r});if(!m){y.textContent=r;return}const w=document.createElement("strong");w.className="dm-badge-pop-title",w.textContent=r;const k=document.createElement("p");k.className="dm-badge-pop-sub",k.textContent=m.dataset.badgeLabel,y.append(w,k)})},u=a=>{const r=a.closest?.("a.sidebar-link, summary");return r&&e.contains(r)?r:null};e.addEventListener("mouseover",a=>{if(o()){const m=u(a.target);m&&!m.contains(a.relatedTarget)&&l(m);return}const r=a.target.closest?.(".sidebar-badge[data-badge-label]");r&&i(r)}),e.addEventListener("mouseout",a=>{if(o()){const r=u(a.target);r&&!r.contains(a.relatedTarget)&&n();return}a.target.closest?.(".sidebar-badge[data-badge-label]")&&n()}),e.addEventListener("focusin",a=>{const r=a.target.closest?.("a, summary");if(o()&&r&&e.contains(r)&&r.querySelector(":scope > .sidebar-text"))return l(r);const m=r?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");m?i(m):n()}),e.addEventListener("focusout",n),e.addEventListener("click",n),window.addEventListener("scroll",n,!0),window.addEventListener("hashchange",n)}async function ot(e=[],t=null){if(t?.title)return t;if(!z(e,"settings"))return{title:"Domma CMS"};try{const s=await h.settings.get();return{...s?.adminBrand,title:s?.adminBrand?.title||s?.title||"Domma CMS"}}catch{return{title:"Domma CMS"}}}export async function renderAdminSidebar({mount:e,permissions:t,adminScope:s=null,brand:o=null}){const n=$(e).get(0);if(!n)return;const d=Ue(h,F()).catch(()=>{});let i,l=null,u=null,a=null,r=null,m=null;try{const c=await h.get("/sidebar/menu");i=Array.isArray(c?.items)?c.items:null,l=c?.variant||null,u=c?.position||null,a=c?.style||null,r=c?.float||null,m=c?.highlight||null}catch{i=null}(!i||!i.some(c=>c&&(!c.hidden||c.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),i=Ke.slice());let y=[];try{y=await H.get("/api/sidebar/registered-items")||[]}catch{}i=Ae(i,ee),i=ke(i,y.filter(c=>(c?.item??c)?.url!==ee)),i=Ce(i),i=Fe(i,[qe()]);const v=await Ve(t);v.length&&Xe(i,v),i=Ee(i,t,s),i=Le(i),await d;const w=Pe(i,G(F()),{hidden:Ie(F())});w&&(i=[w,...i]),De(i);const k=y.filter(c=>z(t,c?.item?.permission)),[q,b]=await Promise.all([ye({user:F(),permissions:t,adminScope:s,registered:k,tree:i,actions:{linkItems:pe,folderItems:he},fetcher:()=>Je(k,t,s)}),ot(t,o)]),A=l?` dm-admin-sidebar--${f(l)}`:"";function B(c,C="px"){if(c==null)return c;const p=String(c).trim();return/^\d+(\.\d+)?$/.test(p)?`${p}${C}`:p}let j="";if(a){const c=[];a.fontFamily&&c.push(`font-family: ${a.fontFamily}, sans-serif`),a.fontSize&&c.push(`font-size: ${B(a.fontSize)}`),a.fontWeight&&c.push(`font-weight: ${a.fontWeight}`),a.letterSpacing&&c.push(`letter-spacing: ${B(a.letterSpacing,"em")}`);const C=[];if(c.length&&C.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 { ${c.join("; ")} }`),a.iconSize){const p=B(a.iconSize);C.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${p} !important; height: ${p} !important; }`)}C.length&&(j=`<style data-admin-sidebar-style>${C.join(" ")}</style>`)}const N=Me(t)?Be():"",U=String(b.logo||"").trim(),R=/^(?:\/(?!\/)|https?:\/\/)/i.test(U)?U:"",x=`<div class="dm-admin-sidebar-header">${R?`<img src="${f(R)}" alt="" style="width:28px;height:28px;object-fit:contain;flex-shrink:0;">`:`<span data-icon="${f(b.icon||"layout")}"></span>`}<span class="dm-admin-sidebar-title">${f(b.title)}</span>${N}${He()}</div>`,W=i.some(c=>c&&c.type==="spacer"&&c.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",D=`${j}<nav class="dm-admin-sidebar${A}${W}">${x}${i.map(c=>le(c,q)).join("")}</nav>`,T=document.createRange();T.selectNodeContents(n);const ge=T.createContextualFragment(D);n.replaceChildren(ge),Domma.icons.scan(n),n.querySelector(".dm-admin-sidebar-arrange")?.addEventListener("click",()=>Te(n));for(const c of L)V.set(c.url,q[c.url]??null);ae(n),nt(n),n.removeAttribute("data-menu-hover"),n.removeAttribute("data-menu-active"),n.style.removeProperty("--dm-menu-highlight"),we(n,{highlight:m});const O=document.querySelector(".dashboard-wrapper");["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(c=>{n.style[c]=""}),n.classList.remove("dm-menu-floating"),O&&(O.style.paddingLeft="",O.style.paddingRight=""),n.__dmGutter=()=>{if(u!=="floating"||!O)return;const c=r&&r.anchor||"TL",C=Number.isFinite(Number(r?.offsetX))?Number(r.offsetX):16,p=n.classList.contains("is-collapsed")?Ne:parseInt(n.style.width,10)||n.getBoundingClientRect().width||260,P=Math.round(p+C*2)+"px";c==="R"||c==="TR"||c==="BR"?O.style.paddingRight=P:O.style.paddingLeft=P},u==="floating"&&$e(n,{position:"floating",float:r||{}}),n.querySelectorAll("details").forEach(c=>{c.addEventListener("toggle",function(){const C=this.getAttribute("data-state-key");C&&S.set(C,this.open);const p=this.querySelector(":scope > .sidebar-children");if(p)if(this.open){const P=p.scrollHeight;p.style.maxHeight="0",requestAnimationFrame(()=>{p.style.maxHeight=P+"px",p.addEventListener("transitionend",function be(){p.style.maxHeight="none",p.removeEventListener("transitionend",be)})})}else{const P=p.scrollHeight;p.style.maxHeight=P+"px",requestAnimationFrame(()=>{p.style.maxHeight="0"})}})}),je(n),at(n),n.dataset.routeWired||(n.dataset.routeWired="1",M.subscribe("router:afterChange",()=>n.__dmSetActive?.()));function Y(){const c=/^#\/(dashboards|my-dashboard)\b/.test(location.hash)?"#/":location.hash||"#/";$(n).find(".sidebar-link").removeClass("active"),$(n).find(`.sidebar-link[data-url="${c}"]`).addClass("active")}Y(),n.__dmSetActive=Y}function me(){try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}function Z(e){return`${location.origin}${location.pathname}${e}`}function _(e,t){const s=F(),o=e(G(s));if(!ze(s,o)){E.toast("Frequent could not be saved - this browser is blocking storage.",{type:"error"});return}o.length&&se(s,!1),fe(s),t&&E.toast(t,{type:"success",duration:2500}),me()}function fe(e){_e(h,e).catch(()=>E.toast("Frequent is saved in this browser only - the server could not keep it.",{type:"warning"}))}function it(e){if(!se(F(),e)){E.toast("Frequent could not be saved - this browser is blocking storage.",{type:"error"});return}fe(F()),E.toast(e?"Frequent hidden. Add a Tool to it from its right-click menu to bring it back.":"Frequent shown.",{type:"success",duration:3500}),me()}function pe(e){const t=e.dataset.url;if(!t)return[];const s=e.querySelector(".sidebar-text")?.textContent.trim()||t,o=G(F()),n=o.includes(t),d=!!e.closest("[data-frequent]"),i=o.indexOf(t),l=!n&&o.length>=te;return[{header:s},{label:"Open",icon:"arrow-right",...oe(t)&&{shortcut:Ge(oe(t))},action:()=>{location.hash=t}},{label:"Open in a new tab",icon:"external-link",action:()=>window.open(Z(t),"_blank","noopener")},{type:"divider"},n?{label:"Remove from Frequent",icon:"star",action:()=>_(u=>Re(u,t),`${s} removed from Frequent.`)}:{label:l?`Frequent is full (${te})`:"Add to Frequent",icon:"star",disabled:l,action:()=>_(u=>Oe(u,t),`${s} added to Frequent.`)},...d?[{label:"Move up",icon:"arrow-up",disabled:i<=0,action:()=>_(u=>ne(u,t,-1))},{label:"Move down",icon:"arrow-down",disabled:i<0||i>=o.length-1,action:()=>_(u=>ne(u,t,1))}]:[],{type:"divider"},{label:"Copy link",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(Z(t)),E.toast("Link copied.",{type:"success",duration:2e3})}catch{E.toast(Z(t),{type:"info",duration:8e3})}}}]}function he(e,t,s={}){const o=e.closest("details, [data-frequent]"),n=s.isOpen||(()=>!!o?.open),d=s.setOpen||(u=>{o&&(o.open=u)}),i=e.querySelector(".sidebar-text")?.textContent.trim()||"Folder",l=s.setAll||(u=>t.querySelectorAll("details").forEach(a=>{a.open!==u&&(a.open=u)}));return[{header:i},{label:n()?"Close":"Open",icon:n()?"chevron-up":"chevron-down",action:()=>d(!n())},...s.singleBranch?[]:[{label:"Open every folder",icon:"chevrons-down",action:()=>l(!0)}],{label:s.singleBranch?"Close all menus":"Close every folder",icon:"chevrons-up",action:()=>l(!1)},...o?.hasAttribute("data-frequent-empty")?[{type:"divider"},{label:"Hide Frequent until I add a Tool",icon:"eye-off",action:()=>it(!0)}]:o?.hasAttribute("data-frequent")?[{type:"divider"},{label:"Clear Frequent",icon:"x",action:()=>_(()=>[],"Frequent cleared.")}]:[]]}function at(e){e.dataset.ctxWired||typeof E>"u"||!E.contextMenu||(e.dataset.ctxWired="1",E.contextMenu(e,{match:"a.sidebar-link, details > summary",inherit:!1,density:"compact",items:t=>t.matches("summary")?he(t,e):pe(t)}))}
7
+ </details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${v}${f}${q}>${l} <span class="sidebar-text">${u}</span>${ot(e,n)}${F}</a>`}async function at(e=[],n=[],t=null){const o=y=>y.then(C=>Array.isArray(C)?C.length:Array.isArray(C?.entries)?C.entries.length:0).catch(()=>0),s=fe(e,t),l=new Set(s.map(y=>y.url)),u={pages:"#/pages",media:"#/media",menus:"#/menus","context-menus":"#/context-menus",theme:"#/theme",collections:"#/collections",forms:"#/forms",views:"#/views",actions:"#/actions",blocks:"#/blocks",components:"#/components",users:"#/users",plugins:"#/plugins",notifications:"#/system/notifications"},i=(y,C)=>W(n,y)&&!l.has(u[y])?C():Promise.resolve(0),[a,d,r,f,h,v,w,q,F,b,A,B,j,I]=await Promise.all([i("pages",()=>o(g.pages.list())),i("media",()=>o(g.media.list())),i("menus",()=>o(g.menus.list())),i("context-menus",()=>o(g.contextMenus.list())),i("theme",()=>g.theme.get().then(y=>(y?.overrides||[]).length).catch(()=>0)),i("collections",()=>o(g.collections.list())),i("forms",()=>o(g.forms.list())),i("views",()=>o(g.views.list())),i("actions",()=>o(g.actions.list())),i("blocks",()=>o(g.blocks.list())),i("components",()=>o(g.components.list())),i("users",()=>o(g.users.list())),i("plugins",()=>o(g.plugins.list())),i("notifications",()=>g.system?.notifications?.unreadCount?.().then(y=>y?.count??0).catch(()=>0)??Promise.resolve(0))]),P=await pe(s),U={"#/pages":a,"#/media":d,"#/menus":r,"#/context-menus":f,"#/theme":h,"#/collections":v,"#/forms":w,"#/views":q,"#/actions":F,"#/blocks":b,"#/components":A,"#/users":B,"#/plugins":j,"#/system/notifications":I},z={};for(const[y,C]of Object.entries(U)){if(l.has(y))continue;const[Y,G]=it[y]??["item","items"],O=D(C);O&&(z[y]={...O,label:`${O.count} ${O.count===1?Y:G}`})}return{...z,...P}}let L=[];const rt="dm:sidebar-count",lt=6e4,ct=ke(4);function fe(e=[],n=null){const t=[],o=s=>{if(s&&!(n&&typeof s.url=="string"&&s.url.startsWith("#/")&&!Fe(n,s.url.slice(1)))){s.url&&typeof s.countUrl=="string"&&s.countUrl&&t.push({url:s.url,countUrl:s.countUrl,text:s.text||"",countLabel:s.countLabel||"",inventory:s.inventory===!0});for(const l of[...s.children||[],...s.items||[]])o(l)}};for(const s of e??[])o(s?.item??s);return t}async function me(e,{priority:n=!1}={}){try{const t=new Date,o=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`,s=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${o}`,l=k()?.id,u=await ct(()=>!Se()||k()?.id!==l?null:H.get(s,{priority:"low"}),{key:JSON.stringify([l,s]),priority:n});return D(u,{fallbackLabel:e.countLabel||e.text})}catch{return null}}function V(e,n,t){Ae(n,t);const o=[...e.querySelectorAll("a.sidebar-link")].filter(s=>s.dataset.url===n);if(o.length){for(const s of o){const l=s.querySelector(".sidebar-badge[data-count-for]");if(!t||!(t.count>0)&&!t.showZero)l?.remove();else{const u=document.createRange();u.selectNodeContents(s);const i=u.createContextualFragment(le(n,t)).firstElementChild;l?l.replaceWith(i):s.querySelector(".sidebar-text")?.after(i)}}X.set(n,t),ce(e)}}const X=new Map;async function pe(e){const n=await qe(e,{legacy:async t=>{const o=await Promise.all(t.map(async s=>[s.url,await me(s)]));return Object.fromEntries(o)}});return Object.fromEntries(e.map(t=>[t.url,n[t.url]==null?null:D(n[t.url],{fallbackLabel:t.countLabel||t.text})]))}async function Z(e,n=null){const t=k()?.id,o=L;try{const s=n?{[n]:await me(L.find(l=>l.url===n),{priority:!0})}:await pe(L);if(k()?.id!==t||L!==o)return;for(const[l,u]of Object.entries(s)){const i=X.get(l);JSON.stringify(i??null)!==JSON.stringify(u)&&V(e,l,u)}}catch{}}function dt(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener(rt,n=>{const t=n.detail||{};if(t.url)if(t.count!==void 0){const o=L.find(s=>s.url===t.url);V(e,t.url,D(t,{fallbackLabel:o?.countLabel||o?.text||""}))}else Z(e,t.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&Z(e)},lt),document.addEventListener("visibilitychange",()=>{document.hidden||Z(e)}),ut(e))}function he(e,n,t){const o=(u,i,a)=>{const d=document.createElement(u);return i&&(d.className=i),a!=null&&(d.textContent=String(a)),d};if(!t){e.textContent=n;return}e.appendChild(o("strong","dm-badge-pop-title",t.title||n)),(t.rows?.length||!t.items?.length)&&e.appendChild(o("p","dm-badge-pop-sub",n));const s=Array.isArray(t.rows)?t.rows.slice(0,6):[];if(s.length){const u=o("dl","dm-badge-pop-rows");for(const i of s)u.appendChild(o("dt",null,i.label)),u.appendChild(o("dd",i.tone==="danger"?"is-danger":null,i.value));e.appendChild(u)}const l=Array.isArray(t.items)?t.items.slice(0,6):[];if(l.length){const u=o("ul","dm-badge-pop-items");for(const i of l){const a=o("li",i.tone==="danger"?"is-danger":null);a.appendChild(o("span","dm-badge-pop-text",i.text)),i.meta&&a.appendChild(o("span","dm-badge-pop-meta",i.meta)),u.appendChild(a)}e.appendChild(u)}else t.empty&&e.appendChild(o("p","dm-badge-pop-empty",t.empty))}function ut(e){let n=null,t=null;const o=()=>e.classList.contains("is-collapsed"),s=()=>{clearTimeout(t),n?.remove(),n=null},l=(d,r)=>{s(),t=setTimeout(()=>{n=document.createElement("div"),n.className="dm-help-pop dm-badge-pop",n.setAttribute("role","tooltip"),r(n),document.body.appendChild(n);const f=d.getBoundingClientRect(),h=n.getBoundingClientRect(),v=Math.min(f.right+10,innerWidth-h.width-8);n.style.left=`${Math.max(8,v)}px`,n.style.top=`${Math.max(8,Math.min(f.top+f.height/2-h.height/2,innerHeight-h.height-8))}px`},150)},u=d=>{const r=d?.dataset.badgeLabel;if(!r)return s();l(d,f=>he(f,r,R.get(d.dataset.countFor)))},i=d=>{const r=d.querySelector(":scope > .sidebar-text")?.textContent.trim()||"",f=d.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");l(d,h=>{const v=f&&R.get(f.dataset.countFor);if(v)return he(h,f.dataset.badgeLabel,{...v,title:v.title||r});if(!f){h.textContent=r;return}const w=document.createElement("strong");w.className="dm-badge-pop-title",w.textContent=r;const q=document.createElement("p");q.className="dm-badge-pop-sub",q.textContent=f.dataset.badgeLabel,h.append(w,q)})},a=d=>{const r=d.closest?.("a.sidebar-link, summary");return r&&e.contains(r)?r:null};e.addEventListener("mouseover",d=>{if(o()){const f=a(d.target);f&&!f.contains(d.relatedTarget)&&i(f);return}const r=d.target.closest?.(".sidebar-badge[data-badge-label]");r&&u(r)}),e.addEventListener("mouseout",d=>{if(o()){const r=a(d.target);r&&!r.contains(d.relatedTarget)&&s();return}d.target.closest?.(".sidebar-badge[data-badge-label]")&&s()}),e.addEventListener("focusin",d=>{const r=d.target.closest?.("a, summary");if(o()&&r&&e.contains(r)&&r.querySelector(":scope > .sidebar-text"))return i(r);const f=r?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");f?u(f):s()}),e.addEventListener("focusout",s),e.addEventListener("click",s),window.addEventListener("scroll",s,!0),window.addEventListener("hashchange",s)}async function ft(e=[],n=null){if(n?.title)return n;if(!W(e,"settings"))return{title:"Domma CMS"};try{const t=await g.settings.get();return{...t?.adminBrand,title:t?.adminBrand?.title||t?.title||"Domma CMS"}}catch{return{title:"Domma CMS"}}}export async function renderAdminSidebar({mount:e,permissions:n,adminScope:t=null,brand:o=null}){const s=$(e).get(0);if(!s)return;const l=Ze(g,k()).catch(()=>{}),u=Promise.resolve().then(()=>H.get("/api/sidebar/registered-items")).catch(()=>[]),i=nt(n);let a,d=null,r=null,f=null,h=null,v=null;try{const c=await g.get("/sidebar/menu");a=Array.isArray(c?.items)?c.items:null,d=c?.variant||null,r=c?.position||null,f=c?.style||null,h=c?.float||null,v=c?.highlight||null}catch{a=null}(!a||!a.some(c=>c&&(!c.hidden||c.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),a=tt.slice());let w=[];try{w=await u||[]}catch{}a=Te(a,se),a=Be(a,w.filter(c=>(c?.item??c)?.url!==se)),a=Oe(a),a=He(a,[Me()]);const q=await i;q.length&&st(a,q),a=Re(a,n,t),a=Ne(a),await l;const F=Ge(a,K(k()),{hidden:Ke(k())});F&&(a=[F,...a]),Qe(a);const b=w.filter(c=>W(n,c?.item?.permission));L=fe(b,t);const A=Ce({user:k(),permissions:n,adminScope:t,registered:b,tree:a,actions:{linkItems:ye,folderItems:ve},fetcher:()=>at(b,n,t)}),B=A.counts,j=await ft(n,o),I=d?` dm-admin-sidebar--${m(d)}`:"";function P(c,x="px"){if(c==null)return c;const p=String(c).trim();return/^\d+(\.\d+)?$/.test(p)?`${p}${x}`:p}let U="";if(f){const c=[];f.fontFamily&&c.push(`font-family: ${f.fontFamily}, sans-serif`),f.fontSize&&c.push(`font-size: ${P(f.fontSize)}`),f.fontWeight&&c.push(`font-weight: ${f.fontWeight}`),f.letterSpacing&&c.push(`letter-spacing: ${P(f.letterSpacing,"em")}`);const x=[];if(c.length&&x.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 { ${c.join("; ")} }`),f.iconSize){const p=P(f.iconSize);x.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${p} !important; height: ${p} !important; }`)}x.length&&(U=`<style data-admin-sidebar-style>${x.join(" ")}</style>`)}const z=Pe(n)?Ie():"",y=String(j.logo||"").trim(),C=/^(?:\/(?!\/)|https?:\/\/)/i.test(y)?y:"",G=`<div class="dm-admin-sidebar-header">${C?`<img src="${m(C)}" alt="" style="width:28px;height:28px;object-fit:contain;flex-shrink:0;">`:`<span data-icon="${m(j.icon||"layout")}"></span>`}<span class="dm-admin-sidebar-title">${m(j.title)}</span>${z}${Ue()}</div>`,O=a.some(c=>c&&c.type==="spacer"&&c.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",xe=`${U}<nav class="dm-admin-sidebar${I}${O}">${G}${a.map(c=>ue(c,B)).join("")}</nav>`,Q=document.createRange();Q.selectNodeContents(s);const $e=Q.createContextualFragment(xe);s.replaceChildren($e),Domma.icons.scan(s),s.querySelector(".dm-admin-sidebar-arrange")?.addEventListener("click",()=>_e(s));for(const c of L)X.set(c.url,B[c.url]??null);ce(s),dt(s),A.refresh.then(c=>{if(!(!A.isActive()||!s.isConnected))for(const[x,p]of Object.entries(c))V(s,x,p)}),s.removeAttribute("data-menu-hover"),s.removeAttribute("data-menu-active"),s.style.removeProperty("--dm-menu-highlight"),Ee(s,{highlight:v});const T=document.querySelector(".dashboard-wrapper");["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(c=>{s.style[c]=""}),s.classList.remove("dm-menu-floating"),T&&(T.style.paddingLeft="",T.style.paddingRight=""),s.__dmGutter=()=>{if(r!=="floating"||!T)return;const c=h&&h.anchor||"TL",x=Number.isFinite(Number(h?.offsetX))?Number(h.offsetX):16,p=s.classList.contains("is-collapsed")?We:parseInt(s.style.width,10)||s.getBoundingClientRect().width||260,N=Math.round(p+x*2)+"px";c==="R"||c==="TR"||c==="BR"?T.style.paddingRight=N:T.style.paddingLeft=N},r==="floating"&&Le(s,{position:"floating",float:h||{}}),s.querySelectorAll("details").forEach(c=>{c.addEventListener("toggle",function(){const x=this.getAttribute("data-state-key");x&&S.set(x,this.open);const p=this.querySelector(":scope > .sidebar-children");if(p)if(this.open){const N=p.scrollHeight;p.style.maxHeight="0",requestAnimationFrame(()=>{p.style.maxHeight=N+"px",p.addEventListener("transitionend",function we(){p.style.maxHeight="none",p.removeEventListener("transitionend",we)})})}else{const N=p.scrollHeight;p.style.maxHeight=N+"px",requestAnimationFrame(()=>{p.style.maxHeight="0"})}})}),ze(s),pt(s),s.dataset.routeWired||(s.dataset.routeWired="1",M.subscribe("router:afterChange",()=>s.__dmSetActive?.()));function ee(){const c=/^#\/(dashboards|my-dashboard)\b/.test(location.hash)?"#/":location.hash||"#/";$(s).find(".sidebar-link").removeClass("active"),$(s).find(`.sidebar-link[data-url="${c}"]`).addClass("active")}ee(),s.__dmSetActive=ee}function ge(){try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}function J(e){return`${location.origin}${location.pathname}${e}`}function _(e,n){const t=k(),o=e(K(t));if(!Je(t,o)){E.toast("Frequent could not be saved - this browser is blocking storage.",{type:"error"});return}o.length&&ae(t,!1),be(t),n&&E.toast(n,{type:"success",duration:2500}),ge()}function be(e){Xe(g,e).catch(()=>E.toast("Frequent is saved in this browser only - the server could not keep it.",{type:"warning"}))}function mt(e){if(!ae(k(),e)){E.toast("Frequent could not be saved - this browser is blocking storage.",{type:"error"});return}be(k()),E.toast(e?"Frequent hidden. Add a Tool to it from its right-click menu to bring it back.":"Frequent shown.",{type:"success",duration:3500}),ge()}function ye(e){const n=e.dataset.url;if(!n)return[];const t=e.querySelector(".sidebar-text")?.textContent.trim()||n,o=K(k()),s=o.includes(n),l=!!e.closest("[data-frequent]"),u=o.indexOf(n),i=!s&&o.length>=oe;return[{header:t},{label:"Open",icon:"arrow-right",...re(n)&&{shortcut:et(re(n))},action:()=>{location.hash=n}},{label:"Open in a new tab",icon:"external-link",action:()=>window.open(J(n),"_blank","noopener")},{type:"divider"},s?{label:"Remove from Frequent",icon:"star",action:()=>_(a=>Ve(a,n),`${t} removed from Frequent.`)}:{label:i?`Frequent is full (${oe})`:"Add to Frequent",icon:"star",disabled:i,action:()=>_(a=>De(a,n),`${t} added to Frequent.`)},...l?[{label:"Move up",icon:"arrow-up",disabled:u<=0,action:()=>_(a=>ie(a,n,-1))},{label:"Move down",icon:"arrow-down",disabled:u<0||u>=o.length-1,action:()=>_(a=>ie(a,n,1))}]:[],{type:"divider"},{label:"Copy link",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(J(n)),E.toast("Link copied.",{type:"success",duration:2e3})}catch{E.toast(J(n),{type:"info",duration:8e3})}}}]}function ve(e,n,t={}){const o=e.closest("details, [data-frequent]"),s=t.isOpen||(()=>!!o?.open),l=t.setOpen||(a=>{o&&(o.open=a)}),u=e.querySelector(".sidebar-text")?.textContent.trim()||"Folder",i=t.setAll||(a=>n.querySelectorAll("details").forEach(d=>{d.open!==a&&(d.open=a)}));return[{header:u},{label:s()?"Close":"Open",icon:s()?"chevron-up":"chevron-down",action:()=>l(!s())},...t.singleBranch?[]:[{label:"Open every folder",icon:"chevrons-down",action:()=>i(!0)}],{label:t.singleBranch?"Close all menus":"Close every folder",icon:"chevrons-up",action:()=>i(!1)},...o?.hasAttribute("data-frequent-empty")?[{type:"divider"},{label:"Hide Frequent until I add a Tool",icon:"eye-off",action:()=>mt(!0)}]:o?.hasAttribute("data-frequent")?[{type:"divider"},{label:"Clear Frequent",icon:"x",action:()=>_(()=>[],"Frequent cleared.")}]:[]]}function pt(e){e.dataset.ctxWired||typeof E>"u"||!E.contextMenu||(e.dataset.ctxWired="1",E.contextMenu(e,{match:"a.sidebar-link, details > summary",inherit:!1,density:"compact",items:n=>n.matches("summary")?ve(n,e):ye(n)}))}
@@ -1 +1 @@
1
- import{dashboardView as o}from"./dashboard.js";import{dashboardsView as i}from"./dashboards.js";import{pagesView as r}from"./pages.js";import{pageEditorView as e}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{themeView as m}from"./theme.js";import{notificationsView as p}from"./notifications.js";import{layoutsView as s}from"./layouts.js";import{mediaView as n}from"./media.js";import{loginView as f}from"./login.js";import{usersView as w}from"./users.js";import{userEditorView as c}from"./user-editor.js";import{pluginsView as V}from"./plugins.js";import{pluginCodeView as a}from"./plugin-code.js";import{documentationView as d}from"./documentation.js";import{tutorialsView as l}from"./tutorials.js";import{docViews as u}from"./doc-pages.js";import{pluginGuidesView as E,pluginGuideView as g}from"./plugin-guides.js";import{apiReferenceView as h}from"./api-reference.js";import{collectionsView as v}from"./collections.js";import{collectionEditorView as b}from"./collection-editor.js";import{collectionEntriesView as y}from"./collection-entries.js";import{formsView as j}from"./forms.js";import{formEditorView as L}from"./form-editor.js";import{formSubmissionsView as x}from"./form-submissions.js";import{viewsListView as M}from"./views-list.js";import{viewEditorView as k}from"./view-editor.js";import{viewPreviewView as D}from"./view-preview.js";import{actionsListView as G}from"./actions-list.js";import{actionEditorView as P}from"./action-editor.js";import{proDocsView as R}from"./pro-docs.js";import{blocksView as S}from"./blocks.js";import{componentsView as A}from"./components.js";import{myProfileView as C}from"./my-profile.js";import{rolesView as I}from"./roles.js";import{roleEditorView as N}from"./role-editor.js";import{effectsView as T}from"./effects.js";import{searchView as U}from"./search.js";import{contentApiView as q}from"./content-api.js";import{notesView as z}from"./notes.js";import{todoView as B}from"./todo.js";import{calculatorView as F}from"../lib/calculator.js";import{analyticsView as H}from"./analytics.js";import{seoView as J}from"./seo.js";import{wordpressImportView as K}from"./wordpress-import.js";import{contactsView as O}from"./contacts.js";import{callNewView as Q}from"./call-new.js";import{menusView as W}from"./menus.js";import{contextMenusView as X}from"./context-menus.js";import{contextMenuEditorView as Y}from"./context-menu-editor.js";import{menuEditorView as Z}from"./menu-editor.js";import{menuLocationsView as _}from"./menu-locations.js";import{projectsView as $}from"./projects.js";import{projectDetailView as oo}from"./project-detail.js";import{projectSettingsView as io}from"./project-settings.js";import{apiTokensView as ro}from"./api-tokens.js";import{apiEndpointsView as eo}from"./api-endpoints.js";import{apiEndpointEditorView as to}from"./api-endpoint-editor.js";const mo={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:$,projectDetail:oo,projectSettings:io,apiTokens:ro,apiEndpoints:eo,apiEndpointEditor:to,dashboard:o,dashboards:i,pages:r,pageEditor:e,settings:t,theme:m,layouts:s,media:n,login:f,users:w,userEditor:c,plugins:V,pluginCode:a,documentation:d,tutorials:l,apiReference:h,collections:v,collectionEditor:b,collectionEntries:y,forms:j,formEditor:L,formSubmissions:x,viewsList:M,viewEditor:k,viewPreview:D,actionsList:G,actionEditor:P,proDocs:R,blocks:S,components:A,myProfile:C,roles:I,roleEditor:N,effects:T,search:U,contentApi:q,notes:z,todo:B,calculator:F,analytics:H,seo:J,wordpressImport:K,contacts:O,callNew:Q,notifications:p,menus:W,contextMenus:X,contextMenuEditor:Y,menuEditor:Z,menuLocations:_,menusRedirect:mo,pluginGuides:E,pluginGuide:g,...u};
1
+ const t={templateUrl:"",async onMount(){location.hash="#/menus"}};export const viewLoaders={projects:()=>import("./projects.js").then(e=>e.projectsView),projectDetail:()=>import("./project-detail.js").then(e=>e.projectDetailView),projectSettings:()=>import("./project-settings.js").then(e=>e.projectSettingsView),apiTokens:()=>import("./api-tokens.js").then(e=>e.apiTokensView),apiEndpoints:()=>import("./api-endpoints.js").then(e=>e.apiEndpointsView),apiEndpointEditor:()=>import("./api-endpoint-editor.js").then(e=>e.apiEndpointEditorView),dashboard:()=>import("./dashboard.js").then(e=>e.dashboardView),dashboards:()=>import("./dashboards.js").then(e=>e.dashboardsView),pages:()=>import("./pages.js").then(e=>e.pagesView),pageEditor:()=>import("./page-editor.js").then(e=>e.pageEditorView),settings:()=>import("./settings.js").then(e=>e.settingsView),theme:()=>import("./theme.js").then(e=>e.themeView),layouts:()=>import("./layouts.js").then(e=>e.layoutsView),media:()=>import("./media.js").then(e=>e.mediaView),login:()=>import("./login.js").then(e=>e.loginView),users:()=>import("./users.js").then(e=>e.usersView),userEditor:()=>import("./user-editor.js").then(e=>e.userEditorView),plugins:()=>import("./plugins.js").then(e=>e.pluginsView),pluginCode:()=>import("./plugin-code.js").then(e=>e.pluginCodeView),documentation:()=>import("./documentation.js").then(e=>e.documentationView),tutorials:()=>import("./tutorials.js").then(e=>e.tutorialsView),apiReference:()=>import("./api-reference.js").then(e=>e.apiReferenceView),collections:()=>import("./collections.js").then(e=>e.collectionsView),collectionEditor:()=>import("./collection-editor.js").then(e=>e.collectionEditorView),collectionEntries:()=>import("./collection-entries.js").then(e=>e.collectionEntriesView),forms:()=>import("./forms.js").then(e=>e.formsView),formEditor:()=>import("./form-editor.js").then(e=>e.formEditorView),formSubmissions:()=>import("./form-submissions.js").then(e=>e.formSubmissionsView),viewsList:()=>import("./views-list.js").then(e=>e.viewsListView),viewEditor:()=>import("./view-editor.js").then(e=>e.viewEditorView),viewPreview:()=>import("./view-preview.js").then(e=>e.viewPreviewView),actionsList:()=>import("./actions-list.js").then(e=>e.actionsListView),actionEditor:()=>import("./action-editor.js").then(e=>e.actionEditorView),proDocs:()=>import("./pro-docs.js").then(e=>e.proDocsView),blocks:()=>import("./blocks.js").then(e=>e.blocksView),components:()=>import("./components.js").then(e=>e.componentsView),myProfile:()=>import("./my-profile.js").then(e=>e.myProfileView),roles:()=>import("./roles.js").then(e=>e.rolesView),roleEditor:()=>import("./role-editor.js").then(e=>e.roleEditorView),effects:()=>import("./effects.js").then(e=>e.effectsView),search:()=>import("./search.js").then(e=>e.searchView),contentApi:()=>import("./content-api.js").then(e=>e.contentApiView),notes:()=>import("./notes.js").then(e=>e.notesView),todo:()=>import("./todo.js").then(e=>e.todoView),calculator:()=>import("../lib/calculator.js").then(e=>e.calculatorView),analytics:()=>import("./analytics.js").then(e=>e.analyticsView),seo:()=>import("./seo.js").then(e=>e.seoView),wordpressImport:()=>import("./wordpress-import.js").then(e=>e.wordpressImportView),contacts:()=>import("./contacts.js").then(e=>e.contactsView),callNew:()=>import("./call-new.js").then(e=>e.callNewView),notifications:()=>import("./notifications.js").then(e=>e.notificationsView),menus:()=>import("./menus.js").then(e=>e.menusView),contextMenus:()=>import("./context-menus.js").then(e=>e.contextMenusView),contextMenuEditor:()=>import("./context-menu-editor.js").then(e=>e.contextMenuEditorView),menuEditor:()=>import("./menu-editor.js").then(e=>e.menuEditorView),menuLocations:()=>import("./menu-locations.js").then(e=>e.menuLocationsView),menusRedirect:async()=>t,pluginGuides:()=>import("./plugin-guides.js").then(e=>e.pluginGuidesView),pluginGuide:()=>import("./plugin-guides.js").then(e=>e.pluginGuideView)};for(const e of["tutorialCrud","tutorialPlugin","tutorialEditor","tutorialForms","componentsReference","componentsHowto","componentsWalkthrough","componentsRules","usagePages","usageMedia","usageNavigation","usageDconfig","usageShortcodes","usageSiteSettings","usagePlugins","usageUsersRoles","usageViews","usageActions","usageCtaShortcode","usageEditions","usageCollections","usageBlocks","usageTools","usageDashboard","usageSearchEngines","apiRefAuthentication","apiRefPages","apiRefSettings","apiRefLayouts","apiRefNavigation","apiRefMedia","apiRefUsers","apiRefPlugins","apiRefCollections","apiRefViews","apiRefActions","apiRefForms","apiRefScaffold","apiRefExternal","apiRefContent","apiRefBuilder"])viewLoaders[e]=()=>import("./doc-pages.js").then(i=>i.docViews[e]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.10.9",
3
+ "version": "1.10.11",
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",
@@ -157,6 +157,36 @@ function inlineStyles(html) {
157
157
  .filter(Boolean).join(';');
158
158
  }
159
159
 
160
+ // The sanitiser unwraps <body>, and with it whatever the message set
161
+ // there - often the text colour for the whole mail, on a dark design.
162
+ // Keep it on a <div> instead, the legacy bgcolor/text attributes as style.
163
+ const body = DomUtils.findOne((node) => node.name === 'body', dom.children, true);
164
+ const bodyStyle = body && [
165
+ body.attribs.bgcolor ? `background-color:${body.attribs.bgcolor}` : '',
166
+ body.attribs.text ? `color:${body.attribs.text}` : '',
167
+ body.attribs.style ?? ''
168
+ ].filter(Boolean).join(';');
169
+ if (bodyStyle) {
170
+ const wrapper = {...body.attribs, style: bodyStyle};
171
+ delete wrapper.bgcolor;
172
+ delete wrapper.text;
173
+ const div = parseDocument('<div></div>').children[0];
174
+ div.attribs = wrapper;
175
+ for (const child of [...body.children]) DomUtils.appendChild(div, child);
176
+ DomUtils.appendChild(body, div);
177
+ }
178
+
179
+ // CSS property names are case-insensitive, the sanitiser's allowlist is
180
+ // not: `COLOR: #000000` (Outlook-era templates) was dropped, and a sheet
181
+ // rule we had just inlined then won instead of it.
182
+ const shouted = DomUtils.findAll((node) => /(^|;)\s*[a-z-]*[A-Z][\w-]*\s*:/.test(node.attribs?.style ?? ''), dom.children);
183
+ for (const element of shouted) {
184
+ element.attribs.style = element.attribs.style.replace(
185
+ /(^|;)(\s*)([\w-]+)(\s*:)/g,
186
+ (match, lead, space, prop, colon) => `${lead}${space}${prop.toLowerCase()}${colon}`
187
+ );
188
+ }
189
+
160
190
  // A background shorthand loses only what would load; its colour stays.
161
191
  const backgrounds = DomUtils.findAll((node) => /background\s*:/i.test(node.attribs?.style ?? ''), dom.children);
162
192
  for (const element of backgrounds) {
@@ -170,7 +200,7 @@ function inlineStyles(html) {
170
200
  }
171
201
 
172
202
  // Untouched mail goes to the sanitiser exactly as it arrived.
173
- if (!pending.size && !backgrounds.length) return html;
203
+ if (!pending.size && !bodyStyle && !shouted.length && !backgrounds.length) return html;
174
204
  return renderDom(dom);
175
205
  }
176
206
 
@@ -0,0 +1,66 @@
1
+ import {authenticate as defaultAuthenticate, checkVisibility} from '../../middleware/auth.js';
2
+ import {getRegisteredSidebarItems} from '../../services/hooks.js';
3
+
4
+ /** Only registered count routes can be requested, always with the caller's credentials. */
5
+ export async function sidebarCountRoutes(fastify, opts = {}) {
6
+ const authenticate = opts.authenticate || defaultAuthenticate;
7
+ const getItems = opts.getItems || getRegisteredSidebarItems;
8
+ const isItemAllowed = opts.isItemAllowed || (() => true);
9
+ const timeoutMs = opts.countTimeoutMs || 5000;
10
+ fastify.post('/sidebar/counts', {
11
+ preHandler: [authenticate], bodyLimit: 32 * 1024,
12
+ schema: {body: {type: 'object', additionalProperties: false, properties: {
13
+ urls: {type: 'array', maxItems: 256, uniqueItems: true, items: {type: 'string', maxLength: 512}},
14
+ today: {type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$'}
15
+ }}}
16
+ }, async (request, reply) => {
17
+ reply.header('Cache-Control', 'no-store');
18
+ const wanted = request.body?.urls ? new Set(request.body.urls) : null;
19
+ const today = request.body?.today || new Date().toISOString().slice(0, 10);
20
+ const items = new Map();
21
+ const visit = item => {
22
+ if (!item || item.hidden || !checkVisibility(request.user, item.visibility) || !isItemAllowed(item, request.user)) return;
23
+ if (typeof item.url === 'string' && typeof item.countUrl === 'string' && (!wanted || wanted.has(item.url))) {
24
+ try {
25
+ const url = new URL(item.countUrl, 'http://localhost');
26
+ const decoded = decodeURIComponent(url.pathname);
27
+ if (item.countUrl.startsWith('/api/') && url.origin === 'http://localhost'
28
+ && !decoded.includes('\\') && !decoded.includes('\0') && !decoded.includes('..')
29
+ && !decoded.startsWith('/api/sidebar/') && !decoded.startsWith('/api/v1/')
30
+ && !decoded.startsWith('/api/x/') && !decoded.startsWith('/api/content/')) {
31
+ url.searchParams.set('today', today);
32
+ items.set(item.url, url.pathname + url.search);
33
+ }
34
+ } catch { /* malformed plugin count route */ }
35
+ }
36
+ for (const child of [...(item.items || []), ...(item.children || [])]) visit(child);
37
+ };
38
+ getItems().forEach(entry => visit(entry.item || entry));
39
+ const targets = [...new Set(items.values())];
40
+ const answers = new Map();
41
+ let cursor = 0;
42
+ await Promise.all(Array.from({length: Math.min(4, targets.length)}, async () => {
43
+ while (cursor < targets.length) {
44
+ const url = targets[cursor++];
45
+ let timer;
46
+ try {
47
+ const response = await Promise.race([
48
+ fastify.inject({method: 'GET', url, remoteAddress: request.ip,
49
+ headers: request.headers.authorization ? {authorization: request.headers.authorization} : {}}),
50
+ new Promise(resolve => { timer = setTimeout(() => resolve(null), timeoutMs); })
51
+ ]);
52
+ if (!response || response.statusCode !== 200) { answers.set(url, null); continue; }
53
+ const answer = response.json();
54
+ const value = typeof answer === 'object' && answer !== null ? answer.count : answer;
55
+ const count = Number(value);
56
+ if (value == null || !Number.isFinite(count) || count < 0) { answers.set(url, null); continue; }
57
+ if (typeof answer !== 'object') answers.set(url, count);
58
+ else answers.set(url, Object.fromEntries(['count', 'label', 'tone', 'inventory', 'showZero', 'details']
59
+ .filter(key => answer[key] !== undefined).map(key => [key, answer[key]])));
60
+ } catch { answers.set(url, null); }
61
+ finally { clearTimeout(timer); }
62
+ }
63
+ }));
64
+ return {counts: Object.fromEntries([...items].map(([url, target]) => [url, answers.get(target) ?? null]))};
65
+ });
66
+ }
@@ -23,12 +23,15 @@ import {filterItemsForUser, getMenu} from '../../services/menus.js';
23
23
  import {isDashboardPro} from '../../services/widgets.js';
24
24
  import {boardName, readPrefs, readSite} from '../../services/dashboardSettings.js';
25
25
  import {frequentProblems, readFrequent, writeFrequent} from '../../services/sidebarFrequent.js';
26
+ import {sidebarCountRoutes} from './sidebar-counts.js';
26
27
 
27
28
  export const ADMIN_SIDEBAR_SLUG = 'admin-sidebar';
28
29
 
29
30
  export async function sidebarRoutes(fastify, opts = {}) {
30
31
  const authenticate = opts.authenticate || defaultAuthenticate;
31
32
  const signedIn = {preHandler: [authenticate]};
33
+ await sidebarCountRoutes(fastify, {...opts, authenticate,
34
+ isItemAllowed: (item, user) => !item.staffOnly || isDcmStaff(user)});
32
35
 
33
36
  /** Dashboard Pro: the Dashboard entry (`#/`) is Dashboards, wearing the name this person (or the site) gave it. */
34
37
  const dashboardsLabel = async (request) => (isDashboardPro()
package/server/server.js CHANGED
@@ -33,6 +33,7 @@ import * as cache from './services/cache/index.js';
33
33
  import {parseTrustProxy} from './services/network.js';
34
34
  import {requestSerializer} from './services/request-log.js';
35
35
  import {rateLimitOptions} from './services/rate-limit.js';
36
+ import {registerAdminAssets} from './services/adminAssets.js';
36
37
  import {connectSources, frameSources, imageSources} from './services/embedCsp.js';
37
38
  import {cspSources as gaSources} from './services/googleAnalytics.js';
38
39
 
@@ -164,6 +165,7 @@ if (publicCorsPrefixes.length) {
164
165
  await app.register(multipart, { limits: { fileSize: serverConfig.uploads.maxFileSize } });
165
166
  await app.register(rateLimit, rateLimitOptions());
166
167
 
168
+ registerAdminAssets(app, {root: ROOT, dommaDist: DOMMA_DIST});
167
169
  // Text assets (scripts, stylesheets, SVG, JSON) from memory, compressed once
168
170
  // per encoding - services/assetCache.js. Its onSend hook must be added BEFORE
169
171
  // compress registers its own, so compress finds content-encoding set and
@@ -206,19 +208,14 @@ await app.register(staticPlugin, {
206
208
  decorateReply: false
207
209
  });
208
210
 
209
- // Serve admin panel assets - no-store on JS/CSS so ESM module URLs are never
210
- // cached. `no-cache` revalidates but browsers may keep the parsed ESM module
211
- // keyed by URL across reloads, which means a hard refresh can still load the
212
- // stale code path. `no-store` skips the cache entirely. Other admin assets
213
- // (images, html) keep no-cache. Local-only impact - admin is not public.
211
+ // Versioned entry assets are immutable; dependencies revalidate with ETags.
214
212
  await app.register(staticPlugin, {
215
213
  root: path.join(ROOT, 'admin'),
216
214
  prefix: '/admin/',
217
215
  decorateReply: false,
218
216
  cacheControl: false,
219
- setHeaders: (reply, filePath) => {
220
- const noStore = /\.(?:js|mjs|css)$/i.test(filePath);
221
- reply.header('Cache-Control', noStore ? 'no-store' : 'no-cache');
217
+ setHeaders: (reply) => {
218
+ reply.header('Cache-Control', 'private, no-cache');
222
219
  }
223
220
  });
224
221
 
@@ -0,0 +1,117 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {createHash} from 'node:crypto';
4
+
5
+ const TYPES = {'.js': 'application/javascript', '.mjs': 'application/javascript', '.css': 'text/css'};
6
+ const IMMUTABLE = 'private, max-age=31536000, immutable';
7
+
8
+ /** Fingerprint entry assets; unversioned module dependencies still revalidate. */
9
+ export function registerAdminAssets(app, {root, dommaDist, maxBytes = 16 * 1024 * 1024}) {
10
+ const entries = new Map();
11
+ const inflight = new Map();
12
+ let bytes = 0;
13
+ function resolve(url) {
14
+ let name;
15
+ try { name = decodeURIComponent(new URL(url, 'http://localhost').pathname); } catch { return null; }
16
+ if (name.includes('\\') || name.includes('\0') || name.split('/').some(p => p === '..')) return null;
17
+ if (!TYPES[path.extname(name)]) return null;
18
+ let base, relative;
19
+ if (name.startsWith('/dist/domma/')) { base = dommaDist; relative = name.slice(12); }
20
+ else if (name.startsWith('/admin/')) { base = path.join(root, 'admin'); relative = name.slice(7); }
21
+ else if (name.startsWith('/public/')) { base = path.join(root, 'public'); relative = name.slice(8); }
22
+ else if (/^\/plugins\/[^/]+\/(admin|public)\//.test(name)) { base = path.join(root, 'plugins'); relative = name.slice(9); }
23
+ else return null;
24
+ const file = path.resolve(base, relative);
25
+ return file.startsWith(path.resolve(base) + path.sep) ? file : null;
26
+ }
27
+ async function load(file) {
28
+ const stat = await fs.stat(file);
29
+ if (!stat.isFile()) throw new Error('Not an asset');
30
+ const signature = `${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}`;
31
+ const previous = entries.get(file);
32
+ if (previous?.signature === signature) return previous;
33
+ const body = await fs.readFile(file);
34
+ const entry = {body, signature, hash: createHash('sha256').update(body).digest('hex').slice(0, 20)};
35
+ const stored = entries.get(file);
36
+ if (stored) { entries.delete(file); bytes -= stored.body.length; }
37
+ if (body.length <= maxBytes) {
38
+ while (bytes + body.length > maxBytes && entries.size) {
39
+ const [key, value] = entries.entries().next().value;
40
+ entries.delete(key); bytes -= value.body.length;
41
+ }
42
+ entries.set(file, entry); bytes += body.length;
43
+ }
44
+ return entry;
45
+ }
46
+ function read(file) {
47
+ if (!inflight.has(file)) inflight.set(file, load(file).finally(() => inflight.delete(file)));
48
+ return inflight.get(file);
49
+ }
50
+ async function version(url) {
51
+ if (!url.startsWith('/') || url.startsWith('//')) return url;
52
+ const file = resolve(url);
53
+ if (!file) return url;
54
+ try {
55
+ const entry = await read(file);
56
+ const parsed = new URL(url, 'http://localhost');
57
+ parsed.searchParams.set('_av', entry.hash);
58
+ return parsed.pathname + parsed.search + parsed.hash;
59
+ } catch { return url; }
60
+ }
61
+ async function index() {
62
+ let html = await fs.readFile(path.join(root, 'admin', 'index.html'), 'utf8');
63
+ const matches = [...html.matchAll(/(<(?:script|link)\b[^>]*?\b(?:src|href)=)(["'])([^"']+)\2/gi)];
64
+ const replacements = await Promise.all(matches.map(async match => {
65
+ const url = match[3].replaceAll('&amp;', '&');
66
+ return match[1] + match[2] + (await version(url)).replaceAll('&', '&amp;') + match[2];
67
+ }));
68
+ for (let i = matches.length - 1; i >= 0; i--) {
69
+ const match = matches[i];
70
+ html = html.slice(0, match.index) + replacements[i] + html.slice(match.index + match[0].length);
71
+ }
72
+ return html;
73
+ }
74
+ // HTML must be rebuilt even when only a referenced script or stylesheet changed.
75
+ for (const url of ['/admin/', '/admin/index.html']) {
76
+ app.get(url, async (_request, reply) => reply.header('Cache-Control', 'no-store')
77
+ .type('text/html; charset=utf-8').send(await index()));
78
+ }
79
+ app.addHook('onRequest', async (request, reply) => {
80
+ if (!['GET', 'HEAD'].includes(request.method)) return;
81
+ let url;
82
+ try { url = new URL(request.raw.url, 'http://localhost'); } catch { return; }
83
+ const versioned = url.searchParams.has('_av');
84
+ const adminCode = url.pathname.startsWith('/admin/') || /^\/plugins\/[^/]+\/admin\//.test(url.pathname);
85
+ if (!versioned && !adminCode) return;
86
+ const file = resolve(request.raw.url);
87
+ if (!file) return;
88
+ try {
89
+ const entry = await read(file);
90
+ if (!versioned) {
91
+ const etag = `"${entry.hash}"`;
92
+ reply.header('Cache-Control', 'private, no-cache').header('ETag', etag);
93
+ const matches = String(request.headers['if-none-match'] || '').split(',').map(value => value.trim().replace(/^W\//, ''));
94
+ if (matches.includes(etag) || matches.includes('*')) return reply.code(304).send();
95
+ return reply.type(TYPES[path.extname(file)] + '; charset=utf-8').send(entry.body);
96
+ }
97
+ if (url.searchParams.get('_av') === entry.hash) {
98
+ // Serve the exact bytes that were hashed, including during an atomic file replacement.
99
+ return reply.header('Cache-Control', IMMUTABLE).type(TYPES[path.extname(file)] + '; charset=utf-8').send(entry.body);
100
+ }
101
+ } catch { /* missing or replaced asset */ }
102
+ return reply.code(404).header('Cache-Control', 'no-store').send({error: 'Asset version unavailable. Reload the admin.'});
103
+ });
104
+ app.addHook('onSend', async (request, reply, payload) => {
105
+ if (request.raw.url.split('?')[0] !== '/api/plugins/admin-config' || reply.statusCode !== 200 || typeof payload !== 'string') return payload;
106
+ const config = JSON.parse(payload);
107
+ await Promise.all(Object.values(config.views || {}).map(async view => {
108
+ if (typeof view.entry === 'string') view.entry = (await version('/plugins/' + view.entry)).slice('/plugins/'.length);
109
+ }));
110
+ await Promise.all((config.css || []).map(async css => {
111
+ if (typeof css.href === 'string') css.href = await version(css.href);
112
+ }));
113
+ reply.removeHeader('content-length');
114
+ return JSON.stringify(config);
115
+ });
116
+ return {version};
117
+ }