domma-cms 0.116.8 → 0.118.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 (37) hide show
  1. package/CLAUDE.md +7 -4
  2. package/admin/js/app.js +2 -2
  3. package/admin/js/lib/api-tokens-arrange.js +3 -3
  4. package/admin/js/lib/content-api-arrange.js +1 -0
  5. package/admin/js/lib/menu-editor-arrange.js +1 -1
  6. package/admin/js/lib/menu-rows.js +1 -1
  7. package/admin/js/lib/sidebar-renderer.js +1 -1
  8. package/admin/js/templates/api-reference.html +2 -1
  9. package/admin/js/templates/api-tokens.html +3 -3
  10. package/admin/js/templates/content-api.html +208 -0
  11. package/admin/js/templates/docs/api-content.html +183 -0
  12. package/admin/js/templates/docs/api-external.html +2 -1
  13. package/admin/js/templates/docs/usage-editions.html +1 -1
  14. package/admin/js/templates/menu-editor.html +6 -0
  15. package/admin/js/views/api-tokens.js +6 -6
  16. package/admin/js/views/content-api.js +7 -0
  17. package/admin/js/views/doc-pages.js +1 -1
  18. package/admin/js/views/index.js +1 -1
  19. package/admin/js/views/menu-editor.js +14 -14
  20. package/package.json +1 -1
  21. package/public/js/menu-decor.mjs +1 -1
  22. package/public/js/site.js +1 -1
  23. package/server/routes/api/collections.js +10 -17
  24. package/server/routes/api/content-api.js +70 -0
  25. package/server/routes/api/content-public.js +299 -0
  26. package/server/routes/public.js +17 -68
  27. package/server/server.js +11 -4
  28. package/server/services/apiTokens.js +61 -5
  29. package/server/services/content.js +4 -4
  30. package/server/services/contentApi.js +248 -0
  31. package/server/services/menuRender.js +9 -3
  32. package/server/services/menus.js +4 -1
  33. package/server/services/pagePublishing.js +85 -0
  34. package/server/services/permissionRegistry.js +1 -1
  35. package/server/services/renderer.js +12 -2
  36. package/server/services/search.js +31 -12
  37. package/server/templates/page.html +1 -1
