domma-cms 0.74.0 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CLAUDE.md +3 -1
  2. package/admin/js/app.js +2 -2
  3. package/admin/js/lib/actions-arrange.js +1 -0
  4. package/admin/js/lib/api-endpoints-arrange.js +1 -0
  5. package/admin/js/lib/blocks-arrange.js +1 -0
  6. package/admin/js/lib/collections-arrange.js +1 -0
  7. package/admin/js/lib/components-arrange.js +1 -0
  8. package/admin/js/lib/context-menus-arrange.js +1 -0
  9. package/admin/js/lib/forms-arrange.js +1 -0
  10. package/admin/js/lib/media-arrange.js +1 -0
  11. package/admin/js/lib/menus-arrange.js +1 -0
  12. package/admin/js/lib/pages-arrange.js +1 -0
  13. package/admin/js/lib/sidebar-renderer.js +3 -3
  14. package/admin/js/lib/theme-settings-arrange.js +1 -0
  15. package/admin/js/lib/views-arrange.js +1 -0
  16. package/admin/js/templates/actions-list.html +73 -14
  17. package/admin/js/templates/api-endpoints.html +63 -10
  18. package/admin/js/templates/blocks.html +76 -15
  19. package/admin/js/templates/collections.html +108 -52
  20. package/admin/js/templates/components.html +82 -15
  21. package/admin/js/templates/context-menus.html +55 -13
  22. package/admin/js/templates/forms.html +67 -14
  23. package/admin/js/templates/media.html +99 -13
  24. package/admin/js/templates/menus.html +57 -11
  25. package/admin/js/templates/pages.html +78 -23
  26. package/admin/js/templates/theme.html +164 -132
  27. package/admin/js/templates/views-list.html +63 -9
  28. package/admin/js/views/actions-list.js +37 -1
  29. package/admin/js/views/api-endpoints.js +11 -7
  30. package/admin/js/views/blocks.js +34 -5
  31. package/admin/js/views/collections.js +36 -1
  32. package/admin/js/views/components.js +27 -5
  33. package/admin/js/views/context-menus.js +10 -5
  34. package/admin/js/views/forms.js +57 -1
  35. package/admin/js/views/media.js +40 -1
  36. package/admin/js/views/menus.js +11 -5
  37. package/admin/js/views/pages.js +28 -8
  38. package/admin/js/views/theme.js +55 -1
  39. package/admin/js/views/views-list.js +11 -1
  40. package/package.json +1 -1
  41. package/server/routes/api/actions.js +64 -2
  42. package/server/routes/api/api-endpoints.js +41 -0
  43. package/server/routes/api/blocks.js +170 -8
  44. package/server/routes/api/collections.js +53 -1
  45. package/server/routes/api/components.js +46 -0
  46. package/server/routes/api/context-menus.js +8 -0
  47. package/server/routes/api/forms.js +123 -10
  48. package/server/routes/api/media.js +56 -1
  49. package/server/routes/api/menus.js +53 -6
  50. package/server/routes/api/pages.js +46 -0
  51. package/server/routes/api/theme.js +55 -0
  52. package/server/routes/api/views.js +27 -3
  53. package/server/server.js +16 -1
  54. package/server/services/contextMenus.js +41 -0
  55. package/server/services/views.js +38 -0
package/CLAUDE.md CHANGED
@@ -201,7 +201,9 @@ changed here or uncommitted. `tests/plugins/free-tier-lock.test.js` fails on a d
201
201
  ships them exactly as locked (no minify, no ?v= stamping), and the updater (`bin/update.js`) never
202
202
  replaces a site's plugin with an OLDER version than it has (`bin/lib/plugin-version.js`).
203
203
 
204
- **Sidebar badges** come from `countUrl` on a sidebar entry (core via `registerSidebarItem`, plugins in
204
+ **Sidebar badges** come from `countUrl` on a sidebar entry (core registers one for every Content and Data
205
+ screen, Projects too - `inventory: true` on those keeps a how-many count out of a closed folder's total, and
206
+ a registered countUrl replaces the core list-fetch count for that url, so it must keep that number) (core via `registerSidebarItem`, plugins in
205
207
  `admin.sidebar`), answering `{count, label, tone, details}`. Only two tones change the look: `danger`
206
208
  (red - late) and `live` (pulsing dot - happening now); `info`/`attention` draw the ordinary badge.
207
209
  `details` - `{title, rows: [{label, value, tone?}], items: [{text, meta?, tone?}], empty}` - fills the
package/admin/js/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import{views as V}from"./views/index.js";import{withPluginChrome as T}from"./lib/plugin-chrome.js";import{api as c,getUser as L,isAuthenticated as d,logout as F}from"./api.js";import{installHttpInterceptor as H}from"./http-interceptor.js";import{installResizableSlideovers as q}from"./lib/slideover-resizable.js";import{installActivityThrobber as K}from"./lib/activity-throbber.js";import{isPathAllowed as z,redirectFor as J}from"./lib/admin-scope.js";$(()=>{H(),K(),q(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:e}=await import("./lib/admin-theme.js");d()?await e(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let l=null,r="/",u=!1;function v(t){const e=C[t]||J(l,t);return e||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"?C[r]||r:null)}const C={};R.use(async(t,e,o)=>{if(t.path==="/login"||t.path==="/reset-password")return o();if(!d()){R.navigate("/login");return}const i=v(t.path);if(i){R.navigate(i);return}o()});async function h(){try{const t=await c.get("/auth/permissions");l=t.adminScope||null,r=t.adminHome||"/";const e=t.permissions||[];return u=z(l,"/settings")&&e.some(o=>o==="settings"||o.startsWith("settings.")),e}catch{return l=null,r="/",u=!1,[]}}document.addEventListener("dm:admin-home-changed",()=>{h()});let D=null,b=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(D){b=!0;return}D=(async()=>{do{b=!1;try{await P(await h())}catch{}}while(b)})().finally(()=>{D=null})});async function P(t){const{renderAdminSidebar:e}=await import("./lib/sidebar-renderer.js");await e({mount:"#admin-sidebar",permissions:t,adminScope:l})}M.subscribe("router:afterChange",({to:t})=>{(t.path==="/login"||t.path==="/reset-password")&&(p&&(clearInterval(p),p=null),l=null,r="/",u=!1)});const x=[{path:"/",view:"dashboard",title:"Dashboard - 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/plugin",view:"tutorialPlugin",title:"Writing a Plugin - 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/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:"/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:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",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:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - 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:e})=>{if(!(t.path==="/login"||t.path==="/reset-password")){if($("#admin-sidebar").show(),$("#admin-topbar").show(),e?.path==="/login"||e?.path==="/reset-password"){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(c)),await N();const o=await h(),i=v(t.path);i&&R.navigate(i),P(o);try{const a=await c.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const g=w(s.data?.title||""),A=w((s.data?.body||"").slice(0,120));E.toast(`${g} - ${A}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}I()}}),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 W=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function _(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 e=t?String(t.path).split("?")[0]:"";if(!t||!_(e))return;const i=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!i||!/\/new$/.test(e))return;let a=0;const n=60,s=1500,g=()=>{a+=n;for(const A of W){const f=document.querySelector(A);if(!f)continue;if(Array.from(f.options||[]).find(O=>O.value===i)){f.value=i,f.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(g,n)};setTimeout(g,n)});const k="cms_card_states",y=S.get(k)||{};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&&y[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 e=$(this).closest(".card");e.toggleClass("card-collapsed");const o=$(this).find("h2, h3").first().text().trim();o&&(y[o]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(k,y))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash==="#/login"||window.location.hash.startsWith("#/reset-password"))return;const e=document.querySelector("#view-container .view-header button.btn-primary");e&&(t.preventDefault(),e.click())});const m={...V},B={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},analytics:{displayName:"Analytics",icon:"chart-bar"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"}};for(const[t,e]of Object.entries(B))m[t]&&(m[t]=T(m[t],{plugin:t,licence:"Built in",...e}));const j=[...x];async function N(){if(d())try{const t=await c.plugins.adminConfig();t.routes?.length&&j.push(...t.routes);for(const[e,o]of Object.entries(t.views||{}))try{const i=await import(`/plugins/${o.entry}`);m[e]=T(i[o.exportName],(t.meta||{})[e])}catch{}for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(C[`/${e}`]=o);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const i=document.createElement("link");i.id=e,i.rel="stylesheet",i.href=o,document.head.appendChild(i)}}catch{}}function I(){const t=L();if(!t||$("#topbar-user-name").length)return;const o={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role;$("#topbar-user").html(`
1
+ import{views as O}from"./views/index.js";import{withPluginChrome as T}from"./lib/plugin-chrome.js";import{api as c,getUser as x,isAuthenticated as d,logout as F}from"./api.js";import{installHttpInterceptor as H}from"./http-interceptor.js";import{installResizableSlideovers as q}from"./lib/slideover-resizable.js";import{installActivityThrobber as K}from"./lib/activity-throbber.js";import{isPathAllowed as z,redirectFor as J}from"./lib/admin-scope.js";$(()=>{H(),K(),q(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:e}=await import("./lib/admin-theme.js");d()?await e(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let l=null,r="/",u=!1;function v(t){const e=C[t]||J(l,t);return e||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"?C[r]||r:null)}const C={};R.use(async(t,e,o)=>{if(t.path==="/login"||t.path==="/reset-password")return o();if(!d()){R.navigate("/login");return}const i=v(t.path);if(i){R.navigate(i);return}o()});async function h(){try{const t=await c.get("/auth/permissions");l=t.adminScope||null,r=t.adminHome||"/";const e=t.permissions||[];return u=z(l,"/settings")&&e.some(o=>o==="settings"||o.startsWith("settings.")),e}catch{return l=null,r="/",u=!1,[]}}document.addEventListener("dm:admin-home-changed",()=>{h()});let D=null,b=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(D){b=!0;return}D=(async()=>{do{b=!1;try{await y(await h())}catch{}}while(b)})().finally(()=>{D=null})});async function y(t){const{renderAdminSidebar:e}=await import("./lib/sidebar-renderer.js");await e({mount:"#admin-sidebar",permissions:t,adminScope:l})}M.subscribe("router:afterChange",({to:t})=>{(t.path==="/login"||t.path==="/reset-password")&&(p&&(clearInterval(p),p=null),l=null,r="/",u=!1)});const L=[{path:"/",view:"dashboard",title:"Dashboard - 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/plugin",view:"tutorialPlugin",title:"Writing a Plugin - 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/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:"/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:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",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:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - 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:e})=>{if(!(t.path==="/login"||t.path==="/reset-password")){if($("#admin-sidebar").show(),$("#admin-topbar").show(),e?.path==="/login"||e?.path==="/reset-password"){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(c)),await k();const o=await h(),i=v(t.path);i&&R.navigate(i),y(o);try{const a=await c.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const g=w(s.data?.title||""),A=w((s.data?.body||"").slice(0,120));E.toast(`${g} - ${A}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}I()}}),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 B=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function W(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 e=t?String(t.path).split("?")[0]:"";if(!t||!W(e))return;const i=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!i||!/\/new$/.test(e))return;let a=0;const n=60,s=1500,g=()=>{a+=n;for(const A of B){const f=document.querySelector(A);if(!f)continue;if(Array.from(f.options||[]).find(V=>V.value===i)){f.value=i,f.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(g,n)};setTimeout(g,n)});const N="cms_card_states",P=S.get(N)||{};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&&P[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 e=$(this).closest(".card");e.toggleClass("card-collapsed");const o=$(this).find("h2, h3").first().text().trim();o&&(P[o]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(N,P))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash==="#/login"||window.location.hash.startsWith("#/reset-password"))return;const e=document.querySelector("#view-container .view-header button.btn-primary");e&&(t.preventDefault(),e.click())});const m={...O},_={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},analytics:{displayName:"Analytics",icon:"chart-bar"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"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"}};for(const[t,e]of Object.entries(_))m[t]&&(m[t]=T(m[t],{plugin:t,licence:"Built in",...e}));const j=[...L];async function k(){if(d())try{const t=await c.plugins.adminConfig();t.routes?.length&&j.push(...t.routes);for(const[e,o]of Object.entries(t.views||{}))try{const i=await import(`/plugins/${o.entry}`);m[e]=T(i[o.exportName],(t.meta||{})[e])}catch{}for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(C[`/${e}`]=o);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const i=document.createElement("link");i.id=e,i.rel="stylesheet",i.href=o,document.head.appendChild(i)}}catch{}}function I(){const t=x();if(!t||$("#topbar-user-name").length)return;const o={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role;$("#topbar-user").html(`
2
2
  <span id="topbar-user-name" class="topbar-user-name">${w(t.name)}</span>
3
3
  <span class="topbar-role-badge topbar-role-badge--${w(t.role)}">${o}</span>
4
4
  `),$("#topbar-actions").html(`
@@ -18,4 +18,4 @@ import{views as V}from"./views/index.js";import{withPluginChrome as T}from"./lib
18
18
  <span data-icon="log-out"></span>
19
19
  <span>Sign out</span>
20
20
  </a>
21
- `);const i=$("#topbar-bell").get(0);i&&(i.hidden=!0);const a=$("#topbar-settings").get(0);a&&(a.hidden=!u),$("#topbar-logout-btn").on("click",n=>{n.preventDefault(),F()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),U(),p||(p=setInterval(U,6e4))}function w(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let p=null;async function U(){if(d())try{const{count:t}=await c.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const e=$("#topbar-bell-badge");t>0?e.text(t>99?"99+":String(t)).removeAttr("hidden"):e.attr("hidden","").text("")}catch{}}(async()=>{if(!d()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash="#/login";else if(await N(),L()){const a=await h(),n=(window.location.hash||"#/").slice(1)||"/",s=v(n);s&&(window.location.hash=`#${s}`),await P(a),I()}R.init({container:"#view-container",routes:j,views:m,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}});const t=R._extractParams.bind(R);R._extractParams=function(i,a){const n=a.indexOf("?");if(n!==-1&&(a=a.slice(0,n)),i.endsWith("/*")){const s=i.slice(0,-2);return a.startsWith(s+"/")?{}:null}return t(i,a)};const e=(window.location.hash||"#/").slice(1)||"/";j.filter(i=>i.path.endsWith("/*")).some(i=>e.startsWith(i.path.slice(0,-2)+"/"))&&R._handleRouteChange()})()});
21
+ `);const i=$("#topbar-bell").get(0);i&&(i.hidden=!0);const a=$("#topbar-settings").get(0);a&&(a.hidden=!u),$("#topbar-logout-btn").on("click",n=>{n.preventDefault(),F()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),U(),p||(p=setInterval(U,6e4))}function w(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let p=null;async function U(){if(d())try{const{count:t}=await c.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const e=$("#topbar-bell-badge");t>0?e.text(t>99?"99+":String(t)).removeAttr("hidden"):e.attr("hidden","").text("")}catch{}}(async()=>{if(!d()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash="#/login";else if(await k(),x()){const a=await h(),n=(window.location.hash||"#/").slice(1)||"/",s=v(n);s&&(window.location.hash=`#${s}`),await y(a),I()}R.init({container:"#view-container",routes:j,views:m,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}});const t=R._extractParams.bind(R);R._extractParams=function(i,a){const n=a.indexOf("?");if(n!==-1&&(a=a.slice(0,n)),i.endsWith("/*")){const s=i.slice(0,-2);return a.startsWith(s+"/")?{}:null}return t(i,a)};const e=(window.location.hash||"#/").slice(1)||"/";j.filter(i=>i.path.endsWith("/*")).some(i=>e.startsWith(i.path.slice(0,-2)+"/"))&&R._handleRouteChange()})()});
@@ -0,0 +1 @@
1
+ export function slugify(t){return String(t??"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}export function isMongoMissing(t){if(!t)return!1;const e=String(t.message||t);return t.status===503||/MongoDB|pro mode|connection/i.test(e)}const g=(t,e,n=`${e}s`)=>`${t} ${t===1?e:n}`;export function transitionText(t){if(!t||!t.field)return"";const e=(Array.isArray(t.from)?t.from:[t.from]).filter(n=>n!=null&&n!=="");return`${t.field}: ${e.length?e.join(" / "):"any"} \u2192 ${t.to??"?"}`}export function actionRow(t){const e=Array.isArray(t.steps)?t.steps.length:0,n=Array.isArray(t.access?.roles)?t.access.roles:[],r=[...new Set((t.steps||[]).map(o=>o?.type).filter(Boolean))];return{slug:t.slug,title:t.title||t.slug,meta:[t.slug,t.description].filter(Boolean).join(" \xB7 "),collection:t.collection||"",trigger:t.trigger?.label||t.trigger?.type||"manual",steps:g(e,"step"),stepsTitle:r.length?r.join(", "):"No steps yet",transition:transitionText(t.transition),roles:n.length?n.join(", "):"admins",project:t.meta?.project||"",noSteps:e===0}}export function collectionsOf(t){return[...new Set((t||[]).map(e=>e.collection).filter(Boolean))].sort()}export function arrangeActions(t,{query:e="",collection:n=""}={}){const r=String(e).trim().toLowerCase();return[...t||[]].filter(o=>!n||o.collection===n).filter(o=>!r||[o.title,o.slug,o.description,o.collection,o.trigger?.label].some(i=>String(i||"").toLowerCase().includes(r))).sort((o,i)=>String(o.title||o.slug).localeCompare(String(i.title||i.slug)))}export function duplicateAction(t,e,n=[]){const r=String(e?.title??"").trim(),o=slugify(e?.slug||r),i={};r||(i.title="The copy needs a title."),o?n.includes(o)&&(i.slug=`"${o}" is already an action.`):i.slug="Needs at least one letter or number.";const{id:u,slug:p,title:a,meta:s,...l}=t||{},c={...l,title:r,slug:o,...s?.project&&{meta:{project:s.project}}};return{errors:i,payload:c}}export function projectNotice(t,e,n){const r=t-e;return!n||r<1?"":`Showing only actions in ${n}. ${r} other action${r===1?" is":"s are"} filed elsewhere.`}
@@ -0,0 +1 @@
1
+ export const FILTERS=["all","enabled","disabled","public"];export function endpointPath(t){const n=String(t?.path||"");return`/api/x/${t?.project||""}${n.startsWith("/")?n:`/${n}`}`}export function endpointUrl(t,n=""){return`${String(n).replace(/\/+$/,"")}${endpointPath(t)}`}export function authLabel(t){return t==="public"?"Public":t==="token"?"API token":`Role: ${t||"unknown"}`}export function pathParams(t){return[...String(t||"").matchAll(/:([A-Za-z0-9_]+)/g)].map(n=>n[1])}export function projectsOf(t){return[...new Set((t||[]).map(n=>n.project).filter(Boolean))].sort()}export function arrangeEndpoints(t,{filter:n="all",project:o="",query:r=""}={}){const a=String(r).trim().toLowerCase(),i={all:()=>!0,enabled:e=>e.enabled!==!1,disabled:e=>e.enabled===!1,public:e=>e.auth==="public"}[n]||(()=>!0);return[...t||[]].filter(i).filter(e=>!o||e.project===o).filter(e=>!a||[e.name,e.path,e.project,e.collection,endpointPath(e)].some(l=>String(l||"").toLowerCase().includes(a))).sort((e,l)=>String(e.project).localeCompare(String(l.project))||String(e.path).localeCompare(String(l.path)))}export function countEndpoints(t){const n=t||[],o=n.filter(r=>r.enabled===!1).length;return{all:n.length,enabled:n.length-o,disabled:o,public:n.filter(r=>r.auth==="public").length}}export function endpointRow(t){const n=pathParams(t.path),o=t.enabled===!1;return{id:t.id,name:t.name||t.path||"(unnamed)",url:endpointPath(t),meta:[`from ${t.collection||"?"}`,t.mode==="single"?"one entry":`list, up to ${t.limit??50}`,n.length?`params: ${n.join(", ")}`:""].filter(Boolean).join(" \xB7 "),project:t.project||"",auth:authLabel(t.auth),authCls:t.auth==="public"?"is-warn":t.auth==="token"?"is-info":"",off:o,rowCls:o?"is-off":""}}
@@ -0,0 +1 @@
1
+ export const NAME_RE=/^[a-z0-9][a-z0-9-]*$/,FILTERS=["all","used","unused"];const i=(e,n,t=`${n}s`)=>`${e} ${e===1?n:t}`;export function formatSize(e){const n=Number(e)||0;return n<1024?`${n} B`:n<1024*1024?`${(n/1024).toFixed(n<10*1024?1:0)} KB`:`${(n/1024/1024).toFixed(1)} MB`}export function usageCount(e){return e?.usage?(e.usage.pages?.length||0)+(e.usage.views?.length||0):null}export function usageText(e){const n=usageCount(e);if(n===null)return"";if(!n)return"Not used on any page or view";const t=e.usage.pages?.length||0,r=e.usage.views?.length||0;return`Used on ${[t&&i(t,"page"),r&&i(r,"view")].filter(Boolean).join(" and ")}`}export function shortcodeFor(e){return e?.example||`[block template="${e?.name??""}" /]`}export function arrangeBlocks(e,{filter:n="all",query:t=""}={}){const r=String(t).trim().toLowerCase();return[...e||[]].filter(o=>n==="used"?usageCount(o)>0:n==="unused"?usageCount(o)===0:!0).filter(o=>!r||[o.name,o.description,o.meta?.project].some(s=>String(s||"").toLowerCase().includes(r))).sort((o,s)=>String(o.name).localeCompare(String(s.name)))}export function blockRow(e,n=t=>String(t||"").slice(0,10)){const t=usageCount(e);return{name:e.name,description:e.description||"",meta:[`${e.name}.html`,formatSize(e.size),e.updatedAt?`changed ${n(e.updatedAt)}`:"",usageText(e)].filter(Boolean).join(" \xB7 "),hasCss:!!e.hasCss,bundled:!!e.bundled,project:e.meta?.project||"",unused:t===0,uses:t===null?"":String(t)}}export function checkNewName(e,n=[]){const t=String(e??"").trim();return t?NAME_RE.test(t)?n.includes(t)?`"${t}" already exists.`:"":"Lowercase letters, digits and hyphens; starting with a letter or digit.":"A block needs a name."}export function copyName(e,n=[]){const t=`${e}-copy`;if(!n.includes(t))return t;for(let r=2;r<1e3;r++)if(!n.includes(`${t}-${r}`))return`${t}-${r}`;return t}
@@ -0,0 +1 @@
1
+ export const SYSTEM_SLUGS=["roles","user-profiles"],FILTERS=["all","own","builtin","plugin"];const u="core";export function slugify(n){return String(n??"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}export function kindOf(n){return n?.builtIn===!0||n?.preset===!0||SYSTEM_SLUGS.includes(n?.slug)?"builtin":n?.plugin?"plugin":"own"}export const projectOf=n=>n?.meta?.project||u;export function arrangeCollections(n,{filter:e="all",query:t="",project:r=null}={}){const i=String(t).trim().toLowerCase();return[...n||[]].filter(o=>!r||projectOf(o)===r).filter(o=>e==="all"||kindOf(o)===e).filter(o=>!i||[o.title,o.slug,o.description,o.plugin].some(l=>String(l||"").toLowerCase().includes(i))).sort((o,l)=>String(o.title||o.slug).localeCompare(String(l.title||l.slug)))}export function kindCounts(n,{project:e=null}={}){const t=(n||[]).filter(i=>!e||projectOf(i)===e),r={all:t.length,own:0,builtin:0,plugin:0};for(const i of t)r[kindOf(i)]++;return r}const a=(n,e,t=`${e}s`)=>`${n} ${n===1?e:t}`;export function collectionRow(n){const e=kindOf(n),t=Array.isArray(n.fields)?n.fields.length:0,r=Number(n.entryCount)||0,i=n.storage?.adapter==="mongodb",o=n.meta?.project||"";return{slug:n.slug,title:n.title||n.slug,meta:[n.slug,a(t,"field"),o?`project ${o}`:""].filter(Boolean).join(" \xB7 "),entries:r,entriesText:r.toLocaleString("en-GB"),mongo:i,mongoTitle:i?`MongoDB - connection "${n.storage.connection||"default"}"`:"",builtin:e==="builtin",bundled:e!=="builtin"&&n.bundled===!0,plugin:n.plugin||"",pluginTitle:n.plugin?`Managed by the ${n.plugin} plugin`:"",canDelete:e!=="builtin",hasForm:!SYSTEM_SLUGS.includes(n.slug)}}export function checkNewCollection(n,e=[]){const t=String(n??"").trim(),r=slugify(t);return t?r?e.includes(r)?{slug:r,error:`"${r}" is already a collection.`}:{slug:r,error:""}:{slug:r,error:"The title needs at least one letter or number."}:{slug:"",error:""}}let s=0;export function connectionRows(n){return Object.entries(n||{}).filter(([e])=>e!=="_comment").map(([e,t])=>({id:`c${++s}`,name:e,uri:String(t?.uri||""),database:String(t?.database||"")}))}export function newConnectionRow(n){const e=new Set((n||[]).map(r=>r.name));let t=(n||[]).length+1;for(;e.has(`connection${t}`);)t++;return{id:`c${++s}`,name:t===1&&!e.has("default")?"default":`connection${t}`,uri:"",database:""}}export function connectionsFromRows(n){const e=[],t={};for(const r of n||[]){const i=String(r.name??"").trim();if(!i){e.push("Every connection needs a name.");continue}if(t[i]){e.push(`Two connections are called "${i}".`);continue}const o=String(r.uri??"").trim(),l=String(r.database??"").trim();o||e.push(`"${i}" needs a URI.`),l||e.push(`"${i}" needs a database.`),t[i]={type:"mongodb",uri:o,database:l,options:{}}}return{errors:e,config:t}}export function parseConnectionsJson(n){let e;try{e=JSON.parse(String(n??"").trim()||"{}")}catch(t){return{error:`Not valid JSON: ${t.message}`,config:null}}return!e||typeof e!="object"||Array.isArray(e)?{error:"The JSON must be an object of named connections.",config:null}:{error:"",config:e}}export function maskUri(n){return String(n??"").replace(/(\/\/[^:/@]+:)([^@]+)(@)/,"$1\u2022\u2022\u2022$3")}
@@ -0,0 +1 @@
1
+ export const NAME_RE=/^[a-z][a-z0-9-]*$/,FILTERS=["all","mine","builtin","plugin"],SORTS=["name","updated"];export function kindOf(t){return t?.origin==="plugin"?"plugin":t?.bundled?"builtin":"mine"}export const KIND_LABEL={mine:"Yours",builtin:"Built in",plugin:"Plugin"};const s=(t,n,e=`${n}s`)=>`${t} ${t===1?n:e}`;export function formatSize(t){const n=Number(t)||0;return n<1024?`${n} B`:n<1024*1024?`${(n/1024).toFixed(1)} KB`:`${(n/1024/1024).toFixed(1)} MB`}export function ago(t,n=Date.now()){const e=new Date(t??"").getTime();if(!t||!Number.isFinite(e))return"";const o=Math.max(0,Math.round((n-e)/1e3));return o<60?"just now":o<3600?`${Math.round(o/60)} min ago`:o<86400?`${s(Math.round(o/3600),"hour")} ago`:o<86400*14?`${s(Math.round(o/86400),"day")} ago`:new Date(e).toISOString().slice(0,10)}export function propsSummary(t){const n=Object.keys(t&&typeof t=="object"?t:{});if(!n.length)return{count:0,text:"no props"};const e=n.slice(0,4).join(", ");return{count:n.length,text:n.length>4?`${e} +${n.length-4}`:e}}export const usageTag=t=>`<dm-${t}></dm-${t}>`;export function arrangeComponents(t,{filter:n="all",query:e="",sort:o="name"}={}){const u=String(e).trim().toLowerCase();return(t||[]).filter(r=>n==="all"||kindOf(r)===n).filter(r=>!u||[r.name,...Object.keys(r.props||{}),r.meta?.project].some(i=>String(i||"").toLowerCase().includes(u))).sort(o==="updated"?(r,i)=>String(i.updatedAt||"").localeCompare(String(r.updatedAt||""))||r.name.localeCompare(i.name):(r,i)=>r.name.localeCompare(i.name))}export function kindCounts(t){const n={all:0,mine:0,builtin:0,plugin:0};for(const e of t||[])n.all++,n[kindOf(e)]++;return n}export function componentRow(t,n=Date.now()){const e=kindOf(t),{count:o,text:u}=propsSummary(t.props),a=ago(t.updatedAt,n);return{name:t.name,file:`${t.name}.dmc`,tag:`<dm-${t.name}>`,meta:[o?`${s(o,"prop")}: ${u}`:"no props",formatSize(t.size),a&&`updated ${a}`].filter(Boolean).join(" \xB7 "),kind:e,kindLabel:KIND_LABEL[e],kindCls:e==="mine"?"":`is-${e}`,showKind:e!=="mine",project:t.meta?.project||"",locked:e==="plugin"}}export function checkName(t,n=[]){const e=String(t??"").trim();return e?NAME_RE.test(e)?n.includes(e)?`"${e}" is already a component.`:"":"A letter first, then lowercase letters, digits and hyphens.":"A component needs a name."}export function copyName(t,n=[]){const e=`${t}-copy`;if(!n.includes(e))return e;for(let o=2;;o++)if(!n.includes(`${e}-${o}`))return`${e}-${o}`}export function checkBundle(t){return!t||typeof t!="object"||Array.isArray(t)?"Not a component bundle.":!t.name||typeof t.name!="string"?"The bundle has no name.":typeof t.source!="string"?"The bundle has no source.":""}
@@ -0,0 +1 @@
1
+ export const FILTERS=["all","on","off"];const i=(e,t,n=`${t}s`)=>`${e} ${e===1?t:n}`;export function scopeOf(e){const t=e?.binding;if(!t)return{kind:"site",text:"Site-wide",title:"Every page"};const n=[...(t.projects||[]).map(r=>`project:${r}`),...t.pages||[]];return n.length?{kind:"scoped",text:i(n.length,"rule"),title:n.join(", ")}:{kind:"never",text:"Never matches",title:"Its binding lists no projects and no pages, so it never appears"}}export function targetText(e){const t=String(e?.selector||"").trim(),n=String(e?.match||"").trim();return!t&&!n?"no target":n?`${t||"page"} \u2192 ${n}`:t}export function arrangeMenus(e,{filter:t="all",query:n=""}={}){const r=String(n).trim().toLowerCase();return[...e||[]].filter(o=>t==="on"?o.enabled!==!1:t==="off"?o.enabled===!1:!0).filter(o=>!r||[o.slug,o.name,o.description,o.selector,o.match].some(s=>String(s||"").toLowerCase().includes(r))).sort((o,s)=>String(o.name||o.slug).localeCompare(String(s.name||s.slug))||String(o.slug).localeCompare(String(s.slug)))}export function menuRow(e){const t=scopeOf(e),n=e.enabled===!1;return{slug:e.slug,name:e.name||e.slug,meta:[e.slug,targetText(e),i(Number(e.itemsCount)||0,"item")].join(" \xB7 "),description:e.description||"",scopeText:t.text,scopeTitle:t.title,scopeCls:t.kind==="never"?"is-warn":t.kind==="scoped"?"is-info":"",off:n,rowCls:n?"is-off":""}}export function countMenus(e){const t=(e||[]).length,n=(e||[]).filter(r=>r.enabled===!1).length;return{all:t,on:t-n,off:n}}
@@ -0,0 +1 @@
1
+ export const FILTERS=["all","recent","empty"];const u=new Set(["page-break","spacer"]),a=(t,o,i=`${o}s`)=>`${t} ${t===1?o:i}`;export function ago(t,o=Date.now()){const i=new Date(t).getTime();if(!Number.isFinite(i))return"";const e=Math.max(0,Math.round((o-i)/1e3));return e<60?"just now":e<3600?`${Math.round(e/60)} min ago`:e<86400?`${a(Math.round(e/3600),"hour")} ago`:e<86400*14?`${a(Math.round(e/86400),"day")} ago`:new Date(i).toISOString().slice(0,10)}export function formShape(t){const o=Array.isArray(t?.fields)?t.fields:[],i=o.filter(e=>e?.type==="page-break").length;return{questions:o.filter(e=>e&&!u.has(e.type)).length,steps:i?i+1:0,logic:o.some(e=>e&&(e.logic||Array.isArray(e.triggers)&&e.triggers.length))}}export function actionsOn(t){const o=t?.actions||{};return Object.entries({email:"email",collection:"saved",webhook:"webhook",action:"action"}).filter(([e])=>o[e]?.enabled).map(([,e])=>e)}export function formRow(t,o=Date.now()){const{questions:i,steps:e,logic:n}=formShape(t),s=Number(t.submissionCount)||0,r=Number(t.submissionsThisWeek)||0,l=[t.slug,a(i,"question"),e?`${e}-step wizard`:"",n?"conditional logic":"",t.lastSubmissionAt?`last submission ${ago(t.lastSubmissionAt,o)}`:"no submissions yet"].filter(Boolean).join(" \xB7 ");return{slug:t.slug,title:t.title||t.slug,meta:l,total:s,totalText:String(s),week:r,weekText:r?`${r} new this week`:"",plugin:t.plugin||"",project:t.meta?.project||"",wizard:e>0,rowCls:r?"is-recent":""}}export function arrangeForms(t,{filter:o="all",query:i=""}={}){const e=String(i).trim().toLowerCase();return[...t||[]].filter(n=>o==="recent"?Number(n.submissionsThisWeek)>0:o==="empty"?!Number(n.submissionCount):!0).filter(n=>!e||[n.title,n.slug,n.description,n.plugin,n.meta?.project].some(s=>String(s||"").toLowerCase().includes(e))).sort((n,s)=>String(s.lastSubmissionAt||"").localeCompare(String(n.lastSubmissionAt||""))||String(n.title||n.slug).localeCompare(String(s.title||s.slug)))}export function duplicatePayload(t,o){const{collection:i,...e}=t?.actions||{};return{title:String(o||`${t?.title||t?.slug||"Form"} (Copy)`).trim(),description:t?.description||"",fields:t?.fields||[],settings:t?.settings||{},actions:e}}export const shortcodeFor=t=>`[form name="${t}" /]`;
@@ -0,0 +1 @@
1
+ export const KINDS=["all","image","video","audio","document","other"],KIND_LABEL={all:"All",image:"Images",video:"Video",audio:"Audio",document:"Documents",other:"Other"},SORTS=["newest","oldest","name","size"];const c={image:["png","jpg","jpeg","gif","webp","svg","avif","ico","bmp","tif","tiff"],video:["mp4","webm","ogv","mov"],audio:["mp3","ogg","oga","wav","weba","m4a"],document:["pdf","txt","csv","doc","docx","xls","xlsx"]},u=new Set(["png","jpg","jpeg","gif","webp","svg","avif","ico","bmp"]),m=new Set(["jpg","jpeg","png","webp","gif","tiff"]);export const extOf=e=>{const t=/\.([a-z0-9]+)$/i.exec(String(e??""));return t?t[1].toLowerCase():""};export function kindOf(e){const t=extOf(e);for(const[o,n]of Object.entries(c))if(n.includes(t))return o;return"other"}export const isPreviewable=e=>u.has(extOf(e)),isEditable=e=>m.has(extOf(e));export function formatBytes(e){const t=Number(e)||0;if(t<1024)return`${t} B`;const o=["KB","MB","GB","TB"];let n=t/1024,r=0;for(;n>=1024&&r<o.length-1;)n/=1024,r++;return`${n.toFixed(1)} ${o[r]}`}export function ago(e,t=Date.now()){const o=new Date(e).getTime();if(!Number.isFinite(o))return"";const n=Math.max(0,Math.round((t-o)/1e3));if(n<60)return"just now";if(n<3600)return`${Math.round(n/60)} min ago`;if(n<86400){const r=Math.round(n/3600);return`${r} hour${r===1?"":"s"} ago`}if(n<86400*14){const r=Math.round(n/86400);return`${r} day${r===1?"":"s"} ago`}return new Date(o).toISOString().slice(0,10)}export function safeName(e){return String(e??"").split(/[\\/]/).pop().replace(/[^a-zA-Z0-9._-]/g,"_")}export function clashes(e,t){const o=new Set(t);return[...new Set(e.map(safeName))].filter(n=>o.has(n))}export function altFromName(e){return String(e??"").replace(/\.[^.]+$/,"").replace(/[-_]+/g," ").replace(/\s+/g," ").trim()}export function markdownFor(e){return isPreviewable(e.name)?`![${altFromName(e.name)}](${e.url})`:`[${e.name}](${e.url})`}export function kindCounts(e){const t=Object.fromEntries(KINDS.map(o=>[o,0]));for(const o of e||[])t.all++,t[kindOf(o.name)]++;return t}export function arrangeMedia(e,{kind:t="all",query:o="",sort:n="newest"}={}){const r=String(o).trim().toLowerCase(),s={newest:(a,i)=>String(i.createdAt).localeCompare(String(a.createdAt)),oldest:(a,i)=>String(a.createdAt).localeCompare(String(i.createdAt)),name:(a,i)=>a.name.localeCompare(i.name,void 0,{numeric:!0,sensitivity:"base"}),size:(a,i)=>(i.size||0)-(a.size||0)}[n]||(()=>0);return[...e||[]].filter(a=>t==="all"||kindOf(a.name)===t).filter(a=>!r||a.name.toLowerCase().includes(r)).sort((a,i)=>s(a,i)||a.name.localeCompare(i.name))}export function mediaTile(e,{now:t=Date.now(),stamp:o=""}={}){const n=kindOf(e.name),r=isPreviewable(e.name);return{name:e.name,url:e.url,thumb:r?`${e.url}${o?`?t=${encodeURIComponent(o)}`:""}`:null,preview:r,icon:{video:"video",audio:"music",document:"file-text"}[n]||(n==="image"?"image":"paperclip"),kind:n,kindLabel:(extOf(e.name)||n).toUpperCase(),meta:[formatBytes(e.size),ago(e.createdAt,t)].filter(Boolean).join(" \xB7 ")}}export function normalisePrefs(e){const t=e&&typeof e=="object"?e:{};return{kind:KINDS.includes(t.kind)?t.kind:"all",sort:SORTS.includes(t.sort)?t.sort:"newest"}}
@@ -0,0 +1 @@
1
+ export const FILTERS=["all","placed","unplaced"];export function slotsByMenu(t){const o={};for(const[l,n]of Object.entries(t||{}))n&&(o[n]??(o[n]=[])).push(l);return o}export function slotLabel(t,o=[]){return o.find(l=>l.slot===t)?.label||t}export function bindingScope(t){if(!t?.slot)return"";const o=[...(t.projects||[]).map(l=>`project ${l}`),...t.pages||[]];return o.length?o.join(", "):"nothing - this binding never matches"}export const isPlaced=(t,o)=>!!((o[t.slug]||[]).length||t.binding?.slot);export function menuRow(t,{bySlug:o={},registry:l=[]}={}){const n=o[t.slug]||[],r=Number(t.itemsCount??(Array.isArray(t.items)?t.items.length:0))||0,i=t.meta?.project||"",e=t.binding?.slot?t.binding:null;return{slug:t.slug,name:t.name||t.slug,meta:[t.slug,`${r} item${r===1?"":"s"}`,i?`project ${i}`:"",t.description||""].filter(Boolean).join(" \xB7 "),slotsText:n.map(s=>slotLabel(s,l)).join(" \xB7 "),hasSlots:n.length>0,bindingText:e?`Bound: ${slotLabel(e.slot,l)}`:"",bindingTitle:e?`Takes over ${slotLabel(e.slot,l)} on: ${bindingScope(e)}`:"",bindingDead:!!(e&&!(e.projects||[]).length&&!(e.pages||[]).length),bundled:!!t.meta?.bundled,placed:n.length>0||!!e,canDelete:n.length===0,deleteWhy:n.length?`In ${n.map(s=>slotLabel(s,l)).join(", ")} - take it out of the location first`:""}}export function arrangeMenus(t,{bySlug:o={},registry:l=[],filter:n="all",query:r=""}={}){const i=String(r).trim().toLowerCase();return[...t||[]].filter(e=>n==="placed"?isPlaced(e,o):n==="unplaced"?!isPlaced(e,o):!0).filter(e=>!i||[e.slug,e.name,e.description,...(o[e.slug]||[]).map(s=>slotLabel(s,l))].some(s=>String(s||"").toLowerCase().includes(i))).sort((e,s)=>String(e.name||e.slug).localeCompare(String(s.name||s.slug)))}export const menuShortcode=t=>`[menu slug="${t}" /]`;
@@ -0,0 +1 @@
1
+ export const STATUSES=["all","published","draft"],SORTS=["order","updated","title","url"],MODES=["list","tree"],DEFAULT_SETTINGS={status:"all",sort:"order",mode:"list"};const l=(t,e,r=`${e}s`)=>`${t} ${t===1?e:r}`;export function normaliseSettings(t){const e=t&&typeof t=="object"?t:{};return{status:STATUSES.includes(e.status)?e.status:DEFAULT_SETTINGS.status,sort:SORTS.includes(e.sort)?e.sort:DEFAULT_SETTINGS.sort,mode:MODES.includes(e.mode)?e.mode:DEFAULT_SETTINGS.mode}}export function ago(t,e=Date.now()){const r=new Date(t).getTime();if(!Number.isFinite(r))return"";const n=Math.max(0,Math.round((e-r)/1e3));return n<60?"just now":n<3600?`${Math.round(n/60)} min ago`:n<86400?`${l(Math.round(n/3600),"hour")} ago`:n<86400*14?`${l(Math.round(n/86400),"day")} ago`:new Date(r).toISOString().slice(0,10)}export function visibilityLabel(t){const e=String(t??"public").trim();return!e||e==="public"?"":e==="private"?"Private":e.startsWith("=")?`Only ${e.slice(1)}`:`${e}+`}const a=t=>`${t.title||""} ${t.urlPath||""} ${(t.tags||[]).join(" ")} ${t.layout||""}`.toLowerCase();export function arrangePages(t,{status:e="all",query:r="",sort:n="order"}={}){const s=String(r).trim().toLowerCase(),i=(t||[]).filter(o=>e==="all"||(e==="draft"?o.status!=="published":o.status==="published")).filter(o=>!s||a(o).includes(s));return n==="updated"?[...i].sort((o,u)=>String(u.updatedAt||"").localeCompare(String(o.updatedAt||""))):n==="title"?[...i].sort((o,u)=>String(o.title||"").localeCompare(String(u.title||""))):n==="url"?[...i].sort((o,u)=>String(o.urlPath).localeCompare(String(u.urlPath))):i}export function statusCounts(t){const e=(t||[]).length,r=(t||[]).filter(n=>n.status==="published").length;return{all:e,published:r,draft:e-r}}export function pageRow(t,e=Date.now()){const r=t.status!=="published",n=Number.isFinite(t.versionCount)?t.versionCount:0,s=t.resolvedProject&&t.resolvedProject!=="core"?t.resolvedProject:"";return{urlPath:t.urlPath,title:t.title||t.urlPath,statusText:r?"Draft":"Published",statusCls:r?"is-warn":"is-ok",visibility:visibilityLabel(t.visibility),project:s,plugin:t.plugin?String(t.plugin):"",bundled:t.bundled===!0,tagsText:(t.tags||[]).filter(Boolean).map(i=>`#${i}`).join(" "),meta:[t.urlPath,t.layout||"default",l(n,"revision"),t.updatedAt?`updated ${ago(t.updatedAt,e)}`:""].filter(Boolean).join(" \xB7 "),revisions:n,rowCls:r?"is-draft":""}}export function treeRows(t){const e=new Set((t||[]).map(r=>r.urlPath));return(t||[]).map(r=>{const n=String(r.urlPath).split("/").filter(Boolean);let s=null;for(let i=n.length-1;i>0&&!s;i--){const o="/"+n.slice(0,i).join("/");e.has(o)&&(s=o)}return{id:r.urlPath,parent_id:s,name:r.title||r.urlPath,icon:r.status==="published"?"check-circle":"file-text"}})}export function checkPrune(t,e){const r=String(t??"").trim();if(!/^\d+$/.test(r))return{error:"A whole number, 0 or more.",keep:0,deletes:0};const n=Number.parseInt(r,10);return{error:"",keep:n,deletes:Math.max(0,(Number(e)||0)-n)}}
@@ -1,4 +1,4 @@
1
- import{api as p}from"../api.js";import{isPathAllowed as ee}from"./admin-scope.js";import{colourToCss as X,applyFloat as te,applyHighlight as ne,itemHighlightAttrs as se}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{placeTools as ie,ensureLockedTools as oe,managePluginsItem as ae,stripItemByUrl as re,insertFoldersBeforeSystem as le,pruneEmptyToolsFolder as ce,MANAGE_PLUGINS_URL as de}from"./sidebar-grouping.js";function h(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const ue=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];function _(t,s){return s?!t||!t.length?!1:t.includes(s)?!0:["read","create","update","delete"].some(n=>t.includes(`${s}.${n}`)):!0}function K(t,s,n=null){const e=[];for(const i of t){if(i.hidden||i.permission&&!_(s,i.permission))continue;const r=typeof i.url=="string"?i.url:"";if(n&&r.startsWith("#/")&&!ee(n,r.slice(1)))continue;const l=Array.isArray(i.items)&&i.items.length>0,o=l?K(i.items,s,n):[];l&&!o.length&&!r||e.push({...i,items:o})}return e}async function me(t){if(!_(t,"projects"))return[];let s=[];try{s=await p.projects.listWithCounts()}catch{return[]}if(!Array.isArray(s))return[];const n=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return s.map(e=>{const r="#/projects/"+encodeURIComponent(e.slug),l=e.counts||{},o=[{text:"Overview",url:r,icon:e.icon||"folder"}];for(const d of n){const u=Number(l[d.key])||0;u<=0||o.push({text:`${d.text} (${u})`,url:r+d.path,icon:d.icon})}o.push({text:"Settings",url:r+"/settings",icon:"settings"});const c=e.enabled===!1;return{text:e.name||e.slug,icon:e.icon||"folder",items:o,...c&&{colour:"neutral",badge:{text:"off"}}}})}function fe(t,s){for(const n of t)if(!n.hidden&&(n.key==="projects"||!n.key&&(n.text||"").toLowerCase()==="projects")){n.items=Array.isArray(n.items)?n.items.slice():[],n.items.push(...s);return}}function V(t,s){const n=J.includes(s.tone)?s.tone:"info",e=s.label||String(s.count);s.details?P.set(t,s.details):P.delete(t);const i=A.some(r=>r.url===t)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${n}" data-count-for="${h(t)}"${i} data-badge-label="${h(e)}" aria-label="${h(e)}">${h(String(s.count))}</span>`}function G(t){for(const s of t.querySelectorAll("details")){const n=s.querySelector(":scope > summary");if(!n)continue;let e=0,i=!1;const r=[];for(const u of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const v=Number(u.textContent)||0;v&&(e+=v,i||(i=u.classList.contains("sidebar-badge--danger")),r.push({text:u.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:u.dataset.badgeLabel,tone:u.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const l=`folder:${s.dataset.stateKey||n.textContent.trim()}`;let o=n.querySelector(":scope > .sidebar-badge--sum");if(!e){o?.remove(),P.delete(l);continue}o||(o=document.createElement("span"),n.querySelector(".sidebar-text")?.after(o));const c=n.querySelector(".sidebar-text")?.textContent.trim()||"This folder",d=r.map(u=>`${u.text}: ${u.meta}`).join(" \xB7 ");o.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${i?"danger":"info"}`,o.dataset.countFor=l,o.dataset.badgeLabel=d,o.setAttribute("aria-label",d),o.textContent=String(e),P.set(l,{title:c,items:r})}}const P=new Map;function pe(t,s){if(!t.url||!s)return"";const n=s[t.url];return!n||!(n.count>0)?"":V(t.url,n)}const J=["info","attention","danger","live"],he={"#/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 T(t,{fallbackLabel:s="",tone:n="info"}={}){const e=Number(typeof t=="object"&&t!==null?t.count:t);if(!Number.isFinite(e)||e<0)return null;const i=typeof t?.label=="string"&&t.label||(s?`${e} - ${s}`:String(e)),r={count:Math.round(e),label:i,tone:J.includes(t?.tone)?t.tone:n};return t?.details&&typeof t.details=="object"&&(r.details=t.details),r}function ge(t){const s=(t.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),n=(t.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${n?`project-${decodeURIComponent(n[1])}.`:""}${s}`}function Y(t,s){if(t&&t.type==="separator")return'<hr class="sidebar-divider">';if(t&&t.type==="spacer"){const y=["xs","sm","md","lg","xl","flex"].includes(t.size)?t.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${h(y)}" aria-hidden="true"></div>`}const n=t.icon?`<span data-icon="${h(t.icon)}"></span>`:"",e=h(t.text||""),i=t.url?h(t.url):"",r=Array.isArray(t.items)&&t.items.length>0,l=t.colour?X(t.colour):"",o=se(t),c=[o.hover&&` data-menu-item-hover="${h(o.hover)}"`,o.active&&` data-menu-item-active="${h(o.active)}"`].filter(Boolean).join(""),d=[l&&`color:${h(l)}`,o.accent&&`--dm-menu-highlight:${h(o.accent)}`].filter(Boolean).join(";"),u=d?` style="${d}"`:"";let v="";if(t.badge&&t.badge.text!=null&&t.badge.text!==""){const y=t.badge.variant?X(t.badge.variant):"";v=`<span class="dm-menu-badge"${y?` style="background:${h(y)};color:var(--dm-text-inverse,#fff)"`:""}>${h(String(t.badge.text))}</span>`}if(r){const y=ge(t),L=S.get(y)!==!1,E=t.items.map(w=>Y(w,s)).join("");return`<details data-state-key="${h(y)}"${L?" open":""}>
2
- <summary${u}>${n} <span class="sidebar-text">${e}</span>${v}</summary>
1
+ import{api as p}from"../api.js";import{isPathAllowed as ee}from"./admin-scope.js";import{colourToCss as X,applyFloat as te,applyHighlight as ne,itemHighlightAttrs as se}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{placeTools as ie,ensureLockedTools as oe,managePluginsItem as ae,stripItemByUrl as re,insertFoldersBeforeSystem as le,pruneEmptyToolsFolder as ce,MANAGE_PLUGINS_URL as de}from"./sidebar-grouping.js";function h(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const ue=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];function z(t,s){return s?!t||!t.length?!1:t.includes(s)?!0:["read","create","update","delete"].some(n=>t.includes(`${s}.${n}`)):!0}function K(t,s,n=null){const e=[];for(const i of t){if(i.hidden||i.permission&&!z(s,i.permission))continue;const o=typeof i.url=="string"?i.url:"";if(n&&o.startsWith("#/")&&!ee(n,o.slice(1)))continue;const l=Array.isArray(i.items)&&i.items.length>0,a=l?K(i.items,s,n):[];l&&!a.length&&!o||e.push({...i,items:a})}return e}async function me(t){if(!z(t,"projects"))return[];let s=[];try{s=await p.projects.listWithCounts()}catch{return[]}if(!Array.isArray(s))return[];const n=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return s.map(e=>{const o="#/projects/"+encodeURIComponent(e.slug),l=e.counts||{},a=[{text:"Overview",url:o,icon:e.icon||"folder"}];for(const u of n){const m=Number(l[u.key])||0;m<=0||a.push({text:`${u.text} (${m})`,url:o+u.path,icon:u.icon})}a.push({text:"Settings",url:o+"/settings",icon:"settings"});const c=e.enabled===!1;return{text:e.name||e.slug,icon:e.icon||"folder",items:a,...c&&{colour:"neutral",badge:{text:"off"}}}})}function fe(t,s){for(const n of t)if(!n.hidden&&(n.key==="projects"||!n.key&&(n.text||"").toLowerCase()==="projects")){n.items=Array.isArray(n.items)?n.items.slice():[],n.items.push(...s);return}}function V(t,s){const n=J.includes(s.tone)?s.tone:"info",e=s.label||String(s.count);s.details?N.set(t,s.details):N.delete(t);const i=C.some(o=>o.url===t&&!o.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${n}" data-count-for="${h(t)}"${i} data-badge-label="${h(e)}" aria-label="${h(e)}">${h(String(s.count))}</span>`}function G(t){for(const s of t.querySelectorAll("details")){const n=s.querySelector(":scope > summary");if(!n)continue;let e=0,i=!1;const o=[];for(const m of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const v=Number(m.textContent)||0;v&&(e+=v,i||(i=m.classList.contains("sidebar-badge--danger")),o.push({text:m.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:m.dataset.badgeLabel,tone:m.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const l=`folder:${s.dataset.stateKey||n.textContent.trim()}`;let a=n.querySelector(":scope > .sidebar-badge--sum");if(!e){a?.remove(),N.delete(l);continue}a||(a=document.createElement("span"),n.querySelector(".sidebar-text")?.after(a));const c=n.querySelector(".sidebar-text")?.textContent.trim()||"This folder",u=o.map(m=>`${m.text}: ${m.meta}`).join(" \xB7 ");a.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${i?"danger":"info"}`,a.dataset.countFor=l,a.dataset.badgeLabel=u,a.setAttribute("aria-label",u),a.textContent=String(e),N.set(l,{title:c,items:o})}}const N=new Map;function pe(t,s){if(!t.url||!s)return"";const n=s[t.url];return!n||!(n.count>0)?"":V(t.url,n)}const J=["info","attention","danger","live"],he={"#/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 U(t,{fallbackLabel:s="",tone:n="info"}={}){const e=Number(typeof t=="object"&&t!==null?t.count:t);if(!Number.isFinite(e)||e<0)return null;const i=typeof t?.label=="string"&&t.label||(s?`${e} - ${s}`:String(e)),o={count:Math.round(e),label:i,tone:J.includes(t?.tone)?t.tone:n};return t?.details&&typeof t.details=="object"&&(o.details=t.details),o}function ge(t){const s=(t.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),n=(t.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${n?`project-${decodeURIComponent(n[1])}.`:""}${s}`}function Y(t,s){if(t&&t.type==="separator")return'<hr class="sidebar-divider">';if(t&&t.type==="spacer"){const y=["xs","sm","md","lg","xl","flex"].includes(t.size)?t.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${h(y)}" aria-hidden="true"></div>`}const n=t.icon?`<span data-icon="${h(t.icon)}"></span>`:"",e=h(t.text||""),i=t.url?h(t.url):"",o=Array.isArray(t.items)&&t.items.length>0,l=t.colour?X(t.colour):"",a=se(t),c=[a.hover&&` data-menu-item-hover="${h(a.hover)}"`,a.active&&` data-menu-item-active="${h(a.active)}"`].filter(Boolean).join(""),u=[l&&`color:${h(l)}`,a.accent&&`--dm-menu-highlight:${h(a.accent)}`].filter(Boolean).join(";"),m=u?` style="${u}"`:"";let v="";if(t.badge&&t.badge.text!=null&&t.badge.text!==""){const y=t.badge.variant?X(t.badge.variant):"";v=`<span class="dm-menu-badge"${y?` style="background:${h(y)};color:var(--dm-text-inverse,#fff)"`:""}>${h(String(t.badge.text))}</span>`}if(o){const y=ge(t),k=S.get(y)!==!1,E=t.items.map(w=>Y(w,s)).join("");return`<details data-state-key="${h(y)}"${k?" open":""}>
2
+ <summary${m}>${n} <span class="sidebar-text">${e}</span>${v}</summary>
3
3
  <div class="sidebar-children">${E}</div>
4
- </details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${u}${c}>${n} <span class="sidebar-text">${e}</span>${pe(t,s)}${v}</a>`}async function be(t=[],s=[]){const n=g=>g.then(f=>Array.isArray(f)?f.length:Array.isArray(f?.entries)?f.entries.length:0).catch(()=>0),e=(g,f)=>_(s,g)?f():Promise.resolve(0),[i,r,l,o,c,d,u,v,y,L,E,w,F,B]=await Promise.all([e("pages",()=>n(p.pages.list())),e("media",()=>n(p.media.list())),e("menus",()=>n(p.menus.list())),e("context-menus",()=>n(p.contextMenus.list())),e("theme",()=>p.theme.get().then(g=>(g?.overrides||[]).length).catch(()=>0)),e("collections",()=>n(p.collections.list())),e("forms",()=>n(p.forms.list())),e("views",()=>n(p.views.list())),e("actions",()=>n(p.actions.list())),e("blocks",()=>n(p.blocks.list())),e("components",()=>n(p.components.list())),e("users",()=>n(p.users.list())),e("plugins",()=>n(p.plugins.list())),e("notifications",()=>p.system?.notifications?.unreadCount?.().then(g=>g?.count??0).catch(()=>0)??Promise.resolve(0))]);A=xe(t);const R={};await Promise.all(A.map(async g=>{const f=await Q(g);f&&(R[g.url]=f)}));const q={"#/pages":i,"#/media":r,"#/menus":l,"#/context-menus":o,"#/theme":c,"#/collections":d,"#/forms":u,"#/views":v,"#/actions":y,"#/blocks":L,"#/components":E,"#/users":w,"#/plugins":F,"#/system/notifications":B},j={};for(const[g,f]of Object.entries(q)){const[C,k]=he[g]??["item","items"],x=T(f);x&&(j[g]={...x,label:`${x.count} ${x.count===1?C:k}`})}return{...j,...R}}let A=[];const ye="dm:sidebar-count",ve=6e4;function xe(t=[]){const s=[],n=e=>{if(e){e.url&&typeof e.countUrl=="string"&&e.countUrl&&s.push({url:e.url,countUrl:e.countUrl,text:e.text||"",countLabel:e.countLabel||""});for(const i of[...e.children||[],...e.items||[]])n(i)}};for(const e of t??[])n(e?.item??e);return s}async function Q(t){try{const s=new Date,n=`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(s.getDate()).padStart(2,"0")}`,e=t.countUrl+(t.countUrl.includes("?")?"&":"?")+`today=${n}`;return T(await H.get(e),{fallbackLabel:t.countLabel||t.text})}catch{return null}}function Z(t,s,n){const e=[...t.querySelectorAll("a.sidebar-link")].find(r=>r.dataset.url===s);if(!e)return;const i=e.querySelector(".sidebar-badge[data-count-for]");if(!n||!(n.count>0))i?.remove();else{const r=document.createRange();r.selectNodeContents(e);const l=r.createContextualFragment(V(s,n)).firstElementChild;i?i.replaceWith(l):e.querySelector(".sidebar-text")?.after(l)}I.set(s,n),G(t)}const I=new Map;async function U(t,s=null){const n=s?A.filter(e=>e.url===s):A;await Promise.all(n.map(async e=>{const i=await Q(e),r=I.get(e.url);JSON.stringify(r??null)!==JSON.stringify(i)&&Z(t,e.url,i)}))}function $e(t){t.dataset.liveCounts||(t.dataset.liveCounts="1",window.addEventListener(ye,s=>{const n=s.detail||{};if(n.url)if(n.count!==void 0){const e=A.find(i=>i.url===n.url);Z(t,n.url,T(n,{fallbackLabel:e?.countLabel||e?.text||""}))}else U(t,n.url)}),setInterval(()=>{!document.hidden&&document.body.contains(t)&&U(t)},ve),document.addEventListener("visibilitychange",()=>{document.hidden||U(t)}),Se(t))}function Ce(t,s,n){const e=(l,o,c)=>{const d=document.createElement(l);return o&&(d.className=o),c!=null&&(d.textContent=String(c)),d};if(!n){t.textContent=s;return}t.appendChild(e("strong","dm-badge-pop-title",n.title||s)),(n.rows?.length||!n.items?.length)&&t.appendChild(e("p","dm-badge-pop-sub",s));const i=Array.isArray(n.rows)?n.rows.slice(0,6):[];if(i.length){const l=e("dl","dm-badge-pop-rows");for(const o of i)l.appendChild(e("dt",null,o.label)),l.appendChild(e("dd",o.tone==="danger"?"is-danger":null,o.value));t.appendChild(l)}const r=Array.isArray(n.items)?n.items.slice(0,6):[];if(r.length){const l=e("ul","dm-badge-pop-items");for(const o of r){const c=e("li",o.tone==="danger"?"is-danger":null);c.appendChild(e("span","dm-badge-pop-text",o.text)),o.meta&&c.appendChild(e("span","dm-badge-pop-meta",o.meta)),l.appendChild(c)}t.appendChild(l)}else n.empty&&t.appendChild(e("p","dm-badge-pop-empty",n.empty))}function Se(t){let s=null,n=null;const e=()=>{clearTimeout(n),s?.remove(),s=null},i=r=>{e();const l=r?.dataset.badgeLabel;l&&(n=setTimeout(()=>{s=document.createElement("div"),s.className="dm-help-pop dm-badge-pop",s.setAttribute("role","tooltip"),Ce(s,l,P.get(r.dataset.countFor)),document.body.appendChild(s);const o=r.getBoundingClientRect(),c=s.getBoundingClientRect(),d=Math.min(o.right+10,innerWidth-c.width-8);s.style.left=`${Math.max(8,d)}px`,s.style.top=`${Math.max(8,Math.min(o.top+o.height/2-c.height/2,innerHeight-c.height-8))}px`},150))};t.addEventListener("mouseover",r=>{const l=r.target.closest?.(".sidebar-badge[data-badge-label]");l&&i(l)}),t.addEventListener("mouseout",r=>{r.target.closest?.(".sidebar-badge[data-badge-label]")&&e()}),t.addEventListener("focusin",r=>{const l=r.target.closest?.("a, summary")?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");l?i(l):e()}),t.addEventListener("focusout",e),window.addEventListener("scroll",e,!0),window.addEventListener("hashchange",e)}async function we(){try{const t=await p.settings.get();return t?.adminBrand?.title||t?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:t,permissions:s,adminScope:n=null}){const e=$(t).get(0);if(!e)return;let i,r=null,l=null,o=null,c=null,d=null;try{const a=await p.get("/sidebar/menu");i=Array.isArray(a?.items)?a.items:null,r=a?.variant||null,l=a?.position||null,o=a?.style||null,c=a?.float||null,d=a?.highlight||null}catch{i=null}(!i||!i.some(a=>a&&(!a.hidden||a.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),i=ue.slice());let u=[];try{u=await H.get("/api/sidebar/registered-items")||[]}catch{}i=re(i,de),i=ie(i,u),i=oe(i),i=le(i,[ae()]);const v=await me(s);v.length&&fe(i,v),i=K(i,s,n),i=ce(i);const[y,L]=await Promise.all([be(u.filter(a=>_(s,a?.item?.permission)),s).catch(()=>({})),we()]),E=r?` dm-admin-sidebar--${h(r)}`:"";function w(a,b="px"){if(a==null)return a;const m=String(a).trim();return/^\d+(\.\d+)?$/.test(m)?`${m}${b}`:m}let F="";if(o){const a=[];o.fontFamily&&a.push(`font-family: ${o.fontFamily}, sans-serif`),o.fontSize&&a.push(`font-size: ${w(o.fontSize)}`),o.fontWeight&&a.push(`font-weight: ${o.fontWeight}`),o.letterSpacing&&a.push(`letter-spacing: ${w(o.letterSpacing,"em")}`);const b=[];if(a.length&&b.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 { ${a.join("; ")} }`),o.iconSize){const m=w(o.iconSize);b.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${m} !important; height: ${m} !important; }`)}b.length&&(F=`<style data-admin-sidebar-style>${b.join(" ")}</style>`)}const B=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${h(L)}</div>`,R=i.some(a=>a&&a.type==="spacer"&&a.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",q=`${F}<nav class="dm-admin-sidebar${E}${R}">${B}${i.map(a=>Y(a,y)).join("")}</nav>`,j=document.createRange();j.selectNodeContents(e);const g=j.createContextualFragment(q);e.replaceChildren(g),Domma.icons.scan(e);for(const a of A)I.set(a.url,y[a.url]??null);G(e),$e(e);const f=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(f)&&f>=180&&f<=480&&(e.style.width=f+"px"),e.removeAttribute("data-menu-hover"),e.removeAttribute("data-menu-active"),e.style.removeProperty("--dm-menu-highlight"),ne(e,{highlight:d});const C=document.querySelector(".dashboard-wrapper");if(["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(a=>{e.style[a]=""}),e.classList.remove("dm-menu-floating"),C&&(C.style.paddingLeft="",C.style.paddingRight=""),l==="floating"){te(e,{position:"floating",float:c||{}});const b=c&&c.anchor||"TL",m=Number.isFinite(Number(c?.offsetX))?Number(c.offsetX):16,N=e.getBoundingClientRect().width||f||260;if(C){const z=Math.round(N+m*2)+"px";b==="R"||b==="TR"||b==="BR"?C.style.paddingRight=z:C.style.paddingLeft=z}}const k=document.createElement("div");k.className="dm-admin-sidebar-handle",k.setAttribute("aria-label","Resize sidebar"),e.appendChild(k),e.querySelectorAll("details").forEach(a=>{a.addEventListener("toggle",function(){const b=this.getAttribute("data-state-key");b&&S.set(b,this.open);const m=this.querySelector(":scope > .sidebar-children");if(m)if(this.open){const N=m.scrollHeight;m.style.maxHeight="0",requestAnimationFrame(()=>{m.style.maxHeight=N+"px",m.addEventListener("transitionend",function z(){m.style.maxHeight="none",m.removeEventListener("transitionend",z)})})}else{const N=m.scrollHeight;m.style.maxHeight=N+"px",requestAnimationFrame(()=>{m.style.maxHeight="0"})}})});let x=!1,W=0,O=0;k.addEventListener("mousedown",a=>{x=!0,W=a.clientX,O=e.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",a.preventDefault()}),e.dataset.resizeWired||(e.dataset.resizeWired="1",document.addEventListener("mousemove",a=>e.__dmResize?.move(a)),document.addEventListener("mouseup",()=>e.__dmResize?.up()),M.subscribe("router:afterChange",()=>e.__dmSetActive?.())),e.__dmResize={move(a){if(!x)return;const b=Math.max(180,Math.min(480,O+(a.clientX-W)));e.style.width=b+"px"},up(){x&&(x=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(e.style.width,10)))}};function D(){const a=location.hash||"#/";$(e).find(".sidebar-link").removeClass("active"),$(e).find(`.sidebar-link[data-url="${a}"]`).addClass("active")}D(),e.__dmSetActive=D}
4
+ </details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${m}${c}>${n} <span class="sidebar-text">${e}</span>${pe(t,s)}${v}</a>`}async function be(t=[],s=[]){const n=d=>d.then(g=>Array.isArray(g)?g.length:Array.isArray(g?.entries)?g.entries.length:0).catch(()=>0);C=xe(t);const e=new Set(C.map(d=>d.url)),i={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"},o=(d,g)=>z(s,d)&&!e.has(i[d])?g():Promise.resolve(0),[l,a,c,u,m,v,y,k,E,w,P,B,q,T]=await Promise.all([o("pages",()=>n(p.pages.list())),o("media",()=>n(p.media.list())),o("menus",()=>n(p.menus.list())),o("context-menus",()=>n(p.contextMenus.list())),o("theme",()=>p.theme.get().then(d=>(d?.overrides||[]).length).catch(()=>0)),o("collections",()=>n(p.collections.list())),o("forms",()=>n(p.forms.list())),o("views",()=>n(p.views.list())),o("actions",()=>n(p.actions.list())),o("blocks",()=>n(p.blocks.list())),o("components",()=>n(p.components.list())),o("users",()=>n(p.users.list())),o("plugins",()=>n(p.plugins.list())),o("notifications",()=>p.system?.notifications?.unreadCount?.().then(d=>d?.count??0).catch(()=>0)??Promise.resolve(0))]),j={};await Promise.all(C.map(async d=>{const g=await Q(d);g&&(j[d.url]=g)}));const I={"#/pages":l,"#/media":a,"#/menus":c,"#/context-menus":u,"#/theme":m,"#/collections":v,"#/forms":y,"#/views":k,"#/actions":E,"#/blocks":w,"#/components":P,"#/users":B,"#/plugins":q,"#/system/notifications":T},x={};for(const[d,g]of Object.entries(I)){if(e.has(d))continue;const[L,R]=he[d]??["item","items"],A=U(g);A&&(x[d]={...A,label:`${A.count} ${A.count===1?L:R}`})}return{...x,...j}}let C=[];const ye="dm:sidebar-count",ve=6e4;function xe(t=[]){const s=[],n=e=>{if(e){e.url&&typeof e.countUrl=="string"&&e.countUrl&&s.push({url:e.url,countUrl:e.countUrl,text:e.text||"",countLabel:e.countLabel||"",inventory:e.inventory===!0});for(const i of[...e.children||[],...e.items||[]])n(i)}};for(const e of t??[])n(e?.item??e);return s}async function Q(t){try{const s=new Date,n=`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(s.getDate()).padStart(2,"0")}`,e=t.countUrl+(t.countUrl.includes("?")?"&":"?")+`today=${n}`;return U(await H.get(e),{fallbackLabel:t.countLabel||t.text})}catch{return null}}function Z(t,s,n){const e=[...t.querySelectorAll("a.sidebar-link")].find(o=>o.dataset.url===s);if(!e)return;const i=e.querySelector(".sidebar-badge[data-count-for]");if(!n||!(n.count>0))i?.remove();else{const o=document.createRange();o.selectNodeContents(e);const l=o.createContextualFragment(V(s,n)).firstElementChild;i?i.replaceWith(l):e.querySelector(".sidebar-text")?.after(l)}O.set(s,n),G(t)}const O=new Map;async function W(t,s=null){const n=s?C.filter(e=>e.url===s):C;await Promise.all(n.map(async e=>{const i=await Q(e),o=O.get(e.url);JSON.stringify(o??null)!==JSON.stringify(i)&&Z(t,e.url,i)}))}function $e(t){t.dataset.liveCounts||(t.dataset.liveCounts="1",window.addEventListener(ye,s=>{const n=s.detail||{};if(n.url)if(n.count!==void 0){const e=C.find(i=>i.url===n.url);Z(t,n.url,U(n,{fallbackLabel:e?.countLabel||e?.text||""}))}else W(t,n.url)}),setInterval(()=>{!document.hidden&&document.body.contains(t)&&W(t)},ve),document.addEventListener("visibilitychange",()=>{document.hidden||W(t)}),Se(t))}function Ce(t,s,n){const e=(l,a,c)=>{const u=document.createElement(l);return a&&(u.className=a),c!=null&&(u.textContent=String(c)),u};if(!n){t.textContent=s;return}t.appendChild(e("strong","dm-badge-pop-title",n.title||s)),(n.rows?.length||!n.items?.length)&&t.appendChild(e("p","dm-badge-pop-sub",s));const i=Array.isArray(n.rows)?n.rows.slice(0,6):[];if(i.length){const l=e("dl","dm-badge-pop-rows");for(const a of i)l.appendChild(e("dt",null,a.label)),l.appendChild(e("dd",a.tone==="danger"?"is-danger":null,a.value));t.appendChild(l)}const o=Array.isArray(n.items)?n.items.slice(0,6):[];if(o.length){const l=e("ul","dm-badge-pop-items");for(const a of o){const c=e("li",a.tone==="danger"?"is-danger":null);c.appendChild(e("span","dm-badge-pop-text",a.text)),a.meta&&c.appendChild(e("span","dm-badge-pop-meta",a.meta)),l.appendChild(c)}t.appendChild(l)}else n.empty&&t.appendChild(e("p","dm-badge-pop-empty",n.empty))}function Se(t){let s=null,n=null;const e=()=>{clearTimeout(n),s?.remove(),s=null},i=o=>{e();const l=o?.dataset.badgeLabel;l&&(n=setTimeout(()=>{s=document.createElement("div"),s.className="dm-help-pop dm-badge-pop",s.setAttribute("role","tooltip"),Ce(s,l,N.get(o.dataset.countFor)),document.body.appendChild(s);const a=o.getBoundingClientRect(),c=s.getBoundingClientRect(),u=Math.min(a.right+10,innerWidth-c.width-8);s.style.left=`${Math.max(8,u)}px`,s.style.top=`${Math.max(8,Math.min(a.top+a.height/2-c.height/2,innerHeight-c.height-8))}px`},150))};t.addEventListener("mouseover",o=>{const l=o.target.closest?.(".sidebar-badge[data-badge-label]");l&&i(l)}),t.addEventListener("mouseout",o=>{o.target.closest?.(".sidebar-badge[data-badge-label]")&&e()}),t.addEventListener("focusin",o=>{const l=o.target.closest?.("a, summary")?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");l?i(l):e()}),t.addEventListener("focusout",e),window.addEventListener("scroll",e,!0),window.addEventListener("hashchange",e)}async function we(){try{const t=await p.settings.get();return t?.adminBrand?.title||t?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:t,permissions:s,adminScope:n=null}){const e=$(t).get(0);if(!e)return;let i,o=null,l=null,a=null,c=null,u=null;try{const r=await p.get("/sidebar/menu");i=Array.isArray(r?.items)?r.items:null,o=r?.variant||null,l=r?.position||null,a=r?.style||null,c=r?.float||null,u=r?.highlight||null}catch{i=null}(!i||!i.some(r=>r&&(!r.hidden||r.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),i=ue.slice());let m=[];try{m=await H.get("/api/sidebar/registered-items")||[]}catch{}i=re(i,de),i=ie(i,m),i=oe(i),i=le(i,[ae()]);const v=await me(s);v.length&&fe(i,v),i=K(i,s,n),i=ce(i);const[y,k]=await Promise.all([be(m.filter(r=>z(s,r?.item?.permission)),s).catch(()=>({})),we()]),E=o?` dm-admin-sidebar--${h(o)}`:"";function w(r,b="px"){if(r==null)return r;const f=String(r).trim();return/^\d+(\.\d+)?$/.test(f)?`${f}${b}`:f}let P="";if(a){const r=[];a.fontFamily&&r.push(`font-family: ${a.fontFamily}, sans-serif`),a.fontSize&&r.push(`font-size: ${w(a.fontSize)}`),a.fontWeight&&r.push(`font-weight: ${a.fontWeight}`),a.letterSpacing&&r.push(`letter-spacing: ${w(a.letterSpacing,"em")}`);const b=[];if(r.length&&b.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 { ${r.join("; ")} }`),a.iconSize){const f=w(a.iconSize);b.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${f} !important; height: ${f} !important; }`)}b.length&&(P=`<style data-admin-sidebar-style>${b.join(" ")}</style>`)}const B=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${h(k)}</div>`,q=i.some(r=>r&&r.type==="spacer"&&r.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",T=`${P}<nav class="dm-admin-sidebar${E}${q}">${B}${i.map(r=>Y(r,y)).join("")}</nav>`,j=document.createRange();j.selectNodeContents(e);const I=j.createContextualFragment(T);e.replaceChildren(I),Domma.icons.scan(e);for(const r of C)O.set(r.url,y[r.url]??null);G(e),$e(e);const x=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(x)&&x>=180&&x<=480&&(e.style.width=x+"px"),e.removeAttribute("data-menu-hover"),e.removeAttribute("data-menu-active"),e.style.removeProperty("--dm-menu-highlight"),ne(e,{highlight:u});const d=document.querySelector(".dashboard-wrapper");if(["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(r=>{e.style[r]=""}),e.classList.remove("dm-menu-floating"),d&&(d.style.paddingLeft="",d.style.paddingRight=""),l==="floating"){te(e,{position:"floating",float:c||{}});const b=c&&c.anchor||"TL",f=Number.isFinite(Number(c?.offsetX))?Number(c.offsetX):16,F=e.getBoundingClientRect().width||x||260;if(d){const _=Math.round(F+f*2)+"px";b==="R"||b==="TR"||b==="BR"?d.style.paddingRight=_:d.style.paddingLeft=_}}const g=document.createElement("div");g.className="dm-admin-sidebar-handle",g.setAttribute("aria-label","Resize sidebar"),e.appendChild(g),e.querySelectorAll("details").forEach(r=>{r.addEventListener("toggle",function(){const b=this.getAttribute("data-state-key");b&&S.set(b,this.open);const f=this.querySelector(":scope > .sidebar-children");if(f)if(this.open){const F=f.scrollHeight;f.style.maxHeight="0",requestAnimationFrame(()=>{f.style.maxHeight=F+"px",f.addEventListener("transitionend",function _(){f.style.maxHeight="none",f.removeEventListener("transitionend",_)})})}else{const F=f.scrollHeight;f.style.maxHeight=F+"px",requestAnimationFrame(()=>{f.style.maxHeight="0"})}})});let L=!1,R=0,A=0;g.addEventListener("mousedown",r=>{L=!0,R=r.clientX,A=e.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",r.preventDefault()}),e.dataset.resizeWired||(e.dataset.resizeWired="1",document.addEventListener("mousemove",r=>e.__dmResize?.move(r)),document.addEventListener("mouseup",()=>e.__dmResize?.up()),M.subscribe("router:afterChange",()=>e.__dmSetActive?.())),e.__dmResize={move(r){if(!L)return;const b=Math.max(180,Math.min(480,A+(r.clientX-R)));e.style.width=b+"px"},up(){L&&(L=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(e.style.width,10)))}};function D(){const r=location.hash||"#/";$(e).find(".sidebar-link").removeClass("active"),$(e).find(`.sidebar-link[data-url="${r}"]`).addClass("active")}D(),e.__dmSetActive=D}
@@ -0,0 +1 @@
1
+ import{THEMES as l}from"./themes.js";export const FILL_TOKENS=["--dm-card-bg","--dm-surface"],VISIBLE_TOKENS=[...FILL_TOKENS,"--dm-text","--dm-primary","--dm-background"],SWITCHER_POSITIONS=["right-center","left-center","bottom-right","bottom-left","top-right","top-left"],newOverrideId=()=>`ov-${Date.now()}-${Math.random().toString(36).slice(2,8)}`;export function cloneOverride(e={},t=newOverrideId){return{id:e.id||t(),label:String(e.label??""),enabled:e.enabled!==!1,scope:String(e.scope??""),theme:String(e.theme??""),tokens:{...e.tokens||{}},css:String(e.css??"")}}export function formFromConfig(e={}){const t=e.autoTheme||{},r=e.switcher||{};return{theme:e.theme||"charcoal-dark",adminTheme:e.adminTheme||"charcoal-dark",autoEnabled:!!t.enabled,dayTheme:t.dayTheme||"charcoal-light",nightTheme:t.nightTheme||"charcoal-dark",dayStart:t.dayStart||"07:00",nightStart:t.nightStart||"19:00",fontFamily:e.font?.family||"Roboto",fontSize:String(e.font?.size??16),swEnabled:r.enabled===!0,swPosition:SWITCHER_POSITIONS.includes(r.position)?r.position:"right-center",swOffsetX:String(r.offsetX??32),swOffsetY:String(r.offsetY??32),overrides:(Array.isArray(e.overrides)?e.overrides:[]).map(n=>cloneOverride(n))}}const h=(e,t)=>{const r=Number.parseInt(e,10);return Number.isFinite(r)?r:t};export function payloadFromForm(e,t={}){const r={enabled:!!e.autoEnabled,dayTheme:e.dayTheme||"charcoal-light",nightTheme:e.nightTheme||"charcoal-dark",dayStart:e.dayStart||"07:00",nightStart:e.nightStart||"19:00"};return{theme:r.enabled?r.dayTheme:e.theme,adminTheme:e.adminTheme,autoTheme:r,font:{family:e.fontFamily||"Roboto",size:h(e.fontSize,16)||16},switcher:{enabled:!!e.swEnabled,position:e.swPosition||"right-center",offsetX:h(e.swOffsetX,0),offsetY:h(e.swOffsetY,0),themes:Array.isArray(t.switcher?.themes)?t.switcher.themes:[]},overrides:(e.overrides||[]).map(n=>({id:n.id,label:String(n.label??"").trim(),enabled:n.enabled!==!1,scope:String(n.scope??"").trim(),theme:n.theme||"",tokens:Object.fromEntries(Object.entries(n.tokens||{}).map(([i,s])=>[String(i).trim(),String(s??"").trim()]).filter(([i,s])=>i&&s)),css:String(n.css??"")}))}}export function snapshot(e,t={}){const r=payloadFromForm(e,t);return JSON.stringify(r)}function c(e){const t=/^(\d{1,2}):(\d{2})$/.exec(String(e||""));return t?Number(t[1])*60+Number(t[2]):null}export function dayOrNight(e={},t=new Date){const r=c(e.dayStart)??420,n=c(e.nightStart)??1140,i=t.getHours()*60+t.getMinutes();return r===n?"day":r<n?i>=r&&i<n?"day":"night":i>=n&&i<r?"night":"day"}export function themeLabel(e,t=[]){if(!e)return"";const r=t.find(i=>i.id===e);if(r)return r.label;const n=l.find(i=>i.id===e);return n?n.admin?`${n.label} ${n.name}`:`${n.label} (${n.dark?"dark":"light"})`:e}export function themeDiff(e,t,r=6){const n=e||{},i=t||{},s=Object.keys(i).filter(a=>String(n[a]??"").toLowerCase()!==String(i[a]).toLowerCase()),o=[...VISIBLE_TOKENS.filter(a=>s.includes(a)),...s.filter(a=>!VISIBLE_TOKENS.includes(a))];return{changed:s.length,total:Object.keys(i).length,keepsFill:!FILL_TOKENS.some(a=>s.includes(a)),shown:o.slice(0,r).map(a=>({name:a,from:n[a]||"",to:i[a]})),more:Math.max(0,o.length-r)}}export const swatchColour=e=>/^#[0-9a-f]{3,8}$/i.test(String(e||""))?String(e):"";export function knownThemeIds(e={}){return new Set([...e.themes||[],...(e.custom||[]).map(t=>t.id)])}export function themeProblems(e,t){if(!t||!t.size)return[];t=new Set([...t,"core-light","core-dark"]);const r=[],n=(i,s)=>{s&&!t.has(s)&&r.push({where:i,id:s})};return e.autoEnabled?(n("Day theme",e.dayTheme),n("Night theme",e.nightTheme)):n("Site theme",e.theme),n("Admin theme",e.adminTheme),(e.overrides||[]).forEach((i,s)=>n(`Override "${i.label||i.scope||s+1}"`,i.theme)),r}export function overrideRow(e,t,{custom:r=[],known:n=null}={}){const i=Object.keys(e.tokens||{}).filter(o=>o&&String(e.tokens[o]??"").trim()).length,s=[e.scope||"no selector yet",e.theme?`adopts ${themeLabel(e.theme,r)}`:"",i?`${i} token${i===1?"":"s"}`:"",String(e.css||"").trim()?"custom CSS":""].filter(Boolean);return{id:e.id,name:e.label||e.scope||`Override ${t+1}`,meta:s.join(" \xB7 "),off:e.enabled===!1,missing:!!(e.theme&&n&&n.size&&!n.has(e.theme)),empty:!e.theme&&!i&&!String(e.css||"").trim()}}export function moveItem(e,t,r){const n=t+r;if(t<0||t>=e.length||n<0||n>=e.length)return e;const i=[...e],[s]=i.splice(t,1);return i.splice(n,0,s),i}
@@ -0,0 +1 @@
1
+ export const FILTERS=["all","public","restricted","broken"];const l={table:"Table",cards:"Cards",list:"List",block:"Block",accordion:"Accordion",timeline:"Timeline",carousel:"Carousel",listgroup:"List group"},u=(e,r,s=`${r}s`)=>`${e} ${e===1?r:s}`;export function projectScope(e=""){const r=String(e).split("?")[0].match(/^#\/projects\/([^/]+)\/views\/?$/);if(!r)return null;try{return decodeURIComponent(r[1])}catch{return r[1]}}export const projectOf=e=>e?.meta?.project||"core",newViewHref=e=>e?`#/views/new?project=${encodeURIComponent(e)}`:"#/views/new",shortcodeFor=e=>`[view slug="${e}" /]`,isBroken=e=>e?.sourceInfo?.exists===!1;export function arrangeViews(e,{filter:r="all",query:s="",project:o=null}={}){const i=String(s).trim().toLowerCase(),n={all:()=>!0,public:t=>t.access?.public===!0,restricted:t=>t.access?.public!==!0,broken:isBroken}[r]||(()=>!0);return[...e||[]].filter(t=>!o||projectOf(t)===o).filter(n).filter(t=>!i||[t.title,t.slug,t.description,t.pipeline?.source,t.sourceInfo?.title].some(c=>String(c||"").toLowerCase().includes(i))).sort((t,c)=>Number(isBroken(c))-Number(isBroken(t))||String(t.title||t.slug).localeCompare(String(c.title||c.slug)))}export function countViews(e,{project:r=null}={}){const s=(e||[]).filter(o=>!r||projectOf(o)===r);return{all:s.length,public:s.filter(o=>o.access?.public===!0).length,restricted:s.filter(o=>o.access?.public!==!0).length,broken:s.filter(isBroken).length}}export function viewRow(e){const r=Array.isArray(e.pipeline?.stages)?e.pipeline.stages.length:0,s=e.pipeline?.source||"-",o=e.sourceInfo,i=isBroken(e),n=!o||i?"":o.adapter==="mongodb"?"MongoDB aggregation":"JS pipeline",t=Array.isArray(e.access?.roles)?e.access.roles:[];return{slug:e.slug,title:e.title||e.slug,meta:[e.slug,`from ${o?.title&&!i?o.title:s}`,u(r,"stage"),l[e.display?.mode]||e.display?.mode||"Table"].join(" \xB7 "),description:e.description||"",access:e.access?.public?"Public":t.length?t.join(", "):"Admins",accessCls:e.access?.public?"is-ok":"",engine:n,project:e.meta?.project&&e.meta.project!=="core"?e.meta.project:"",broken:i,brokenText:i?`Source "${s}" is gone`:"",rowCls:i?"is-broken":""}}
@@ -1,19 +1,78 @@
1
- <div class="view-header">
2
- <h1><span data-icon="zap"></span> Actions</h1>
3
- <button id="create-action-btn" class="btn btn-primary">
4
- <span data-icon="plus"></span> New Action
5
- </button>
6
- </div>
1
+ <!-- Actions. Bound with M.applyBindings - see admin/views/actions-list.js, which also inserts the row
2
+ template (the router renders a templateUrl through _.render, eating any {{…}}). No page heading:
3
+ the Tool banner above is the heading, and New action is a banner action. -->
4
+ <div class="acx">
5
+ <p class="acx-note">Buttons on a collection's entries that run steps - update a field, move or create an entry,
6
+ call a webhook, send an email.
7
+ <span data-help-title="Actions" data-help="An action belongs to one collection and shows as a button on its entries (in the admin's entry list, and on the site with the [transitions] shortcode). Its steps run in order. With a transition set, it only appears while the entry's field has one of its 'from' values, and moves it to 'to'. Roles decide who may run it."></span></p>
7
8
 
8
- <div id="actions-pro-notice" class="card mb-3" style="display:none;">
9
- <div class="card-body" style="color:var(--dm-warning,#f59e0b);display:flex;align-items:center;gap:.6rem;">
10
- <span data-icon="warning"></span>
11
- <span>Actions require a MongoDB connection (pro mode). Configure one under Collections → Options.</span>
9
+ <!-- File mode: actions live in MongoDB. A state of the site, explained, not an error. -->
10
+ <div class="acx-card acx-pro" data-bind-hidden="!needsMongo.value">
11
+ <div class="acx-card-title"><span data-icon="database" data-icon-size="16"></span> Actions need MongoDB</div>
12
+ <p class="acx-note">Actions are a Pro feature: their settings are kept in MongoDB, and this site has no
13
+ MongoDB connection. Add a "default" connection (pro mode) and they appear here. Nothing else on the site
14
+ is affected.</p>
12
15
  </div>
13
- </div>
14
16
 
15
- <div class="card">
16
- <div class="card-body">
17
- <div id="actions-table"></div>
17
+ <p class="acx-notice" data-bind-hidden="!notice.value">
18
+ <span data-bind-text="notice.value"></span> <a href="#/actions">Show all actions</a></p>
19
+
20
+ <div class="acx-card" data-bind-hidden="needsMongo.value">
21
+ <div class="acx-bar">
22
+ <input type="search" class="form-input acx-search" placeholder="Search actions…" aria-label="Search actions" data-model="query.value">
23
+ <select class="form-input acx-filter" aria-label="Collection" data-model="collection.value" data-bind-hidden="!collections.value.length">
24
+ <option value="">All collections</option>
25
+ </select>
26
+ <span class="acx-count" data-bind-text="countText.value"></span>
27
+ <span data-help-title="The list" data-help="Click an action to edit it. Right-click it (or ⋮, or Shift+F10) to edit, duplicate, open its collection's entries, copy its slug or delete it."></span>
28
+ </div>
29
+ <p class="acx-note" data-bind-hidden="!loading.value">Loading…</p>
30
+ <p class="acx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
31
+ <p class="acx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
32
+ <div class="acx-list" data-each="rows.value key=slug"></div>
18
33
  </div>
19
34
  </div>
35
+
36
+ <style>
37
+ .acx { display: flex; flex-direction: column; gap: .75rem; }
38
+ .acx [hidden], .acx-form [hidden] { display: none !important; }
39
+ .acx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
40
+ .acx-note.is-bad { color: var(--dm-danger); }
41
+ .acx-notice { margin: 0; padding: .5rem .75rem; border-radius: var(--dm-radius-md); font-size: var(--dm-text-sm);
42
+ background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 12%, transparent); color: var(--dm-text); }
43
+ .acx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
44
+ .acx-card-title { display: flex; align-items: center; gap: .4rem; font-weight: 600; }
45
+ .acx-pro { border-left: 3px solid var(--dm-warning); }
46
+ .acx-pro .acx-card-title { color: var(--dm-warning); }
47
+ .acx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
48
+ .acx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
49
+ .acx-filter { width: auto; max-width: 14rem; }
50
+ .acx-count { color: var(--dm-text-muted); font-size: var(--dm-text-sm); margin-left: auto; }
51
+ .acx-list { display: flex; flex-direction: column; }
52
+ .acx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
53
+ .acx-row:last-child { border-bottom: 0; }
54
+ .acx-row:hover, .acx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
55
+ .acx-icon { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; flex-shrink: 0;
56
+ border-radius: var(--dm-radius-md); color: var(--dm-warning); background: color-mix(in srgb, var(--dm-warning) 14%, transparent); }
57
+ .acx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
58
+ .acx-main strong, .acx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
59
+ .acx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
60
+ .acx-chips { display: inline-flex; flex-wrap: wrap; justify-content: flex-end; gap: .3rem; max-width: 55%; }
61
+ .acx-chip { display: inline-flex; align-items: center; gap: .25rem; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem;
62
+ border-radius: 999px; white-space: nowrap; background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
63
+ .acx-chip.is-col { background: color-mix(in srgb, var(--dm-primary) 14%, transparent); color: var(--dm-primary); font-family: var(--dm-font-mono, monospace); }
64
+ .acx-chip.is-trig { background: color-mix(in srgb, var(--dm-warning) 16%, transparent); color: var(--dm-warning); }
65
+ .acx-chip.is-flow { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
66
+ .acx-chip.is-warn { background: color-mix(in srgb, var(--dm-danger) 14%, transparent); color: var(--dm-danger); }
67
+ .acx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
68
+ .acx-kebab:hover { color: var(--dm-primary); }
69
+ /* The Duplicate slideover */
70
+ .acx-form { display: flex; flex-direction: column; gap: .85rem; }
71
+ .acx-form .form-label { display: flex; align-items: center; gap: .3rem; }
72
+ .acx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
73
+ .acx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
74
+ .acx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
75
+ .acx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
76
+ .acx-spacer { flex: 1; }
77
+ @media (max-width: 720px) { .acx-chips { display: none; } }
78
+ </style>
@@ -1,13 +1,66 @@
1
- <div class="page-header">
2
- <h2><span data-icon="code"></span> API Builder</h2>
3
- <div class="header-actions">
4
- <a class="btn btn-primary" href="#/api-endpoints/new"><span data-icon="plus"></span> New endpoint</a>
5
- </div>
6
- </div>
7
- <p class="text-muted mb-4">Custom REST endpoints over collection data, served at <code>/api/x/&lt;project&gt;&lt;path&gt;</code>. Each endpoint binds a clean URL to a fixed collection query - definitions are data, never code.</p>
1
+ <!-- API Builder list. Bound with M.applyBindings - see admin/views/api-endpoints.js, which also inserts the
2
+ row template (the router renders a templateUrl through _.render, eating any {{…}}). No page heading:
3
+ the Tool banner above is the heading, and New endpoint is a banner action. -->
4
+ <div class="apx">
5
+ <p class="apx-note">Clean REST URLs over collection data, served at <code>/api/x/&lt;project&gt;&lt;path&gt;</code>.
6
+ Each endpoint binds a URL to a fixed collection query - definitions are data, never code.
7
+ <span data-help-title="API Builder" data-help="An endpoint answers GET requests at its URL with entries from one collection, filtered by values from the URL (:params) and the query string. Who may call it is set per endpoint: Public (anyone), API token (a token for its project), or a role (signed-in users). Only the fields you list are returned. An endpoint may only read collections in its own project or Core."></span></p>
8
8
 
9
- <div class="card">
10
- <div class="card-body">
11
- <div id="api-endpoints-table"></div>
9
+ <div class="apx-card">
10
+ <div class="apx-bar">
11
+ <div class="apx-seg" role="group" aria-label="Show">
12
+ <button type="button" data-on-click="showAll" data-bind-class="filter.value === 'all' && 'is-on'">All <b data-bind-text="counts.value.all"></b></button>
13
+ <button type="button" data-on-click="showEnabled" data-bind-class="filter.value === 'enabled' && 'is-on'">Enabled <b data-bind-text="counts.value.enabled"></b></button>
14
+ <button type="button" data-on-click="showDisabled" data-bind-class="filter.value === 'disabled' && 'is-on'">Disabled <b data-bind-text="counts.value.disabled"></b></button>
15
+ <button type="button" data-on-click="showPublic" data-bind-class="filter.value === 'public' && 'is-on'">Public <b data-bind-text="counts.value.public"></b></button>
16
+ </div>
17
+ <select class="form-input apx-project" aria-label="Project" data-model="project.value" data-bind-hidden="projectLocked.value"></select>
18
+ <input type="search" class="form-input apx-search" placeholder="Search endpoints…" aria-label="Search endpoints" data-model="query.value">
19
+ <span data-help-title="The list" data-help="Click an endpoint to open it in the builder, where you can try it. Right-click it (or ⋮, or Shift+F10) to copy its URL, open it in a new tab, enable or disable it, or delete it. A disabled endpoint answers 404 until it is enabled again."></span>
20
+ </div>
21
+ <p class="apx-note apx-scope" data-bind-hidden="!projectLocked.value">
22
+ <span data-bind-text="scopeText.value"></span> <a href="#/api-endpoints">Show all endpoints</a></p>
23
+ <p class="apx-note" data-bind-hidden="!loading.value">Loading…</p>
24
+ <p class="apx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
25
+ <p class="apx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
26
+ <div class="apx-list" data-each="rows.value key=id"></div>
12
27
  </div>
13
28
  </div>
29
+
30
+ <style>
31
+ .apx { display: flex; flex-direction: column; gap: .75rem; }
32
+ .apx [hidden] { display: none !important; }
33
+ .apx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
34
+ .apx-note.is-bad { color: var(--dm-danger); }
35
+ .apx-note code { font-size: .95em; }
36
+ .apx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
37
+ .apx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
38
+ .apx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
39
+ .apx-project { width: auto; min-width: 9rem; }
40
+ .apx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
41
+ .apx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
42
+ .apx-seg button + button { border-left: 1px solid var(--dm-border); }
43
+ .apx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
44
+ .apx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
45
+ .apx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
46
+ .apx-list { display: flex; flex-direction: column; }
47
+ .apx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
48
+ .apx-row:last-child { border-bottom: 0; }
49
+ .apx-row:hover, .apx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
50
+ .apx-verb { flex-shrink: 0; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-xs); font-weight: 700; padding: .15rem .45rem;
51
+ border-radius: var(--dm-radius-sm); color: var(--dm-success); background: color-mix(in srgb, var(--dm-success) 14%, transparent); }
52
+ .apx-row.is-off .apx-verb { color: var(--dm-text-muted); background: color-mix(in srgb, var(--dm-text-muted) 12%, transparent); }
53
+ .apx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
54
+ .apx-main strong, .apx-main code, .apx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
55
+ .apx-main code { font-size: var(--dm-text-sm); color: var(--dm-text); background: none; padding: 0; }
56
+ .apx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
57
+ .apx-row.is-off .apx-main strong, .apx-row.is-off .apx-main code { color: var(--dm-text-muted); }
58
+ .apx-chips { display: inline-flex; gap: .3rem; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
59
+ .apx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
60
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
61
+ .apx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
62
+ .apx-chip.is-info { background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 14%, transparent); color: var(--dm-info, var(--dm-primary)); }
63
+ .apx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
64
+ .apx-kebab:hover { color: var(--dm-primary); }
65
+ @media (max-width: 720px) { .apx-chips .apx-chip:not(.is-warn):not(.is-off) { display: none; } }
66
+ </style>