package/CLAUDE.md CHANGED
@@ -259,6 +259,7 @@ false})` - no injected snippets or beacon near a URL that carries a host key.
259
259
  | `config/connections.json` | Yours (Pro only) | Named MongoDB connections; absence = file mode |
260
260
  | `config/effects.json` | Yours | Effects / celebrate shortcode config |
261
261
  | `config/search.json` | Yours | Public site search: enabled, trigger, placement, limits |
262
+ | `config/content-api.json` | Yours (optional) | Content API: enabled, access, resources, formats, page list - absence = off |
262
263
  | `config/cache.json` | Yours (optional) | Response cache: enabled, driver (memory/none/redis), TTL - absence = defaults (off in dev, on in prod) |
263
264
 
264
265
  See `docs/configuration.md` for full schema reference.
@@ -489,7 +490,9 @@ skill** (`.claude/skills/domma-projects/SKILL.md`) - load it before touching pro
489
490
  artefact tagging, or per-project access checks.
490
491
  ## External API & API tokens, custom API endpoints
491
492
 
492
- The external surface - `/api/v1/:slug` collection access, project-scoped API tokens, and API Builder
493
- endpoints at `/api/x/<project><path>` - has its own contracts (strict token auth, project binding,
494
- read-field allowlists, cross-project refusals). Full detail in the **`domma-api-surface` skill**
495
- (`.claude/skills/domma-api-surface/SKILL.md`) - load it before touching any of that.
493
+ The external surface - `/api/v1/:slug` collection access, project-scoped API tokens, API Builder
494
+ endpoints at `/api/x/<project><path>`, and the read-only Content API at `/api/content/v1` (pages, menus,
495
+ site settings, search - one page decision shared with the site via `resolvePublishedPage()`) - has its
496
+ own contracts (strict token auth, project binding, read-field allowlists, cross-project refusals).
497
+ Full detail in the **`domma-api-surface` skill** (`.claude/skills/domma-api-surface/SKILL.md`) - load
498
+ it before touching any of that.
package/admin/js/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import{views as q}from"./views/index.js";import{withPluginChrome as B}from"./lib/plugin-chrome.js";import{api as r,getUser as F,isAuthenticated as c,logout as G,sendToLogin as z}from"./api.js";import{isAuthScreen as C,loginHashFor as K}from"./lib/login-arrange.js";import{installHttpInterceptor as J}from"./http-interceptor.js";import{installResizableSlideovers as Y}from"./lib/slideover-resizable.js";import{installActivityThrobber as Q}from"./lib/activity-throbber.js";import{isPathAllowed as X,redirectFor as Z}from"./lib/admin-scope.js";import{deliverDesktop as tt,desktopActive as et,notificationSettings as it}from"./lib/desktop-notify.js";import{avatarUrl as ot}from"./lib/avatar.js";import{installCalculator as at}from"./lib/calculator.js";$(()=>{J(),at(),Q(),Y(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");c()?await i(r,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let m=null,l="/",h=!1,w=null;function D(t){if(y[t])return E.toast(`${y[t]} is switched off on this site.`,{type:"info"}),"/";const i=b[t]||Z(m,t);return i||((t.split("?")[0]||"/")==="/"&&l&&l!=="/"?b[l]||l:null)}const b={},y={};R.use(async(t,i,e)=>{if(C(t.path))return e();if(!c()){z();return}const o=D(t.path);if(o){R.navigate(o);return}e()});async function g(){try{const t=await r.get("/auth/permissions");m=t.adminScope||null,l=t.adminHome||"/";const i=t.permissions||[];return h=X(m,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),w=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return m=null,l="/",h=!1,w=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{g()});let N=null,P=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(N){P=!0;return}N=(async()=>{do{P=!1;try{await j(await g())}catch{}}while(P)})().finally(()=>{N=null})});async function j(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:m})}M.subscribe("router:afterChange",({to:t})=>{C(t.path)&&(u&&(clearInterval(u),u=null),m=null,l="/",h=!1,w=null)});const O=[{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/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"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:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:i})=>{if(!C(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),C(i?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(r)),await I();const e=await g(),o=D(t.path);if(o&&R.navigate(o),j(e),it.read().signInToasts)try{const a=await r.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 f=d(s.data?.title||""),T=d((s.data?.body||"").slice(0,120));E.toast(`${f} - ${T}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}x()}}),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 _=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function V(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const i=t?String(t.path).split("?")[0]:"";if(!t||!V(i))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(i))return;let a=0;const n=60,s=1500,f=()=>{a+=n;for(const T of _){const v=document.querySelector(T);if(!v)continue;if(Array.from(v.options||[]).find(H=>H.value===o)){v.value=o,v.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(f,n)};setTimeout(f,n)});const U="cms_card_states",A=S.get(U)||{};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&&A[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const i=$(this).closest(".card");i.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(A[e]=i.hasClass("card-collapsed")?"collapsed":"open",S.set(U,A))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const p={...q},W={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},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"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},dashboard:{displayName:"Dashboard",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},blockEditor:{displayName:"Blocks",section:"Edit",icon:"box"},componentEditor:{displayName:"Components",section:"Edit",icon:"component"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,i]of Object.entries(W))p[t]&&(p[t]=B(p[t],{plugin:t,licence:"Built in",...i}));const k=[...O];async function I(){if(c())try{const t=await r.plugins.adminConfig(),i=new Set(k.map(e=>e.path));for(const e of t.routes||[])i.has(e.path)||k.push(e);for(const[e,o]of Object.entries(t.views||{}))try{const a=await import(`/plugins/${o.entry}`);p[e]=B(a[o.exportName],(t.meta||{})[e])}catch{}for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(b[`/${e}`]=o);for(const[e,{enabled:o,displayName:a}]of Object.entries(t.tools||{}))o===!1&&(y[`/${e}`]=a);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const a=document.createElement("link");a.id=e,a.rel="stylesheet",a.href=o,document.head.appendChild(a)}}catch{}}function x(){const t=F();if(!t||$("#topbar-user-name").length)return;const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,o=ot(t);$("#topbar-user").html(`
1
+ import{views as q}from"./views/index.js";import{withPluginChrome as B}from"./lib/plugin-chrome.js";import{api as r,getUser as F,isAuthenticated as c,logout as G,sendToLogin as z}from"./api.js";import{isAuthScreen as C,loginHashFor as K}from"./lib/login-arrange.js";import{installHttpInterceptor as J}from"./http-interceptor.js";import{installResizableSlideovers as Y}from"./lib/slideover-resizable.js";import{installActivityThrobber as Q}from"./lib/activity-throbber.js";import{isPathAllowed as X,redirectFor as Z}from"./lib/admin-scope.js";import{deliverDesktop as tt,desktopActive as et,notificationSettings as it}from"./lib/desktop-notify.js";import{avatarUrl as ot}from"./lib/avatar.js";import{installCalculator as at}from"./lib/calculator.js";$(()=>{J(),at(),Q(),Y(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");c()?await i(r,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let m=null,l="/",h=!1,w=null;function D(t){if(y[t])return E.toast(`${y[t]} is switched off on this site.`,{type:"info"}),"/";const i=b[t]||Z(m,t);return i||((t.split("?")[0]||"/")==="/"&&l&&l!=="/"?b[l]||l:null)}const b={},y={};R.use(async(t,i,e)=>{if(C(t.path))return e();if(!c()){z();return}const o=D(t.path);if(o){R.navigate(o);return}e()});async function g(){try{const t=await r.get("/auth/permissions");m=t.adminScope||null,l=t.adminHome||"/";const i=t.permissions||[];return h=X(m,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),w=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return m=null,l="/",h=!1,w=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{g()});let P=null,N=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(P){N=!0;return}P=(async()=>{do{N=!1;try{await A(await g())}catch{}}while(N)})().finally(()=>{P=null})});async function A(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:m})}M.subscribe("router:afterChange",({to:t})=>{C(t.path)&&(u&&(clearInterval(u),u=null),m=null,l="/",h=!1,w=null)});const O=[{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/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"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:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:i})=>{if(!C(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),C(i?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(r)),await I();const e=await g(),o=D(t.path);if(o&&R.navigate(o),A(e),it.read().signInToasts)try{const a=await r.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 f=d(s.data?.title||""),x=d((s.data?.body||"").slice(0,120));E.toast(`${f} - ${x}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}U()}}),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 _=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function V(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const i=t?String(t.path).split("?")[0]:"";if(!t||!V(i))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(i))return;let a=0;const n=60,s=1500,f=()=>{a+=n;for(const x of _){const v=document.querySelector(x);if(!v)continue;if(Array.from(v.options||[]).find(H=>H.value===o)){v.value=o,v.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(f,n)};setTimeout(f,n)});const T="cms_card_states",j=S.get(T)||{};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&&j[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const i=$(this).closest(".card");i.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(j[e]=i.hasClass("card-collapsed")?"collapsed":"open",S.set(T,j))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const p={...q},W={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},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"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},blockEditor:{displayName:"Blocks",section:"Edit",icon:"box"},componentEditor:{displayName:"Components",section:"Edit",icon:"component"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,i]of Object.entries(W))p[t]&&(p[t]=B(p[t],{plugin:t,licence:"Built in",...i}));const k=[...O];async function I(){if(c())try{const t=await r.plugins.adminConfig(),i=new Set(k.map(e=>e.path));for(const e of t.routes||[])i.has(e.path)||k.push(e);for(const[e,o]of Object.entries(t.views||{}))try{const a=await import(`/plugins/${o.entry}`);p[e]=B(a[o.exportName],(t.meta||{})[e])}catch{}for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(b[`/${e}`]=o);for(const[e,{enabled:o,displayName:a}]of Object.entries(t.tools||{}))o===!1&&(y[`/${e}`]=a);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const a=document.createElement("link");a.id=e,a.rel="stylesheet",a.href=o,document.head.appendChild(a)}}catch{}}function U(){const t=F();if(!t||$("#topbar-user-name").length)return;const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,o=ot(t);$("#topbar-user").html(`
2
2
  ${o?`<img id="topbar-avatar" class="topbar-avatar" src="${d(o)}" alt="">`:""}
3
3
  <span id="topbar-user-name" class="topbar-user-name">${d(t.name)}</span>
4
4
  <span class="topbar-role-badge topbar-role-badge--${d(t.role)}">${e}</span>
@@ -19,4 +19,4 @@ import{views as q}from"./views/index.js";import{withPluginChrome as B}from"./lib
19
19
  <span data-icon="log-out"></span>
20
20
  <span>Sign out</span>
21
21
  </a>
22
- `);const a=$("#topbar-bell").get(0);a&&(a.hidden=!0);const n=$("#topbar-settings").get(0);n&&(n.hidden=!h),$("#topbar-logout-btn").on("click",s=>{s.preventDefault(),G()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),L(),u||(u=setInterval(L,6e4))}function d(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let u=null;async function L(){if(!(!c()||w===!1))try{const{count:t}=await r.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const i=$("#topbar-bell-badge");t>0?i.text(t>99?"99+":String(t)).removeAttr("hidden"):i.attr("hidden","").text(""),t>0&&et()&&tt(await r.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!c()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash.startsWith("#/login")||(window.location.hash=K(window.location.hash));else if(await I(),F()){const a=await g(),n=(window.location.hash||"#/").slice(1)||"/",s=D(n);s&&(window.location.hash=`#${s}`),await j(a),x()}const t=R._extractParams.bind(R);R._extractParams=function(o,a){const n=a.indexOf("?");if(n!==-1&&(a=a.slice(0,n)),o.endsWith("/*")){const s=o.slice(0,-2);return a.startsWith(s+"/")?{}:null}return t(o,a)};const i=R._renderNotFound.bind(R);let e="";R._renderNotFound=async function(o){return c()&&e!==o&&(e=o,await I(),R._matchRoute(o).route)?(e="",R._handleRouteChange()):(e="",c()&&$("#admin-sidebar").get(0)?.style.display==="none"?(E.toast("That link goes nowhere - here is your home page.",{type:"warning"}),R.navigate("/")):i(o))},R.init({container:"#view-container",routes:k,views:p,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}})})()});
22
+ `);const a=$("#topbar-bell").get(0);a&&(a.hidden=!0);const n=$("#topbar-settings").get(0);n&&(n.hidden=!h),$("#topbar-logout-btn").on("click",s=>{s.preventDefault(),G()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),L(),u||(u=setInterval(L,6e4))}function d(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")}let u=null;async function L(){if(!(!c()||w===!1))try{const{count:t}=await r.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const i=$("#topbar-bell-badge");t>0?i.text(t>99?"99+":String(t)).removeAttr("hidden"):i.attr("hidden","").text(""),t>0&&et()&&tt(await r.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!c()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash.startsWith("#/login")||(window.location.hash=K(window.location.hash));else if(await I(),F()){const a=await g(),n=(window.location.hash||"#/").slice(1)||"/",s=D(n);s&&(window.location.hash=`#${s}`),await A(a),U()}const t=R._extractParams.bind(R);R._extractParams=function(o,a){const n=a.indexOf("?");if(n!==-1&&(a=a.slice(0,n)),o.endsWith("/*")){const s=o.slice(0,-2);return a.startsWith(s+"/")?{}:null}return t(o,a)};const i=R._renderNotFound.bind(R);let e="";R._renderNotFound=async function(o){return c()&&e!==o&&(e=o,await I(),R._matchRoute(o).route)?(e="",R._handleRouteChange()):(e="",c()&&$("#admin-sidebar").get(0)?.style.display==="none"?(E.toast("That link goes nowhere - here is your home page.",{type:"warning"}),R.navigate("/")):i(o))},R.init({container:"#view-container",routes:k,views:p,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}})})()});
@@ -1,3 +1,3 @@
1
- import{fmtDate as f}from"./dates.js";export const VERBS=["create","read","update","delete"],FILTERS=["all","active","expiring","expired","disabled"],SOON_DAYS=14;const x=864e5,d=(e,t,r=`${t}s`)=>`${e} ${e===1?t:r}`;export function tokenState(e,t=Date.now()){const r=e?.expiresAt?Date.parse(e.expiresAt):NaN,n=Number.isFinite(r)?Math.ceil((r-t)/x):null;return Number.isFinite(r)&&r<=t?{state:"expired",days:n}:e?.enabled===!1?{state:"disabled",days:n}:n!==null&&n<=SOON_DAYS?{state:"expiring",days:n}:{state:"active",days:n}}export function expiresIn(e){return e==null?"":e<=0?"today":e===1?"tomorrow":`in ${e} days`}export function ago(e,t=Date.now()){const r=Date.parse(e);if(!Number.isFinite(r))return"";const n=Math.max(0,Math.round((t-r)/1e3));return n<60?"just now":n<3600?`${Math.round(n/60)} min ago`:n<86400?`${d(Math.round(n/3600),"hour")} ago`:n<86400*30?`${d(Math.round(n/86400),"day")} ago`:f(r)}export function scopesToText(e){return(Array.isArray(e)?e:[]).map(t=>Array.isArray(t.verbs)&&t.verbs.length?`${t.collection}: ${t.verbs.join(", ")}`:t.collection).join(`
2
- `)}export function parseScopes(e){const t=[],r=new Set;for(const n of String(e??"").split(`
3
- `)){const i=n.trim();if(!i)continue;const s=i.indexOf(":"),a=(s<0?i:i.slice(0,s)).trim(),o=s<0?"":i.slice(s+1).trim();if(!a)return{error:`"${i}" names no collection.`};if(r.has(a))return{error:`"${a}" is listed twice.`};r.add(a);const c={collection:a};if(o){const p=o.split(",").map(l=>l.trim().toLowerCase()).filter(Boolean),u=p.find(l=>!VERBS.includes(l));if(u)return{error:`"${u}" is not a verb - use ${VERBS.join(", ")}.`};c.verbs=[...new Set(p)]}t.push(c)}return{scopes:t}}export function scopesSummary(e){return!Array.isArray(e)||!e.length?"every collection in the project":e.map(t=>`${t.collection}: ${Array.isArray(t.verbs)&&t.verbs.length?t.verbs.join(", "):"all"}`).join(" \xB7 ")}export function arrangeTokens(e,{filter:t="all",project:r="",query:n=""}={},i=Date.now()){const s=String(n).trim().toLowerCase(),a={expired:0,expiring:1,active:2,disabled:3};return(e||[]).map(o=>({t:o,s:tokenState(o,i)})).filter(({s:o})=>t==="all"||o.state===t).filter(({t:o})=>!r||o.project===r).filter(({t:o})=>!s||[o.name,o.project,o.tokenHint,o.createdBy,scopesSummary(o.scopes)].some(c=>String(c||"").toLowerCase().includes(s))).sort((o,c)=>a[o.s.state]-a[c.s.state]||(o.s.state==="expiring"?o.s.days-c.s.days:0)||String(o.t.name||"").localeCompare(String(c.t.name||""))).map(({t:o})=>o)}export function countStates(e,t=Date.now()){const r={all:0,active:0,expiring:0,expired:0,disabled:0};for(const n of e||[])r.all++,r[tokenState(n,t).state]++;return r}const m={active:["Active","is-ok"],disabled:["Off",""],expired:["Expired","is-danger"],expiring:["","is-warn"]};export function tokenRow(e,t=Date.now()){const{state:r,days:n}=tokenState(e,t),[i,s]=m[r],a=e.expiresAt?r==="expired"?`expired ${ago(e.expiresAt,t)}`:`expires ${expiresIn(n)}`:"never expires";return{id:e.id,name:e.name||"(unnamed)",hint:`dcms_\u2026${e.tokenHint||"????"}`,project:e.project||"",meta:[scopesSummary(e.scopes),a,e.createdBy?`by ${e.createdBy}`:""].filter(Boolean).join(" \xB7 "),used:e.lastUsedAt?`used ${ago(e.lastUsedAt,t)}`:"never used",stateLabel:r==="expiring"?`Expires ${expiresIn(n)}`:i,stateCls:s,rowCls:r==="disabled"||r==="expired"?"is-off":""}}export function toLocalInput(e){const t=Date.parse(e);if(!Number.isFinite(t))return"";const r=new Date(t),n=i=>String(i).padStart(2,"0");return`${r.getFullYear()}-${n(r.getMonth()+1)}-${n(r.getDate())}T${n(r.getHours())}:${n(r.getMinutes())}`}export function checkToken(e,{isNew:t,projects:r=[],now:n=Date.now()}={}){const i=e||{},s={},a=String(i.name??"").trim();a||(s.name="A token needs a name - who or what will use it."),t&&!String(i.project||"")?s.project="Choose the project it works in.":t&&r.length&&!r.includes(i.project)&&(s.project="Choose one of your projects.");const{scopes:o,error:c}=parseScopes(i.scopes);c&&(s.scopes=c);let p=null;if(String(i.expires||"").trim()){const l=Date.parse(i.expires);Number.isFinite(l)?(p=new Date(l).toISOString(),t&&l<=n&&(s.expires="That is already past - a new token would be dead on arrival.")):s.expires="Not a date."}const u=t?{name:a,project:i.project,scopes:o||[],expiresAt:p}:{name:a,enabled:i.enabled!==!1,scopes:o||[],expiresAt:p};return{errors:s,payload:u}}
1
+ import{fmtDate as f}from"./dates.js";export const VERBS=["create","read","update","delete"],CONTENT_RESOURCES=["pages","menus","site","search"],FILTERS=["all","active","expiring","expired","disabled"],SOON_DAYS=14;const x=864e5,d=(e,n,r=`${n}s`)=>`${e} ${e===1?n:r}`;export function tokenState(e,n=Date.now()){const r=e?.expiresAt?Date.parse(e.expiresAt):NaN,t=Number.isFinite(r)?Math.ceil((r-n)/x):null;return Number.isFinite(r)&&r<=n?{state:"expired",days:t}:e?.enabled===!1?{state:"disabled",days:t}:t!==null&&t<=SOON_DAYS?{state:"expiring",days:t}:{state:"active",days:t}}export function expiresIn(e){return e==null?"":e<=0?"today":e===1?"tomorrow":`in ${e} days`}export function ago(e,n=Date.now()){const r=Date.parse(e);if(!Number.isFinite(r))return"";const t=Math.max(0,Math.round((n-r)/1e3));return t<60?"just now":t<3600?`${Math.round(t/60)} min ago`:t<86400?`${d(Math.round(t/3600),"hour")} ago`:t<86400*30?`${d(Math.round(t/86400),"day")} ago`:f(r)}export function scopesToText(e){return(Array.isArray(e)?e:[]).map(n=>n.content!==void 0?`@${n.content}`:Array.isArray(n.verbs)&&n.verbs.length?`${n.collection}: ${n.verbs.join(", ")}`:n.collection).join(`
2
+ `)}export function parseScopes(e){const n=[],r=new Set;for(const t of String(e??"").split(`
3
+ `)){const o=t.trim();if(!o)continue;if(o.startsWith("@")){if(o.includes(":"))return{error:`"${o}" - content scopes are read-only and take no verbs.`};const c=o.slice(1).trim().toLowerCase();if(!c)return{error:'"@" names no content resource.'};if(!CONTENT_RESOURCES.includes(c))return{error:`"@${c}" is not a content resource - use ${CONTENT_RESOURCES.map(p=>"@"+p).join(", ").replace(/, ([^,]*)$/," or $1")}.`};if(r.has("@"+c))return{error:`"@${c}" is listed twice.`};r.add("@"+c),n.push({content:c});continue}const s=o.indexOf(":"),a=(s<0?o:o.slice(0,s)).trim(),i=s<0?"":o.slice(s+1).trim();if(!a)return{error:`"${o}" names no collection.`};if(r.has(a))return{error:`"${a}" is listed twice.`};r.add(a);const l={collection:a};if(i){const c=i.split(",").map(u=>u.trim().toLowerCase()).filter(Boolean),p=c.find(u=>!VERBS.includes(u));if(p)return{error:`"${p}" is not a verb - use ${VERBS.join(", ")}.`};l.verbs=[...new Set(c)]}n.push(l)}return{scopes:n}}export function scopesSummary(e){if(!Array.isArray(e)||!e.length)return"everything in the project - its collections and the Content API";const n=e.filter(t=>t.content===void 0).map(t=>`${t.collection}: ${Array.isArray(t.verbs)&&t.verbs.length?t.verbs.join(", "):"all"}`),r=e.filter(t=>t.content!==void 0).map(t=>t.content);return[...n,...r.length?[`content: ${r.join(", ")}`]:[]].join(" \xB7 ")}export function arrangeTokens(e,{filter:n="all",project:r="",query:t=""}={},o=Date.now()){const s=String(t).trim().toLowerCase(),a={expired:0,expiring:1,active:2,disabled:3};return(e||[]).map(i=>({t:i,s:tokenState(i,o)})).filter(({s:i})=>n==="all"||i.state===n).filter(({t:i})=>!r||i.project===r).filter(({t:i})=>!s||[i.name,i.project,i.tokenHint,i.createdBy,scopesSummary(i.scopes)].some(l=>String(l||"").toLowerCase().includes(s))).sort((i,l)=>a[i.s.state]-a[l.s.state]||(i.s.state==="expiring"?i.s.days-l.s.days:0)||String(i.t.name||"").localeCompare(String(l.t.name||""))).map(({t:i})=>i)}export function countStates(e,n=Date.now()){const r={all:0,active:0,expiring:0,expired:0,disabled:0};for(const t of e||[])r.all++,r[tokenState(t,n).state]++;return r}const m={active:["Active","is-ok"],disabled:["Off",""],expired:["Expired","is-danger"],expiring:["","is-warn"]};export function tokenRow(e,n=Date.now()){const{state:r,days:t}=tokenState(e,n),[o,s]=m[r],a=e.expiresAt?r==="expired"?`expired ${ago(e.expiresAt,n)}`:`expires ${expiresIn(t)}`:"never expires";return{id:e.id,name:e.name||"(unnamed)",hint:`dcms_\u2026${e.tokenHint||"????"}`,project:e.project||"",meta:[scopesSummary(e.scopes),a,e.createdBy?`by ${e.createdBy}`:""].filter(Boolean).join(" \xB7 "),used:e.lastUsedAt?`used ${ago(e.lastUsedAt,n)}`:"never used",stateLabel:r==="expiring"?`Expires ${expiresIn(t)}`:o,stateCls:s,rowCls:r==="disabled"||r==="expired"?"is-off":""}}export function toLocalInput(e){const n=Date.parse(e);if(!Number.isFinite(n))return"";const r=new Date(n),t=o=>String(o).padStart(2,"0");return`${r.getFullYear()}-${t(r.getMonth()+1)}-${t(r.getDate())}T${t(r.getHours())}:${t(r.getMinutes())}`}export function checkToken(e,{isNew:n,projects:r=[],now:t=Date.now()}={}){const o=e||{},s={},a=String(o.name??"").trim();a||(s.name="A token needs a name - who or what will use it."),n&&!String(o.project||"")?s.project="Choose the project it works in.":n&&r.length&&!r.includes(o.project)&&(s.project="Choose one of your projects.");const{scopes:i,error:l}=parseScopes(o.scopes);l&&(s.scopes=l);let c=null;if(String(o.expires||"").trim()){const u=Date.parse(o.expires);Number.isFinite(u)?(c=new Date(u).toISOString(),n&&u<=t&&(s.expires="That is already past - a new token would be dead on arrival.")):s.expires="Not a date."}const p=n?{name:a,project:o.project,scopes:i||[],expiresAt:c}:{name:a,enabled:o.enabled!==!1,scopes:i||[],expiresAt:c};return{errors:s,payload:p}}
@@ -0,0 +1 @@
1
+ export const RESOURCES=["pages","menus","site","search"],MAX_LIMIT=200,DEFAULTS=Object.freeze({enabled:!1,access:"token",listable:!0,maxLimit:200,formats:["html"],resources:{pages:{enabled:!0},menus:{enabled:!0},site:{enabled:!0},search:{enabled:!0}}}),BASE_PATH="/api/content/v1";const l={pages:"resPages",menus:"resMenus",site:"resSite",search:"resSearch"};export function formFromSettings(s){const e=s&&typeof s=="object"?s:{},n=Array.isArray(e.formats)?e.formats:DEFAULTS.formats,t={enabled:e.enabled===!0,access:typeof e.access=="string"&&e.access.trim()?e.access.trim():DEFAULTS.access,listable:e.listable!==!1,maxLimit:String(Number.isFinite(Number(e.maxLimit))&&e.maxLimit!==""?e.maxLimit:DEFAULTS.maxLimit),fmtHtml:n.includes("html"),fmtMarkdown:n.includes("markdown")};for(const r of RESOURCES)t[l[r]]=e.resources?.[r]?.enabled!==!1;return t}export function payloadFromForm(s){const e={...formFromSettings(null),...s||{}},n=[e.fmtHtml&&"html",e.fmtMarkdown&&"markdown"].filter(Boolean),t=Math.trunc(Number(String(e.maxLimit??"").trim()));return{enabled:e.enabled===!0,access:String(e.access||"").trim()||DEFAULTS.access,resources:Object.fromEntries(RESOURCES.map(r=>[r,{enabled:e[l[r]]!==!1}])),formats:n.length?n:["html"],listable:e.listable!==!1,maxLimit:String(e.maxLimit??"").trim()===""||!Number.isFinite(t)?200:Math.min(200,Math.max(1,t))}}export const snapshot=s=>JSON.stringify(payloadFromForm(s));export function saveNotes(s){const e=[];!s?.fmtHtml&&!s?.fmtMarkdown&&e.push("With no format chosen, HTML is kept - a page needs one.");const n=String(s?.maxLimit??"").trim(),t=Number(n);return n&&(!Number.isFinite(t)||t<1||t>200||!Number.isInteger(t))&&e.push(`Page size limit is saved as ${payloadFromForm(s).maxLimit} (1-200).`),e}export function checkForm(s,e=[]){const n=String(s?.access||"").trim();return n==="public"||n==="token"||!e.length?{}:e.some(t=>t.name===n)?{}:{access:`"${n}" is not a role on this site - pick one that exists.`}}export function accessText(s,e=[]){if(s==="public")return"Public";if(s==="token")return"API token";const n=e.find(t=>t.name===s);return n?n.level===0?`${n.label} only`:`${n.label} and above`:`Role: ${s}`}export function liveChips(s,e=null,n=[]){const t=payloadFromForm(formFromSettings(s));if(!t.enabled)return[{text:"Off - every address answers 404",tone:"warn"}];const r=[{text:"On",tone:"ok"},{text:accessText(t.access,n),tone:""}];return r.push({text:t.formats.map(o=>o==="html"?"HTML":"Markdown").join(" + "),tone:""}),t.listable||r.push({text:"No page list",tone:""}),e&&Number.isFinite(e.served)&&r.push({text:`${e.served} page${e.served===1?"":"s"} served`,tone:""}),r}export function endpointRows(s,e){const n=e||DEFAULTS,t=o=>n.resources?.[o]?.enabled!==!1,r=(o,i,a,c)=>({method:"GET",path:o,url:`${s}${o}`,label:i,state:n.enabled&&a?"on":"off",note:n.enabled?a?"":c:"The Content API is off"});return[r(`${BASE_PATH}/pages`,"Page list - summaries, paged",t("pages")&&n.listable!==!1,t("pages")?"Page list is off - single pages only":"Pages are off"),r(`${BASE_PATH}/pages/about`,"One page by its path",t("pages"),"Pages are off"),r(`${BASE_PATH}/menus/navbar`,"The menu at a location",t("menus"),"Menus are off"),r(`${BASE_PATH}/site`,"Site title, brand, footer, social",t("site"),"Site settings are off"),r(`${BASE_PATH}/search?q=hello`,"Site search",t("search"),"Search is off")]}export function normalisePath(s){const e=String(s??"").trim();return!e||e==="/"?"/":"/"+e.replace(/^\/+/,"").replace(/\/+$/,"")}export function curlExample(s,e,n,t="html"){const r=n||DEFAULTS,o=normalisePath(e),i=t&&t!=="html"?`?format=${t}`:"",a=`${s}${BASE_PATH}/pages${o==="/"?"/":o}${i}`;return r.access==="public"?`curl '${a}'`:`curl -H 'Authorization: Bearer ${r.access==="token"?"dcms_\u2026":"<access JWT>"}' '${a}'`}export function previewRow(s={}){const e=Number(s.status)||0,n=e===200?"ok":"bad",t=s.body?.requires;let r="";e===200&&(r=t?.domma&&t.components?.length?`Needs Domma: ${t.components.join(", ")}`:"Plain HTML - no Domma needed");const o={403:"Forbidden - the page is role-gated, and callers are anonymous",404:"Not found - missing, unpublished, in a disabled project, or marked api: false"}[e]||"";return{statusText:String(e||"-"),tone:n,requiresText:r,why:o,offeredNote:s.offered===!1?"This site does not offer that format yet - callers would get 400.":""}}
@@ -1,2 +1,2 @@
1
1
  const d=["separator","spacer"];export const isStructural=t=>d.includes(t?.type);export function siblingsOf(t,n){const e=t.find(i=>i.id===n);return e?t.filter(i=>i.parentId===e.parentId):[]}export function moveRow(t,n,e){const i=siblingsOf(t,n),r=i.findIndex(a=>a.id===n),s=r+(e<0?-1:1);if(r<0||s<0||s>=i.length)return!1;i[r].touched=!0;const o=t.indexOf(i[r]),l=t.indexOf(i[s]);return[t[o],t[l]]=[t[l],t[o]],!0}export function indentRow(t,n){const e=siblingsOf(t,n),i=e.findIndex(r=>r.id===n);return i<=0||isStructural(e[i-1])?!1:(e[i].parentId=e[i-1].id,e[i].touched=!0,!0)}export function outdentRow(t,n){const e=t.find(r=>r.id===n);if(!e||e.parentId==null)return!1;const i=t.find(r=>r.id===e.parentId);return e.parentId=i?i.parentId:null,e.touched=!0,!0}export function newRow(t,n,e=null){return t==="separator"?{id:n,parentId:e,type:"separator"}:t==="spacer"?{id:n,parentId:e,type:"spacer",size:"md"}:{id:n,parentId:e,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}}export function insertBelow(t,n,e,i){const r=t.findIndex(o=>o.id===n);if(r===-1)return null;const s=newRow(e,i,t[r].parentId);return t.splice(r+1,0,s),s}export function addChild(t,n,e){const i=t.find(s=>s.id===n);if(!i||isStructural(i))return null;const r=newRow("item",e,i.id);return t.push(r),i.touched=!0,r}export function duplicateRow(t,n,e){const i=t.find(a=>a.id===n);if(!i||i.tool)return null;const r=new Map,s=[],o=(a,c)=>{if(a.tool)return;const u={...g(a),id:e(),parentId:c};delete u.tool,delete u.locked,u.touched=!0,r.set(a.id,u.id),s.push(u);for(const f of t.filter(p=>p.parentId===a.id))o(f,u.id)};if(o(i,i.parentId),!s.length)return null;s[0].text&&(s[0].text=`${s[0].text} (copy)`);const l=t.indexOf(i);return t.splice(l+1,0,s[0]),t.push(...s.slice(1)),s[0]}function g(t){return JSON.parse(JSON.stringify(t))}export function bindingFrom({slot:t="",projects:n=[],pagesText:e=""}={}){return t?{slot:t,projects:[...n],pages:String(e).split(`
2
- `).map(i=>i.trim()).filter(Boolean)}:null}export function badPagePatterns(t=[]){return t.filter(n=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(n))}const m=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"];export function menuFromForm(t={},n={},e=null,i=[],r={}){const s=Object.fromEntries(m.filter(a=>t[a]).map(a=>[a,t[a]])),o={};if(t.position==="floating"){t.floatAnchor&&(o.anchor=t.floatAnchor);for(const[a,c]of[["offsetX",t.floatOffsetX],["offsetY",t.floatOffsetY],["width",t.floatWidth],["radius",t.floatRadius],["opacity",t.floatOpacity]])c!==""&&c!=null&&Number.isFinite(Number(c))&&(o[a]=Number(c));t.floatShadow&&(o.shadow=t.floatShadow),t.floatAccent&&(o.accent=t.floatAccent),t.floatTitle&&(o.title=String(t.floatTitle).trim()),t.floatCollapsible&&(o.collapsible=!0),t.floatCollapsible&&t.floatCollapsed&&(o.collapsed=!0),t.floatMovable&&(o.movable=!0),t.floatResizable&&(o.resizable=!0)}const l={};return t.highlightHover&&(l.hover=t.highlightHover),t.highlightActive&&(l.active=t.highlightActive),t.highlightAccent&&(l.accent=t.highlightAccent),{slug:String(n.slug||"").trim(),name:String(n.name||"").trim(),description:String(n.description||"").trim(),...t.variant&&{variant:t.variant},...t.position&&{position:t.position},...t.orientation&&{orientation:t.orientation},...t.orientation==="vertical"&&t.side&&{side:t.side},...t.orientation!=="vertical"&&t.align&&{align:t.align},...Object.keys(o).length&&{float:o},...Object.keys(l).length&&{highlight:l},...t.appearOnHover&&{appearOnHover:!0},...Object.keys(s).length&&{style:s},binding:e,items:i,meta:{...r||{},project:n.project||null}}}export function menuProblems(t){const n=[];/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(t.slug||"")||n.push("The slug must be lowercase letters, numbers and hyphens, no hyphen at either end (Meta tab)."),t.name||n.push("The menu needs a name (Meta tab).");const e=countUnlabelled(t.items);if(e&&n.push(`${e} item${e===1?" has":"s have"} no label - every item needs one (Items tab).`),t.binding){const i=badPagePatterns(t.binding.pages);i.length&&n.push(`Not valid page patterns: ${i.join(", ")} (Binding tab).`)}return n}export function countUnlabelled(t=[]){let n=0;for(const e of t||[])!isStructural(e)&&!(e.ref===!0&&e.url)&&!String(e.text||"").trim()&&n++,n+=countUnlabelled(e.items);return n}export const snapshot=t=>JSON.stringify(t);export function depthOfItems(t){if(!Array.isArray(t)||!t.length)return 0;let n=0;for(const e of t){const i=Array.isArray(e.items)&&e.items.length?depthOfItems(e.items):0;i>n&&(n=i)}return n+1}export function locationRows(t=[],n={},e={},i=[],r={}){const s=Object.fromEntries(i.map(o=>[o.slug,o.name||o.slug]));return t.map(o=>{const l=n[o.slot]||"",a=l&&r[l]||0,c=!!l&&o.maxDepth!=null&&a>o.maxDepth,u=o.slot==="overlay";return{slot:o.slot,label:o.label||o.slot,source:o.source||"",description:o.description||"",menu:l,menuName:l?s[l]||`${l} (missing)`:"",missing:!!l&&!s[l],tooDeep:c,depthText:c?`Nested ${a} deep - this slot shows ${o.maxDepth}`:"",empty:!l,emptyText:l?"":u?"Optional":"Nothing mapped",changed:(n[o.slot]||"")!==(e[o.slot]||"")}})}export function locationsPayload(t={},n=[]){const e=new Set(n.map(i=>i.slot));return Object.fromEntries(Object.entries(t).filter(([i,r])=>r&&(!n.length||e.has(i))))}export function locationsSnapshot(t={}){return JSON.stringify(Object.keys(t).filter(n=>t[n]).sort().map(n=>[n,t[n]]))}
2
+ `).map(i=>i.trim()).filter(Boolean)}:null}export function badPagePatterns(t=[]){return t.filter(n=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(n))}const m=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"];export function menuFromForm(t={},n={},e=null,i=[],r={}){const s=Object.fromEntries(m.filter(a=>t[a]).map(a=>[a,t[a]])),o={};if(t.position==="floating"){t.floatAnchor&&(o.anchor=t.floatAnchor);for(const[a,c]of[["offsetX",t.floatOffsetX],["offsetY",t.floatOffsetY],["width",t.floatWidth],["radius",t.floatRadius],["opacity",t.floatOpacity]])c!==""&&c!=null&&Number.isFinite(Number(c))&&(o[a]=Number(c));t.floatShadow&&(o.shadow=t.floatShadow),t.floatAccent&&(o.accent=t.floatAccent),t.floatTitle&&(o.title=String(t.floatTitle).trim()),t.floatCollapsible&&(o.collapsible=!0),t.floatCollapsible&&t.floatCollapsed&&(o.collapsed=!0),t.floatMovable&&(o.movable=!0),t.floatResizable&&(o.resizable=!0)}const l={};return t.highlightHover&&(l.hover=t.highlightHover),t.highlightActive&&(l.active=t.highlightActive),t.highlightAccent&&(l.accent=t.highlightAccent),{slug:String(n.slug||"").trim(),name:String(n.name||"").trim(),description:String(n.description||"").trim(),...t.variant&&{variant:t.variant},...t.position&&{position:t.position},...t.orientation&&{orientation:t.orientation},...t.orientation==="vertical"&&t.side&&{side:t.side},...t.orientation!=="vertical"&&t.align&&{align:t.align},...Object.keys(o).length&&{float:o},...Object.keys(l).length&&{highlight:l},...t.appearOnHover&&{appearOnHover:!0},...t.class&&String(t.class).trim()&&{class:String(t.class).trim()},...Object.keys(s).length&&{style:s},binding:e,items:i,meta:{...r||{},project:n.project||null}}}export function menuProblems(t){const n=[];/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(t.slug||"")||n.push("The slug must be lowercase letters, numbers and hyphens, no hyphen at either end (Meta tab)."),t.name||n.push("The menu needs a name (Meta tab).");const e=countUnlabelled(t.items);if(e&&n.push(`${e} item${e===1?" has":"s have"} no label - every item needs one (Items tab).`),t.binding){const i=badPagePatterns(t.binding.pages);i.length&&n.push(`Not valid page patterns: ${i.join(", ")} (Binding tab).`)}return n}export function countUnlabelled(t=[]){let n=0;for(const e of t||[])!isStructural(e)&&!(e.ref===!0&&e.url)&&!String(e.text||"").trim()&&n++,n+=countUnlabelled(e.items);return n}export const snapshot=t=>JSON.stringify(t);export function depthOfItems(t){if(!Array.isArray(t)||!t.length)return 0;let n=0;for(const e of t){const i=Array.isArray(e.items)&&e.items.length?depthOfItems(e.items):0;i>n&&(n=i)}return n+1}export function locationRows(t=[],n={},e={},i=[],r={}){const s=Object.fromEntries(i.map(o=>[o.slug,o.name||o.slug]));return t.map(o=>{const l=n[o.slot]||"",a=l&&r[l]||0,c=!!l&&o.maxDepth!=null&&a>o.maxDepth,u=o.slot==="overlay";return{slot:o.slot,label:o.label||o.slot,source:o.source||"",description:o.description||"",menu:l,menuName:l?s[l]||`${l} (missing)`:"",missing:!!l&&!s[l],tooDeep:c,depthText:c?`Nested ${a} deep - this slot shows ${o.maxDepth}`:"",empty:!l,emptyText:l?"":u?"Optional":"Nothing mapped",changed:(n[o.slot]||"")!==(e[o.slot]||"")}})}export function locationsPayload(t={},n=[]){const e=new Set(n.map(i=>i.slot));return Object.fromEntries(Object.entries(t).filter(([i,r])=>r&&(!n.length||e.has(i))))}export function locationsSnapshot(t={}){return JSON.stringify(Object.keys(t).filter(n=>t[n]).sort().map(n=>[n,t[n]]))}
@@ -1 +1 @@
1
- let d=1;export const uid=()=>"i_"+d++;export function flatten(c,l=null,e=[]){for(const i of c||[]){const o=uid();if(i&&i.type==="separator"){e.push({id:o,parentId:l,type:"separator"});continue}if(i&&i.type==="spacer"){e.push({id:o,parentId:l,type:"spacer",size:i.size||"md"});continue}e.push({id:o,parentId:l,text:i.text||"",url:i.url||"",icon:i.icon||"",visibility:i.visibility||"",permission:i.permission||"",hidden:!!i.hidden,bundled:!!i.bundled,badge:i.badge?{...i.badge}:null,pill:i.pill?{...i.pill}:null,colour:i.colour||"",highlight:i.highlight?{...i.highlight}:null,key:i.key||"",ref:!!i.ref,tool:i.$tool||null,touched:!1,synthesised:!!i.synthesised}),Array.isArray(i.items)&&i.items.length&&flatten(i.items,o,e)}return e}export function nest(c){const l=new Map;for(const t of c)l.has(t.parentId)||l.set(t.parentId,[]),l.get(t.parentId).push(t);function e(t){const n=[];let a=!1;const g=l.get(t)||[];for(let h=g.length-1;h>=0;h--){const s=g[h].tool?o(g[h],a):i(g[h]);s&&(n.unshift(s),a=!0)}return n}function i(t){if(t.type==="separator")return{type:"separator"};if(t.type==="spacer")return{type:"spacer",...t.size&&t.size!=="md"&&{size:t.size}};const n=e(t.id);return t.synthesised&&!n.length?null:{text:(t.text||"").trim(),url:(t.url||"").trim(),...t.icon&&{icon:t.icon.trim()},...t.visibility&&{visibility:t.visibility},...t.permission&&{permission:t.permission},...t.hidden&&{hidden:!0},...t.bundled&&{bundled:!0},...t.badge&&(t.badge.text||t.badge.countFrom)?{badge:{...t.badge.text&&{text:t.badge.text},...t.badge.countFrom&&{countFrom:t.badge.countFrom},...t.badge.variant&&{variant:t.badge.variant}}}:{},...t.pill&&t.pill.variant?{pill:{style:t.pill.style||"filled",variant:t.pill.variant}}:{},...t.colour&&{colour:t.colour},...t.key&&{key:t.key},...t.ref&&{ref:!0},...t.highlight&&(t.highlight.hover||t.highlight.active||t.highlight.accent)?{highlight:{...t.highlight.hover&&{hover:t.highlight.hover},...t.highlight.active&&{active:t.highlight.active},...t.highlight.accent&&{accent:t.highlight.accent}}}:{},...n.length&&{items:n}}}function o(t,n=!1){const a=e(t.id);if(!t.tool.persisted&&!t.touched&&!a.length&&!n)return null;const g=t.tool.defaults,h=(t.text||"").trim(),s=(t.icon||"").trim();return{url:(t.url||"").trim(),ref:!0,...h&&{text:h},...s&&s!==g.icon&&{icon:s},...t.visibility&&{visibility:t.visibility},...t.hidden&&{hidden:!0},...t.badge&&(t.badge.text||t.badge.countFrom)?{badge:{...t.badge.text&&{text:t.badge.text},...t.badge.countFrom&&{countFrom:t.badge.countFrom},...t.badge.variant&&{variant:t.badge.variant}}}:{},...t.colour&&{colour:t.colour},...t.highlight&&(t.highlight.hover||t.highlight.active||t.highlight.accent)?{highlight:{...t.highlight.hover&&{hover:t.highlight.hover},...t.highlight.active&&{active:t.highlight.active},...t.highlight.accent&&{accent:t.highlight.accent}}}:{},...a.length&&{items:a}}}return e(null)}
1
+ let d=1;export const uid=()=>"i_"+d++;export function flatten(g,l=null,e=[]){for(const i of g||[]){const o=uid();if(i&&i.type==="separator"){e.push({id:o,parentId:l,type:"separator"});continue}if(i&&i.type==="spacer"){e.push({id:o,parentId:l,type:"spacer",size:i.size||"md"});continue}e.push({id:o,parentId:l,text:i.text||"",url:i.url||"",icon:i.icon||"",visibility:i.visibility||"",permission:i.permission||"",hidden:!!i.hidden,bundled:!!i.bundled,badge:i.badge?{...i.badge}:null,pill:i.pill?{...i.pill}:null,colour:i.colour||"",highlight:i.highlight?{...i.highlight}:null,class:i.class||"",key:i.key||"",ref:!!i.ref,tool:i.$tool||null,touched:!1,synthesised:!!i.synthesised}),Array.isArray(i.items)&&i.items.length&&flatten(i.items,o,e)}return e}export function nest(g){const l=new Map;for(const t of g)l.has(t.parentId)||l.set(t.parentId,[]),l.get(t.parentId).push(t);function e(t){const n=[];let s=!1;const a=l.get(t)||[];for(let h=a.length-1;h>=0;h--){const c=a[h].tool?o(a[h],s):i(a[h]);c&&(n.unshift(c),s=!0)}return n}function i(t){if(t.type==="separator")return{type:"separator"};if(t.type==="spacer")return{type:"spacer",...t.size&&t.size!=="md"&&{size:t.size}};const n=e(t.id);return t.synthesised&&!n.length?null:{text:(t.text||"").trim(),url:(t.url||"").trim(),...t.icon&&{icon:t.icon.trim()},...t.visibility&&{visibility:t.visibility},...t.permission&&{permission:t.permission},...t.hidden&&{hidden:!0},...t.bundled&&{bundled:!0},...t.badge&&(t.badge.text||t.badge.countFrom)?{badge:{...t.badge.text&&{text:t.badge.text},...t.badge.countFrom&&{countFrom:t.badge.countFrom},...t.badge.variant&&{variant:t.badge.variant}}}:{},...t.pill&&t.pill.variant?{pill:{style:t.pill.style||"filled",variant:t.pill.variant}}:{},...t.colour&&{colour:t.colour},...t.class&&t.class.trim()&&{class:t.class.trim()},...t.key&&{key:t.key},...t.ref&&{ref:!0},...t.highlight&&(t.highlight.hover||t.highlight.active||t.highlight.accent)?{highlight:{...t.highlight.hover&&{hover:t.highlight.hover},...t.highlight.active&&{active:t.highlight.active},...t.highlight.accent&&{accent:t.highlight.accent}}}:{},...n.length&&{items:n}}}function o(t,n=!1){const s=e(t.id);if(!t.tool.persisted&&!t.touched&&!s.length&&!n)return null;const a=t.tool.defaults,h=(t.text||"").trim(),c=(t.icon||"").trim();return{url:(t.url||"").trim(),ref:!0,...h&&{text:h},...c&&c!==a.icon&&{icon:c},...t.visibility&&{visibility:t.visibility},...t.hidden&&{hidden:!0},...t.badge&&(t.badge.text||t.badge.countFrom)?{badge:{...t.badge.text&&{text:t.badge.text},...t.badge.countFrom&&{countFrom:t.badge.countFrom},...t.badge.variant&&{variant:t.badge.variant}}}:{},...t.colour&&{colour:t.colour},...t.class&&t.class.trim()&&{class:t.class.trim()},...t.highlight&&(t.highlight.hover||t.highlight.active||t.highlight.accent)?{highlight:{...t.highlight.hover&&{hover:t.highlight.hover},...t.highlight.active&&{active:t.highlight.active},...t.highlight.accent&&{accent:t.highlight.accent}}}:{},...s.length&&{items:s}}}return e(null)}
@@ -1,4 +1,4 @@
1
- import{api as h}from"../api.js";import{isPathAllowed as J}from"./admin-scope.js";import{colourToCss as U,applyFloat as Y,applyHighlight as Q,itemHighlightAttrs as ee}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{placeTools as te,ensureLockedTools as ne,managePluginsItem as se,stripItemByUrl as ie,insertFoldersBeforeSystem as ae,pruneEmptyToolsFolder as oe,sidebarStateKey as re,canAny as B,filterByPermission as le,MANAGE_PLUGINS_URL as z}from"./sidebar-grouping.js";import{arrangeButtonMarkup as ce,canArrange as de,startArrange as ue}from"./sidebar-arrange.js";import{collapseButtonMarkup as me,wireSidebarSize as fe,COLLAPSED_WIDTH as ge}from"./sidebar-size.js";function p(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const pe=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];async function he(e){if(!B(e,"projects"))return[];let s=[];try{s=await h.projects.listWithCounts()}catch{return[]}if(!Array.isArray(s))return[];const i=[{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(t=>{const d="#/projects/"+encodeURIComponent(t.slug),c=t.counts||{},o=[{text:"Overview",url:d,icon:t.icon||"folder"}];for(const r of i){const l=Number(c[r.key])||0;l<=0||o.push({text:`${r.text} (${l})`,url:d+r.path,icon:r.icon})}o.push({text:"Settings",url:d+"/settings",icon:"settings"});const m=t.enabled===!1;return{text:t.name||t.slug,key:`project.${t.slug}`,icon:t.icon||"folder",items:o,...m&&{colour:"neutral",badge:{text:"off"}}}})}function be(e,s){for(const i of e)if(!i.hidden&&(i.key==="projects"||!i.key&&(i.text||"").toLowerCase()==="projects")){i.items=Array.isArray(i.items)?i.items.slice():[],i.items.push(...s);return}}function O(e,s){const i=D.includes(s.tone)?s.tone:"info",t=s.label||String(s.count);s.details?E.set(e,s.details):E.delete(e);const n=!s.inventory&&A.some(d=>d.url===e&&!d.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${i}" data-count-for="${p(e)}"${n} data-badge-label="${p(t)}" aria-label="${p(t)}">${p(String(s.count))}</span>`}function W(e){for(const s of e.querySelectorAll("details")){const i=s.querySelector(":scope > summary");if(!i)continue;let t=0,n=!1;const d=[];for(const l of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const u=Number(l.textContent)||0;u&&(t+=u,n||(n=l.classList.contains("sidebar-badge--danger")),d.push({text:l.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:l.dataset.badgeLabel,tone:l.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const c=`folder:${s.dataset.stateKey||i.textContent.trim()}`;let o=i.querySelector(":scope > .sidebar-badge--sum");if(!t){o?.remove(),E.delete(c);continue}o||(o=document.createElement("span"),i.querySelector(".sidebar-text")?.after(o));const m=i.querySelector(".sidebar-text")?.textContent.trim()||"This folder",r=d.map(l=>`${l.text}: ${l.meta}`).join(" \xB7 ");o.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${n?"danger":"info"}`,o.dataset.countFor=c,o.dataset.badgeLabel=r,o.setAttribute("aria-label",r),o.textContent=String(t),E.set(c,{title:m,items:d})}}const E=new Map;function ye(e,s){if(!e.url||!s)return"";const i=s[e.url];return!i||!(i.count>0)&&!i.showZero?"":O(e.url,i)}const D=["info","attention","danger","live"],ve={"#/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 _(e,{fallbackLabel:s="",tone:i="info"}={}){const t=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(t)||t<0)return null;const n=typeof e?.label=="string"&&e.label||(s?`${t} - ${s}`:String(t)),d={count:Math.round(t),label:n,tone:D.includes(e?.tone)?e.tone:i};return e?.details&&typeof e.details=="object"&&(d.details=e.details),e?.inventory===!0&&(d.inventory=!0),e?.showZero===!0&&(d.showZero=!0),d}function G(e,s,i=null){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="spacer"){const b=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${p(b)}" aria-hidden="true"></div>`}const t=String(e.text||"?").trim().charAt(0).toUpperCase()||"?",n=e.icon?`<span data-icon="${p(e.icon)}"></span>`:`<span class="sidebar-initial" aria-hidden="true">${p(t)}</span>`,d=p(e.text||""),c=e.url?p(e.url):"",o=Array.isArray(e.items)&&e.items.length>0,m=e.colour?U(e.colour):"",r=ee(e),l=[r.hover&&` data-menu-item-hover="${p(r.hover)}"`,r.active&&` data-menu-item-active="${p(r.active)}"`].filter(Boolean).join(""),u=[m&&`color:${p(m)}`,r.accent&&`--dm-menu-highlight:${p(r.accent)}`].filter(Boolean).join(";"),y=u?` style="${u}"`:"";let v="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const b=e.badge.variant?U(e.badge.variant):"";v=`<span class="dm-menu-badge"${b?` style="background:${p(b)};color:var(--dm-text-inverse,#fff)"`:""}>${p(String(e.badge.text))}</span>`}if(o){const b=re(e,i),C=S.get(b)!==!1,L=e.items.map(j=>G(j,s,b)).join("");return`<details data-state-key="${p(b)}"${C?" open":""}>
1
+ import{api as h}from"../api.js";import{isPathAllowed as J}from"./admin-scope.js";import{colourToCss as U,applyFloat as Y,applyHighlight as Q,itemHighlightAttrs as ee}from"/public/js/menu-decor.mjs?v=20261009-menu-classes";import{placeTools as te,ensureLockedTools as ne,managePluginsItem as se,stripItemByUrl as ie,insertFoldersBeforeSystem as ae,pruneEmptyToolsFolder as oe,sidebarStateKey as re,canAny as B,filterByPermission as le,MANAGE_PLUGINS_URL as z}from"./sidebar-grouping.js";import{arrangeButtonMarkup as ce,canArrange as de,startArrange as ue}from"./sidebar-arrange.js";import{collapseButtonMarkup as me,wireSidebarSize as fe,COLLAPSED_WIDTH as ge}from"./sidebar-size.js";function p(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const pe=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];async function he(e){if(!B(e,"projects"))return[];let s=[];try{s=await h.projects.listWithCounts()}catch{return[]}if(!Array.isArray(s))return[];const i=[{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(t=>{const d="#/projects/"+encodeURIComponent(t.slug),c=t.counts||{},o=[{text:"Overview",url:d,icon:t.icon||"folder"}];for(const r of i){const l=Number(c[r.key])||0;l<=0||o.push({text:`${r.text} (${l})`,url:d+r.path,icon:r.icon})}o.push({text:"Settings",url:d+"/settings",icon:"settings"});const m=t.enabled===!1;return{text:t.name||t.slug,key:`project.${t.slug}`,icon:t.icon||"folder",items:o,...m&&{colour:"neutral",badge:{text:"off"}}}})}function be(e,s){for(const i of e)if(!i.hidden&&(i.key==="projects"||!i.key&&(i.text||"").toLowerCase()==="projects")){i.items=Array.isArray(i.items)?i.items.slice():[],i.items.push(...s);return}}function O(e,s){const i=D.includes(s.tone)?s.tone:"info",t=s.label||String(s.count);s.details?E.set(e,s.details):E.delete(e);const n=!s.inventory&&A.some(d=>d.url===e&&!d.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${i}" data-count-for="${p(e)}"${n} data-badge-label="${p(t)}" aria-label="${p(t)}">${p(String(s.count))}</span>`}function W(e){for(const s of e.querySelectorAll("details")){const i=s.querySelector(":scope > summary");if(!i)continue;let t=0,n=!1;const d=[];for(const l of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const u=Number(l.textContent)||0;u&&(t+=u,n||(n=l.classList.contains("sidebar-badge--danger")),d.push({text:l.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:l.dataset.badgeLabel,tone:l.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const c=`folder:${s.dataset.stateKey||i.textContent.trim()}`;let o=i.querySelector(":scope > .sidebar-badge--sum");if(!t){o?.remove(),E.delete(c);continue}o||(o=document.createElement("span"),i.querySelector(".sidebar-text")?.after(o));const m=i.querySelector(".sidebar-text")?.textContent.trim()||"This folder",r=d.map(l=>`${l.text}: ${l.meta}`).join(" \xB7 ");o.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${n?"danger":"info"}`,o.dataset.countFor=c,o.dataset.badgeLabel=r,o.setAttribute("aria-label",r),o.textContent=String(t),E.set(c,{title:m,items:d})}}const E=new Map;function ye(e,s){if(!e.url||!s)return"";const i=s[e.url];return!i||!(i.count>0)&&!i.showZero?"":O(e.url,i)}const D=["info","attention","danger","live"],ve={"#/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 _(e,{fallbackLabel:s="",tone:i="info"}={}){const t=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(t)||t<0)return null;const n=typeof e?.label=="string"&&e.label||(s?`${t} - ${s}`:String(t)),d={count:Math.round(t),label:n,tone:D.includes(e?.tone)?e.tone:i};return e?.details&&typeof e.details=="object"&&(d.details=e.details),e?.inventory===!0&&(d.inventory=!0),e?.showZero===!0&&(d.showZero=!0),d}function G(e,s,i=null){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="spacer"){const b=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${p(b)}" aria-hidden="true"></div>`}const t=String(e.text||"?").trim().charAt(0).toUpperCase()||"?",n=e.icon?`<span data-icon="${p(e.icon)}"></span>`:`<span class="sidebar-initial" aria-hidden="true">${p(t)}</span>`,d=p(e.text||""),c=e.url?p(e.url):"",o=Array.isArray(e.items)&&e.items.length>0,m=e.colour?U(e.colour):"",r=ee(e),l=[r.hover&&` data-menu-item-hover="${p(r.hover)}"`,r.active&&` data-menu-item-active="${p(r.active)}"`].filter(Boolean).join(""),u=[m&&`color:${p(m)}`,r.accent&&`--dm-menu-highlight:${p(r.accent)}`].filter(Boolean).join(";"),y=u?` style="${u}"`:"";let v="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const b=e.badge.variant?U(e.badge.variant):"";v=`<span class="dm-menu-badge"${b?` style="background:${p(b)};color:var(--dm-text-inverse,#fff)"`:""}>${p(String(e.badge.text))}</span>`}if(o){const b=re(e,i),C=S.get(b)!==!1,L=e.items.map(j=>G(j,s,b)).join("");return`<details data-state-key="${p(b)}"${C?" open":""}>
2
2
  <summary${y}>${n} <span class="sidebar-text">${d}</span>${v}</summary>
3
3
  <div class="sidebar-children">${L}</div>
4
4
  </details>`}return`<a href="${c}" class="sidebar-link" data-url="${c}"${y}${l}>${n} <span class="sidebar-text">${d}</span>${ye(e,s)}${v}</a>`}async function xe(e=[],s=[],i=null){const t=g=>g.then(a=>Array.isArray(a)?a.length:Array.isArray(a?.entries)?a.entries.length:0).catch(()=>0);A=Ce(e,i);const n=new Set(A.map(g=>g.url)),d={pages:"#/pages",media:"#/media",menus:"#/menus","context-menus":"#/context-menus",theme:"#/theme",collections:"#/collections",forms:"#/forms",views:"#/views",actions:"#/actions",blocks:"#/blocks",components:"#/components",users:"#/users",plugins:"#/plugins",notifications:"#/system/notifications"},c=(g,a)=>B(s,g)&&!n.has(d[g])?a():Promise.resolve(0),[o,m,r,l,u,y,v,b,C,L,j,F,N,P]=await Promise.all([c("pages",()=>t(h.pages.list())),c("media",()=>t(h.media.list())),c("menus",()=>t(h.menus.list())),c("context-menus",()=>t(h.contextMenus.list())),c("theme",()=>h.theme.get().then(g=>(g?.overrides||[]).length).catch(()=>0)),c("collections",()=>t(h.collections.list())),c("forms",()=>t(h.forms.list())),c("views",()=>t(h.views.list())),c("actions",()=>t(h.actions.list())),c("blocks",()=>t(h.blocks.list())),c("components",()=>t(h.components.list())),c("users",()=>t(h.users.list())),c("plugins",()=>t(h.plugins.list())),c("notifications",()=>h.system?.notifications?.unreadCount?.().then(g=>g?.count??0).catch(()=>0)??Promise.resolve(0))]),q={};await Promise.all(A.map(async g=>{const a=await K(g);a&&(q[g.url]=a)}));const T={"#/pages":o,"#/media":m,"#/menus":r,"#/context-menus":l,"#/theme":u,"#/collections":y,"#/forms":v,"#/views":b,"#/actions":C,"#/blocks":L,"#/components":j,"#/users":F,"#/plugins":N,"#/system/notifications":P},k={};for(const[g,a]of Object.entries(T)){if(n.has(g))continue;const[x,f]=ve[g]??["item","items"],w=_(a);w&&(k[g]={...w,label:`${w.count} ${w.count===1?x:f}`})}return{...k,...q}}let A=[];const $e="dm:sidebar-count",Se=6e4;function Ce(e=[],s=null){const i=[],t=n=>{if(n&&!(s&&typeof n.url=="string"&&n.url.startsWith("#/")&&!J(s,n.url.slice(1)))){n.url&&typeof n.countUrl=="string"&&n.countUrl&&i.push({url:n.url,countUrl:n.countUrl,text:n.text||"",countLabel:n.countLabel||"",inventory:n.inventory===!0});for(const d of[...n.children||[],...n.items||[]])t(d)}};for(const n of e??[])t(n?.item??n);return i}async function K(e){try{const s=new Date,i=`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(s.getDate()).padStart(2,"0")}`,t=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${i}`;return _(await H.get(t),{fallbackLabel:e.countLabel||e.text})}catch{return null}}function V(e,s,i){const t=[...e.querySelectorAll("a.sidebar-link")].find(d=>d.dataset.url===s);if(!t)return;const n=t.querySelector(".sidebar-badge[data-count-for]");if(!i||!(i.count>0)&&!i.showZero)n?.remove();else{const d=document.createRange();d.selectNodeContents(t);const c=d.createContextualFragment(O(s,i)).firstElementChild;n?n.replaceWith(c):t.querySelector(".sidebar-text")?.after(c)}R.set(s,i),W(e)}const R=new Map;async function I(e,s=null){const i=s?A.filter(t=>t.url===s):A;await Promise.all(i.map(async t=>{const n=await K(t),d=R.get(t.url);JSON.stringify(d??null)!==JSON.stringify(n)&&V(e,t.url,n)}))}function we(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener($e,s=>{const i=s.detail||{};if(i.url)if(i.count!==void 0){const t=A.find(n=>n.url===i.url);V(e,i.url,_(i,{fallbackLabel:t?.countLabel||t?.text||""}))}else I(e,i.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&I(e)},Se),document.addEventListener("visibilitychange",()=>{document.hidden||I(e)}),ke(e))}function Z(e,s,i){const t=(c,o,m)=>{const r=document.createElement(c);return o&&(r.className=o),m!=null&&(r.textContent=String(m)),r};if(!i){e.textContent=s;return}e.appendChild(t("strong","dm-badge-pop-title",i.title||s)),(i.rows?.length||!i.items?.length)&&e.appendChild(t("p","dm-badge-pop-sub",s));const n=Array.isArray(i.rows)?i.rows.slice(0,6):[];if(n.length){const c=t("dl","dm-badge-pop-rows");for(const o of n)c.appendChild(t("dt",null,o.label)),c.appendChild(t("dd",o.tone==="danger"?"is-danger":null,o.value));e.appendChild(c)}const d=Array.isArray(i.items)?i.items.slice(0,6):[];if(d.length){const c=t("ul","dm-badge-pop-items");for(const o of d){const m=t("li",o.tone==="danger"?"is-danger":null);m.appendChild(t("span","dm-badge-pop-text",o.text)),o.meta&&m.appendChild(t("span","dm-badge-pop-meta",o.meta)),c.appendChild(m)}e.appendChild(c)}else i.empty&&e.appendChild(t("p","dm-badge-pop-empty",i.empty))}function ke(e){let s=null,i=null;const t=()=>e.classList.contains("is-collapsed"),n=()=>{clearTimeout(i),s?.remove(),s=null},d=(r,l)=>{n(),i=setTimeout(()=>{s=document.createElement("div"),s.className="dm-help-pop dm-badge-pop",s.setAttribute("role","tooltip"),l(s),document.body.appendChild(s);const u=r.getBoundingClientRect(),y=s.getBoundingClientRect(),v=Math.min(u.right+10,innerWidth-y.width-8);s.style.left=`${Math.max(8,v)}px`,s.style.top=`${Math.max(8,Math.min(u.top+u.height/2-y.height/2,innerHeight-y.height-8))}px`},150)},c=r=>{const l=r?.dataset.badgeLabel;if(!l)return n();d(r,u=>Z(u,l,E.get(r.dataset.countFor)))},o=r=>{const l=r.querySelector(":scope > .sidebar-text")?.textContent.trim()||"",u=r.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");d(r,y=>{const v=u&&E.get(u.dataset.countFor);if(v)return Z(y,u.dataset.badgeLabel,{...v,title:v.title||l});if(!u){y.textContent=l;return}const b=document.createElement("strong");b.className="dm-badge-pop-title",b.textContent=l;const C=document.createElement("p");C.className="dm-badge-pop-sub",C.textContent=u.dataset.badgeLabel,y.append(b,C)})},m=r=>{const l=r.closest?.("a.sidebar-link, summary");return l&&e.contains(l)?l:null};e.addEventListener("mouseover",r=>{if(t()){const u=m(r.target);u&&!u.contains(r.relatedTarget)&&o(u);return}const l=r.target.closest?.(".sidebar-badge[data-badge-label]");l&&c(l)}),e.addEventListener("mouseout",r=>{if(t()){const l=m(r.target);l&&!l.contains(r.relatedTarget)&&n();return}r.target.closest?.(".sidebar-badge[data-badge-label]")&&n()}),e.addEventListener("focusin",r=>{const l=r.target.closest?.("a, summary");if(t()&&l&&e.contains(l)&&l.querySelector(":scope > .sidebar-text"))return o(l);const u=l?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");u?c(u):n()}),e.addEventListener("focusout",n),e.addEventListener("click",n),window.addEventListener("scroll",n,!0),window.addEventListener("hashchange",n)}async function Ae(e=[]){if(!B(e,"settings"))return"Admin";try{const s=await h.settings.get();return s?.adminBrand?.title||s?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:s,adminScope:i=null}){const t=$(e).get(0);if(!t)return;let n,d=null,c=null,o=null,m=null,r=null;try{const a=await h.get("/sidebar/menu");n=Array.isArray(a?.items)?a.items:null,d=a?.variant||null,c=a?.position||null,o=a?.style||null,m=a?.float||null,r=a?.highlight||null}catch{n=null}(!n||!n.some(a=>a&&(!a.hidden||a.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),n=pe.slice());let l=[];try{l=await H.get("/api/sidebar/registered-items")||[]}catch{}n=ie(n,z),n=te(n,l.filter(a=>(a?.item??a)?.url!==z)),n=ne(n),n=ae(n,[se()]);const u=await he(s);u.length&&be(n,u),n=le(n,s,i),n=oe(n);const[y,v]=await Promise.all([xe(l.filter(a=>B(s,a?.item?.permission)),s,i).catch(()=>({})),Ae(s)]),b=d?` dm-admin-sidebar--${p(d)}`:"";function C(a,x="px"){if(a==null)return a;const f=String(a).trim();return/^\d+(\.\d+)?$/.test(f)?`${f}${x}`:f}let L="";if(o){const a=[];o.fontFamily&&a.push(`font-family: ${o.fontFamily}, sans-serif`),o.fontSize&&a.push(`font-size: ${C(o.fontSize)}`),o.fontWeight&&a.push(`font-weight: ${o.fontWeight}`),o.letterSpacing&&a.push(`letter-spacing: ${C(o.letterSpacing,"em")}`);const x=[];if(a.length&&x.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${a.join("; ")} }`),o.iconSize){const f=C(o.iconSize);x.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${f} !important; height: ${f} !important; }`)}x.length&&(L=`<style data-admin-sidebar-style>${x.join(" ")}</style>`)}const j=de(s)?ce():"",F=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span><span class="dm-admin-sidebar-title">${p(v)}</span>${j}${me()}</div>`,N=n.some(a=>a&&a.type==="spacer"&&a.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",P=`${L}<nav class="dm-admin-sidebar${b}${N}">${F}${n.map(a=>G(a,y)).join("")}</nav>`,q=document.createRange();q.selectNodeContents(t);const T=q.createContextualFragment(P);t.replaceChildren(T),Domma.icons.scan(t),t.querySelector(".dm-admin-sidebar-arrange")?.addEventListener("click",()=>ue(t));for(const a of A)R.set(a.url,y[a.url]??null);W(t),we(t),t.removeAttribute("data-menu-hover"),t.removeAttribute("data-menu-active"),t.style.removeProperty("--dm-menu-highlight"),Q(t,{highlight:r});const k=document.querySelector(".dashboard-wrapper");["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(a=>{t.style[a]=""}),t.classList.remove("dm-menu-floating"),k&&(k.style.paddingLeft="",k.style.paddingRight=""),t.__dmGutter=()=>{if(c!=="floating"||!k)return;const a=m&&m.anchor||"TL",x=Number.isFinite(Number(m?.offsetX))?Number(m.offsetX):16,f=t.classList.contains("is-collapsed")?ge:parseInt(t.style.width,10)||t.getBoundingClientRect().width||260,w=Math.round(f+x*2)+"px";a==="R"||a==="TR"||a==="BR"?k.style.paddingRight=w:k.style.paddingLeft=w},c==="floating"&&Y(t,{position:"floating",float:m||{}}),t.querySelectorAll("details").forEach(a=>{a.addEventListener("toggle",function(){const x=this.getAttribute("data-state-key");x&&S.set(x,this.open);const f=this.querySelector(":scope > .sidebar-children");if(f)if(this.open){const w=f.scrollHeight;f.style.maxHeight="0",requestAnimationFrame(()=>{f.style.maxHeight=w+"px",f.addEventListener("transitionend",function X(){f.style.maxHeight="none",f.removeEventListener("transitionend",X)})})}else{const w=f.scrollHeight;f.style.maxHeight=w+"px",requestAnimationFrame(()=>{f.style.maxHeight="0"})}})}),fe(t),t.dataset.routeWired||(t.dataset.routeWired="1",M.subscribe("router:afterChange",()=>t.__dmSetActive?.()));function g(){const a=location.hash||"#/";$(t).find(".sidebar-link").removeClass("active"),$(t).find(`.sidebar-link[data-url="${a}"]`).addClass("active")}g(),t.__dmSetActive=g}
@@ -74,7 +74,8 @@
74
74
  <hr style="margin-top:2rem;">
75
75
  <p class="text-muted" style="font-size:.9rem;">
76
76
  For an external, token-based surface over your collections, see also
77
- <a href="#/api-tokens">API Tokens</a> and the <a href="#/api-endpoints">API Builder</a>.
77
+ <a href="#/api-tokens">API Tokens</a> and the <a href="#/api-endpoints">API Builder</a>. For headless,
78
+ read-only access to your pages, menus and site settings, see the <a href="#/docs/api/content">Content API</a>.
78
79
  </p>
79
80
 
80
81
  </div>
@@ -2,9 +2,9 @@
2
2
  template (the router renders a templateUrl through _.render, eating any {{…}}). No page heading:
3
3
  the Tool banner above is the heading, and New token is a banner action. -->
4
4
  <div class="atx">
5
- <p class="atx-note">Project-scoped keys for the external collections API (<code>/api/v1/&lt;collection&gt;</code>) and API Builder
6
- endpoints set to token access.
7
- <span data-help-title="API tokens" data-help="A token works only on collections in its own project, and only where a collection's API access is set to Token. The token itself is shown once, when it is made - only a fingerprint is kept, so a lost token is revoked and a new one made. Scopes narrow a token to some collections and verbs; an expiry date retires it on its own."></span></p>
5
+ <p class="atx-note">Project-scoped keys for the external collections API (<code>/api/v1/&lt;collection&gt;</code>), API Builder
6
+ endpoints set to token access, and the Content API (<code>/api/content/v1</code>) when it is set to API tokens.
7
+ <span data-help-title="API tokens" data-help="A token works only on collections in its own project, and only where a collection's API access is set to Token. On the Content API it reads only its own project's pages (menus and site settings are site-wide). The token itself is shown once, when it is made - only a fingerprint is kept, so a lost token is revoked and a new one made. Scopes narrow a token to some collections and verbs, or to @pages, @menus, @site or @search; an expiry date retires it on its own."></span></p>
8
8
 
9
9
  <div class="atx-card">
10
10
  <div class="atx-bar">
@@ -0,0 +1,208 @@
1
+ <!-- System › Content API. Bound with M.applyBindings - see admin/js/views/content-api.js, which also
2
+ inserts the row templates (the router renders a templateUrl through _.render, eating any {{…}}).
3
+ No page heading: the Tool banner above is the heading; Save and API Tokens are banner actions. -->
4
+ <div class="cax">
5
+ <div class="cax-note dm-loading-slot" data-bind-hidden="!loading.value">Loading…</div>
6
+
7
+ <p class="cax-alert is-dirty" hidden data-bind-hidden="!dirty.value">
8
+ <span data-icon="edit-3" data-icon-size="16"></span>
9
+ <span>Unsaved changes - Save (Ctrl+S) to apply them. Leave and they are kept as a draft for next time.</span>
10
+ <span class="cax-spacer"></span>
11
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="discard">Discard</button>
12
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="saving.value || !!accessErr.value">Save</button></p>
13
+ <p class="cax-alert" hidden data-bind-hidden="!notesText.value">
14
+ <span data-icon="alert-triangle" data-icon-size="16"></span>
15
+ <span data-bind-text="notesText.value"></span></p>
16
+
17
+ <div class="tabs cax-tabs" hidden data-bind-hidden="loading.value">
18
+ <div class="tab-list">
19
+ <button type="button" class="tab-item active">Access</button>
20
+ <button type="button" class="tab-item">Endpoints</button>
21
+ <button type="button" class="tab-item">Try it</button>
22
+ <button type="button" class="tab-item">How it works</button>
23
+ </div>
24
+
25
+ <!-- Access -->
26
+ <div class="tab-panel active">
27
+ <div class="cax-card">
28
+ <div class="cax-card-title">Live now
29
+ <span data-help-title="Live now" data-help="What callers get at the moment - the saved settings, not what you are editing. Pages served counts the published pages an anonymous caller could open, less any marked api: false."></span></div>
30
+ <div class="cax-chips" data-each="chips.value key=text"></div>
31
+ </div>
32
+
33
+ <div class="cax-card">
34
+ <div class="cax-card-title">The Content API</div>
35
+ <div class="cax-setting">
36
+ <label class="form-label" for="cax-enabled">Enable the Content API
37
+ <span data-help="Off answers 404 on every /api/content/v1 address. The public site is not affected either way."></span></label>
38
+ <input id="cax-enabled" type="checkbox" class="form-switch" data-model="enabled.value">
39
+ </div>
40
+ <div class="cax-grid">
41
+ <div>
42
+ <label class="form-label" for="cax-access">Who may call it
43
+ <span data-help-title="Access" data-help="Public: anyone, no credential. API token: a token from System › API Tokens - it reads only its own project's pages. A role: a signed-in user of that role or more senior, sending their access token as a Bearer header (the site's login cookie is never accepted)."></span></label>
44
+ <select id="cax-access" class="form-select" data-model="access.value"></select>
45
+ <small class="cax-err" data-bind-hidden="!accessErr.value" data-bind-text="accessErr.value"></small>
46
+ </div>
47
+ <div>
48
+ <label class="form-label" for="cax-limit">Page size limit
49
+ <span data-help="The most pages one list request can ask for (1-200). Callers ask with ?limit=; anything higher is cut to this."></span></label>
50
+ <input id="cax-limit" type="number" class="form-input" min="1" max="200" data-model="maxLimit.value">
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="cax-card">
56
+ <div class="cax-card-title">What it serves
57
+ <span data-help="Each part can be switched off on its own; an address that is off answers 404."></span></div>
58
+ <div class="cax-setting">
59
+ <label class="form-label" for="cax-res-pages">Pages
60
+ <span data-help="Published pages a caller could open on the site - drafts, private and role-gated pages follow the site's own rules. A page whose front matter says api: false is never served."></span></label>
61
+ <input id="cax-res-pages" type="checkbox" class="form-switch" data-model="resPages.value">
62
+ </div>
63
+ <div class="cax-setting cax-indent" data-bind-hidden="!resPages.value">
64
+ <label class="form-label" for="cax-listable">Page list
65
+ <span data-help="Off keeps single pages but removes the list, so callers cannot read your site's full set of addresses - they must know a page's path."></span></label>
66
+ <input id="cax-listable" type="checkbox" class="form-switch" data-model="listable.value">
67
+ </div>
68
+ <div class="cax-setting">
69
+ <label class="form-label" for="cax-res-menus">Menus
70
+ <span data-help="The menu at a location (navbar, footer…). The admin sidebar and page overlays are never served."></span></label>
71
+ <input id="cax-res-menus" type="checkbox" class="form-switch" data-model="resMenus.value">
72
+ </div>
73
+ <div class="cax-setting">
74
+ <label class="form-label" for="cax-res-site">Site settings
75
+ <span data-help="Title, brand, footer, social links, SEO defaults and theme - what every public page already shows. Never mail settings or anything from the admin."></span></label>
76
+ <input id="cax-res-site" type="checkbox" class="form-switch" data-model="resSite.value">
77
+ </div>
78
+ <div class="cax-setting">
79
+ <label class="form-label" for="cax-res-search">Search
80
+ <span data-help="The site's own search, as a visitor gets it. Off here, or off in System › Search, and it answers 404."></span></label>
81
+ <input id="cax-res-search" type="checkbox" class="form-switch" data-model="resSearch.value">
82
+ </div>
83
+ </div>
84
+
85
+ <div class="cax-card">
86
+ <div class="cax-card-title">Page formats
87
+ <span data-help="What a caller may ask for with ?format=. A format that is not offered answers 400."></span></div>
88
+ <div class="cax-setting">
89
+ <label class="form-label" for="cax-fmt-html">HTML
90
+ <span data-help="The rendered page body - shortcodes already turned into markup. Tabs, carousels and the like need Domma's CSS and JS to work; each page says which it needs."></span></label>
91
+ <input id="cax-fmt-html" type="checkbox" class="form-switch" data-model="fmtHtml.value">
92
+ </div>
93
+ <div class="cax-setting">
94
+ <label class="form-label" for="cax-fmt-md">Markdown
95
+ <span data-help="The page source, shortcodes included - which can name collections, views and forms the HTML does not show. Offer it only if callers should see that."></span></label>
96
+ <input id="cax-fmt-md" type="checkbox" class="form-switch" data-model="fmtMarkdown.value">
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <!-- Endpoints -->
102
+ <div class="tab-panel">
103
+ <div class="cax-card">
104
+ <div class="cax-card-title">Addresses
105
+ <span data-help-title="Endpoints" data-help="Every Content API address, as the settings you are editing would answer it. Right-click (or ⋮) to copy an address or a curl command."></span></div>
106
+ <div class="cax-list cax-endpoints" data-each="endpoints.value key=path"></div>
107
+ <p class="cax-note">Calling from a browser on another domain needs that origin allowed in <code>config/server.json</code> (<code>cors</code>).</p>
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Try it -->
112
+ <div class="tab-panel">
113
+ <div class="cax-card">
114
+ <div class="cax-card-title">Try a page
115
+ <span data-help-title="Try it" data-help="Fetches a page exactly as an anonymous caller would get it - even before you switch the API on - so you can look first. It uses the saved settings' rules for drafts and visibility; a format you have not offered still shows, with a note."></span></div>
116
+ <div class="cax-try">
117
+ <input type="text" class="form-input cax-path" placeholder="/about" aria-label="Page path" data-model="tryPath.value">
118
+ <select class="form-select cax-format" aria-label="Format" data-model="tryFormat.value">
119
+ <option value="html">HTML</option>
120
+ <option value="markdown">Markdown</option>
121
+ <option value="both">Both</option>
122
+ </select>
123
+ <button type="button" class="btn btn-primary" data-on-click="run" data-bind-disabled="running.value">
124
+ <span data-icon="play" data-icon-size="14"></span> Fetch</button>
125
+ </div>
126
+ <div class="cax-result" data-bind-hidden="!tryStatus.value">
127
+ <div class="cax-chips">
128
+ <span class="cax-chip" data-bind-class="tryTone.value">Status <span data-bind-text="tryStatus.value"></span></span>
129
+ <span class="cax-chip" data-bind-hidden="!tryRequires.value" data-bind-text="tryRequires.value"></span>
130
+ </div>
131
+ <p class="cax-note" data-bind-hidden="!tryWhy.value" data-bind-text="tryWhy.value"></p>
132
+ <p class="cax-note is-warn" data-bind-hidden="!tryOffered.value" data-bind-text="tryOffered.value"></p>
133
+ <pre class="cax-json" data-bind-text="tryJson.value"></pre>
134
+ </div>
135
+ <p class="cax-note is-bad" data-bind-hidden="!tryError.value" data-bind-text="tryError.value"></p>
136
+ <div class="cax-card-title cax-sub">The same request with curl</div>
137
+ <div class="cax-curl">
138
+ <pre class="cax-json" data-bind-text="curl.value"></pre>
139
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="copyCurl" title="Copy">
140
+ <span data-icon="copy" data-icon-size="14"></span> Copy</button>
141
+ </div>
142
+ </div>
143
+ </div>
144
+
145
+ <!-- How it works -->
146
+ <div class="tab-panel">
147
+ <div class="cax-card">
148
+ <div class="cax-card-title">How the Content API works</div>
149
+ <ul class="cax-how">
150
+ <li>It is read-only: pages, menus, site settings and search, as JSON, at <code>/api/content/v1</code>.</li>
151
+ <li>A page the API serves is exactly a page the site would serve the same caller - drafts, private and role-gated pages, and pages in a disabled project follow the site's own rules.</li>
152
+ <li>Front matter <code>api: false</code> keeps a page off the API; it still shows on the site.</li>
153
+ <li>An API token reads only the pages of its own project. Give it <code>@pages</code>, <code>@menus</code>, <code>@site</code> or <code>@search</code> scopes to narrow it further; a token scoped only to collections gets no content.</li>
154
+ <li>Public answers may be cached for a minute; token answers are private; signed-in answers are never stored.</li>
155
+ <li>The full reference is under API Reference › Content API.</li>
156
+ </ul>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ <style>
162
+ .cax { display: flex; flex-direction: column; gap: .75rem; }
163
+ .cax [hidden] { display: none !important; }
164
+ .cax-alert { margin: 0; display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; padding: .6rem .8rem; border-radius: var(--dm-radius-md);
165
+ background: color-mix(in srgb, var(--dm-warning) 16%, transparent); color: var(--dm-text); }
166
+ .cax-alert.is-dirty { background: color-mix(in srgb, var(--dm-primary) 12%, transparent); }
167
+ .cax-spacer { flex: 1; }
168
+ .cax-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .7rem; margin-top: .75rem; }
169
+ .cax-card-title { display: flex; align-items: center; gap: .35rem; font-weight: 600; }
170
+ .cax-sub { font-size: var(--dm-text-sm); }
171
+ .cax-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem 1rem; }
172
+ .cax .form-label { display: flex; align-items: center; gap: .3rem; }
173
+ .cax-setting { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
174
+ .cax-setting .form-label { margin: 0; }
175
+ .cax-indent { padding-left: 1.25rem; }
176
+ .cax-err { color: var(--dm-danger); font-size: var(--dm-text-sm); }
177
+ .cax-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
178
+ .cax-note.is-bad { color: var(--dm-danger); }
179
+ .cax-note.is-warn { color: var(--dm-warning); }
180
+ .cax-chips { display: flex; flex-wrap: wrap; gap: .4rem; }
181
+ .cax-chip { display: inline-flex; align-items: center; gap: .25rem; font-size: var(--dm-text-xs); font-weight: 600; padding: .15rem .55rem; border-radius: 999px; white-space: nowrap;
182
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
183
+ .cax-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
184
+ .cax-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
185
+ .cax-chip.is-bad { background: color-mix(in srgb, var(--dm-danger) 16%, transparent); color: var(--dm-danger); }
186
+ .cax-list { display: flex; flex-direction: column; }
187
+ .cax-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); }
188
+ .cax-row:last-child { border-bottom: 0; }
189
+ .cax-row:hover, .cax-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
190
+ .cax-row.is-off .cax-main code { opacity: .55; }
191
+ .cax-method { font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-xs); font-weight: 700; color: var(--dm-success); min-width: 2.5rem; }
192
+ .cax-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: .1rem; }
193
+ .cax-main code { overflow-wrap: anywhere; background: none; padding: 0; color: var(--dm-text); font-size: var(--dm-text-sm); }
194
+ .cax-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
195
+ .cax-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; }
196
+ .cax-kebab:hover { color: var(--dm-primary); }
197
+ .cax-try .btn, .cax-curl .btn { display: inline-flex; align-items: center; gap: .35rem; }
198
+ .cax-try { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
199
+ .cax-path { flex: 1 1 16rem; max-width: 32rem; }
200
+ .cax-format { width: auto; }
201
+ .cax-result { display: flex; flex-direction: column; gap: .5rem; }
202
+ .cax-json { margin: 0; max-height: 24rem; overflow: auto; padding: .6rem .75rem; border-radius: var(--dm-radius-md); font-size: var(--dm-text-xs);
203
+ background: color-mix(in srgb, var(--dm-text) 5%, transparent); white-space: pre-wrap; overflow-wrap: anywhere; }
204
+ .cax-curl { display: flex; gap: .5rem; align-items: flex-start; }
205
+ .cax-curl .cax-json { flex: 1; }
206
+ .cax-how { margin: 0; padding-left: 1.25rem; line-height: 1.8; color: var(--dm-text-muted); }
207
+ @media (max-width: 720px) { .cax-grid { grid-template-columns: 1fr; } }
208
+ </style>