domma-cms 1.6.47 → 1.6.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/admin/js/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import{views as G}from"./views/index.js";import{withPluginChrome as B}from"./lib/plugin-chrome.js";import{api as c,getUser as F,isAuthenticated as m,logout as z,sendToLogin as K}from"./api.js";import{isAuthScreen as C,loginHashFor as J}from"./lib/login-arrange.js";import{installHttpInterceptor as Y}from"./http-interceptor.js";import{installResizableSlideovers as Q}from"./lib/slideover-resizable.js";import{installActivityThrobber as X}from"./lib/activity-throbber.js";import{installUpdateNotice as Z}from"./lib/update-notice.js";import{isPathAllowed as tt,redirectFor as et}from"./lib/admin-scope.js";import{deliverDesktop as it,desktopActive as ot,notificationSettings as at}from"./lib/desktop-notify.js";import{avatarUrl as st}from"./lib/avatar.js";import{installCalculator as nt}from"./lib/calculator.js";import{takeOverviewPass as lt,withDashboardStrip as O}from"./lib/dashboards-strip.js";import{installHotkeys as rt}from"./lib/hotkeys-ui.js";$(()=>{Y(),nt(),Z(),X(),Q(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");m()?await i(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,r="/",h=!1,w=null;function b(t){if(y[t])return E.toast(`${y[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const i=D[t]||et(p,t);return i||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"&&!lt()?D[r]||r:null)}const D={},y={};R.use(async(t,i,e)=>{if(C(t.path))return e();if(!m()){K();return}const o=b(t.path);if(o){R.navigate(o);return}e()});async function g(){try{const t=await c.get("/auth/permissions");p=t.adminScope||null,r=t.adminHome||"/";const i=t.permissions||[];return h=tt(p,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),w=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return p=null,r="/",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 A(await g())}catch{}}while(P)})().finally(()=>{N=null})});async function A(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:p})}M.subscribe("router:afterChange",({to:t})=>{C(t.path)&&(u&&(clearInterval(u),u=null),p=null,r="/",h=!1,w=null)});const _=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/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/usage/dashboard",view:"usageDashboard",title:"Dashboard - 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(c)),await I();const e=await g(),o=b(t.path);if(o&&R.navigate(o),A(e),at.read().signInToasts)try{const a=await c.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const f=d(s.data?.title||""),U=d((s.data?.body||"").slice(0,120));E.toast(`${f} - ${U}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}T()}}),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 V=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function W(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const i=t?String(t.path).split("?")[0]:"";if(!t||!W(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 U of V){const v=document.querySelector(U);if(!v)continue;if(Array.from(v.options||[]).find(q=>q.value===o)){v.value=o,v.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(f,n)};setTimeout(f,n)});const x="cms_card_states",j=S.get(x)||{};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(x,j))}),rt(),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const l={...G},H={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:"Site menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},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(H))l[t]&&(l[t]=B(l[t],{plugin:t,licence:"Built in",...i}));l.dashboard=O(l.dashboard);const k=[..._];async function I(){if(m())try{const t=await c.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}`);l[e]=B(a[o.exportName],(t.meta||{})[e])}catch{}l.dashboard=O(l.dashboard);for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(D[`/${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 T(){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=st(t);$("#topbar-user").html(`
1
+ import{views as G}from"./views/index.js";import{withPluginChrome as B}from"./lib/plugin-chrome.js";import{api as c,getUser as F,isAuthenticated as m,logout as z,sendToLogin as K}from"./api.js";import{isAuthScreen as C,loginHashFor as J}from"./lib/login-arrange.js";import{installHttpInterceptor as Y}from"./http-interceptor.js";import{installResizableSlideovers as Q}from"./lib/slideover-resizable.js";import{installActivityThrobber as X}from"./lib/activity-throbber.js";import{installUpdateNotice as Z}from"./lib/update-notice.js";import{isPathAllowed as tt,redirectFor as et}from"./lib/admin-scope.js";import{deliverDesktop as it,desktopActive as ot,notificationSettings as at}from"./lib/desktop-notify.js";import{avatarUrl as st}from"./lib/avatar.js";import{installCalculator as nt}from"./lib/calculator.js";import{takeOverviewPass as lt,withDashboardStrip as O}from"./lib/dashboards-strip.js";import{installHotkeys as rt}from"./lib/hotkeys-ui.js";$(()=>{Y(),nt(),Z(),X(),Q(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");m()?await i(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,r="/",h=!1,w=null;function b(t){if(y[t])return E.toast(`${y[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const i=D[t]||et(p,t);return i||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"&&!lt()?D[r]||r:null)}const D={},y={};R.use(async(t,i,e)=>{if(C(t.path))return e();if(!m()){K();return}const o=b(t.path);if(o){R.navigate(o);return}e()});async function g(){try{const t=await c.get("/auth/permissions");p=t.adminScope||null,r=t.adminHome||"/";const i=t.permissions||[];return h=tt(p,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),w=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return p=null,r="/",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 A(await g())}catch{}}while(P)})().finally(()=>{N=null})});async function A(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:p})}M.subscribe("router:afterChange",({to:t})=>{C(t.path)&&(u&&(clearInterval(u),u=null),p=null,r="/",h=!1,w=null)});const _=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Tool - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/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(c)),await I();const e=await g(),o=b(t.path);if(o&&R.navigate(o),A(e),at.read().signInToasts)try{const a=await c.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const f=d(s.data?.title||""),U=d((s.data?.body||"").slice(0,120));E.toast(`${f} - ${U}`,{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 V=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function W(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const i=t?String(t.path).split("?")[0]:"";if(!t||!W(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 U of V){const v=document.querySelector(U);if(!v)continue;if(Array.from(v.options||[]).find(q=>q.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))}),rt(),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const l={...G},H={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:"Site menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},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(H))l[t]&&(l[t]=B(l[t],{plugin:t,licence:"Built in",...i}));l.dashboard=O(l.dashboard);const k=[..._];async function I(){if(m())try{const t=await c.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}`);l[e]=B(a[o.exportName],(t.meta||{})[e])}catch{}l.dashboard=O(l.dashboard);for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(D[`/${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=st(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>
@@ -20,4 +20,4 @@ import{views as G}from"./views/index.js";import{withPluginChrome as B}from"./lib
20
20
  <span data-icon="log-out"></span>
21
21
  <span>Sign out</span>
22
22
  </a>
23
- `);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(),z()}),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(!(!m()||w===!1))try{const{count:t}=await c.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&&ot()&&it(await c.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!m()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash.startsWith("#/login")||(window.location.hash=J(window.location.hash));else if(await I(),F()){const a=await g(),n=(window.location.hash||"#/").slice(1)||"/",s=b(n);s&&(window.location.hash=`#${s}`),await A(a),T()}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 m()&&e!==o&&(e=o,await I(),R._matchRoute(o).route)?(e="",R._handleRouteChange()):(e="",m()&&$("#admin-sidebar").get(0)?.style.display==="none"?(E.toast("That link goes nowhere - here is your home page.",{type:"warning"}),R.navigate("/")):i(o))},R.init({container:"#view-container",routes:k,views:l,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}})})()});
23
+ `);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(),z()}),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(!(!m()||w===!1))try{const{count:t}=await c.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&&ot()&&it(await c.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!m()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash.startsWith("#/login")||(window.location.hash=J(window.location.hash));else if(await I(),F()){const a=await g(),n=(window.location.hash||"#/").slice(1)||"/",s=b(n);s&&(window.location.hash=`#${s}`),await A(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 m()&&e!==o&&(e=o,await I(),R._matchRoute(o).route)?(e="",R._handleRouteChange()):(e="",m()&&$("#admin-sidebar").get(0)?.style.display==="none"?(E.toast("That link goes nowhere - here is your home page.",{type:"warning"}),R.navigate("/")):i(o))},R.init({container:"#view-container",routes:k,views:l,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}})})()});
@@ -4,7 +4,7 @@
4
4
  <div class="blx">
5
5
  <p class="blx-note">Reusable HTML templates - one look for a collection's entries, a card, a call to action - placed on a
6
6
  page with a shortcode.
7
- <span data-help-title="Blocks" data-help="A block is an HTML template with a placeholder for each field (the field name in double curly braces), and optionally its own CSS. Use it to show collection entries ([collection … display=&quot;block&quot; block=&quot;name&quot;]), on its own ([block template=&quot;name&quot; …]), or by its name as a short tag ([name … /]). Each block's comment at the top says what it expects; right-click a block to copy its shortcode."></span></p>
7
+ <span data-help-title="Blocks" data-help="A block is an HTML template with a placeholder for each field (the field name in double curly braces), and optionally its own CSS. Use it to show collection entries ([collection … display=&quot;block&quot; block=&quot;name&quot;]), on its own ([block template=&quot;name&quot; …]), or by its name as a short tag ([name … /]). Each block's comment at the top says what it expects; right-click a block to copy its shortcode. The book button above opens the full Blocks guide."></span></p>
8
8
 
9
9
  <!-- Hidden file picker driven by the Import banner action -->
10
10
  <input class="blx-import-file" type="file" accept=".dmblock.json,application/json" hidden>
@@ -367,7 +367,7 @@
367
367
 
368
368
  <hr>
369
369
  <p class="text-muted" style="font-size:.9rem;">
370
- Next: <a href="#/tutorials/plugin">Writing a Plugin</a>
370
+ Next: <a href="#/tutorials/plugin">Writing a Tool</a>
371
371
  </p>
372
372
 
373
373
  </div>
@@ -1,5 +1,5 @@
1
1
  <div class="view-header">
2
- <h1><span data-icon="document"></span> Writing a Plugin</h1>
2
+ <h1><span data-icon="document"></span> Writing a Tool</h1>
3
3
  <a href="#/tutorials" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> All tutorials</a>
4
4
  </div>
5
5
 
@@ -7,12 +7,18 @@
7
7
  <div class="col-12">
8
8
  <div class="docs-body">
9
9
 
10
- <p>Plugins live in the <code>plugins/</code> directory. Each plugin is a self-contained folder with
11
- three required files and optional <code>admin/</code>, <code>public/</code> and <code>docs/</code>
12
- subdirectories.</p>
10
+ <p>A <strong>Tool</strong> is a screen in the admin with its own place in the sidebar: Notes, Invoices,
11
+ Booking and Email are all Tools. The ones you add yourself are <strong>plugins</strong>. That is the older
12
+ name, and it is still used for the folder they live in, the API under <code>/api/plugins/</code> and the
13
+ files you share. Once a plugin is switched on it is a Tool like any other: it files under
14
+ <strong>Tools</strong> in the sidebar, gets the same banner, and an admin can move it, hide it or give it a
15
+ hotkey.</p>
16
+ <p>Each Tool is a self-contained folder in <code>plugins/</code>, with three required files and optional
17
+ <code>admin/</code>, <code>public/</code> and <code>docs/</code> subdirectories.</p>
13
18
  <p>The quickest start is <strong>New plugin</strong> in the banner of <a href="#/plugins">System &gt;
14
19
  Plugins</a> (the Marketplace screen). It copies the built-in template into <code>plugins/&lt;slug&gt;/</code>
15
- with a server entry, config defaults, a wired admin view and a <code>CLAUDE.md</code>, switched on. Restart
20
+ with a server entry, config defaults, a wired admin view, a starter user guide and a <code>CLAUDE.md</code>,
21
+ switched on. Restart
16
22
  the server to activate it. The rest of this page explains what those files do.</p>
17
23
 
18
24
  <h3>Directory structure</h3>
@@ -81,7 +87,9 @@
81
87
  <tr>
82
88
  <td><code>admin.sidebar</code></td>
83
89
  <td>array</td>
84
- <td>Sidebar items to add to the admin panel.</td>
90
+ <td>The Tool's sidebar entries. Each may name its default <code>folder</code> (<code>tools</code>
91
+ unless you say otherwise), a <code>group</code> inside it, a <code>hotkey</code> and a
92
+ <code>countUrl</code> for its badge - see section 5.</td>
85
93
  </tr>
86
94
  <tr>
87
95
  <td><code>admin.routes</code></td>
@@ -252,7 +260,94 @@ export const myPluginView = {
252
260
 
253
261
  <hr>
254
262
 
255
- <h3>5. Injection snippets (optional)</h3>
263
+ <h3>5. Making it feel like a Tool</h3>
264
+ <p>These are what make a screen look and behave like the built-in Tools. None is required, and each one is
265
+ small.</p>
266
+
267
+ <h4>Its place in the sidebar</h4>
268
+ <pre class="code-block"><code>"sidebar": [
269
+ {
270
+ "text": "Invoices",
271
+ "url": "#/plugins/invoices",
272
+ "icon": "file-text",
273
+ "folder": "tools",
274
+ "group": "Business",
275
+ "hotkey": "i",
276
+ "permission": "invoices",
277
+ "countUrl": "/api/plugins/invoices/_count"
278
+ }
279
+ ]</code></pre>
280
+ <ul>
281
+ <li><code>folder</code> is where the Tool starts: <code>overview</code>, <code>content</code>,
282
+ <code>data</code>, <code>tools</code> (the default), <code>system</code> or <code>documentation</code>.
283
+ <code>group</code> puts it in a sub-folder by name, made if it is not there. An admin can move the Tool
284
+ anywhere in <strong>Content &gt; Site menus &gt; Admin sidebar</strong>, and their choice wins.</li>
285
+ <li><code>hotkey</code> is one letter: <kbd>G</kbd> then that letter opens the Tool. The built-in Tools claim
286
+ their letters first, and the first Tool to ask for a free one gets it.</li>
287
+ <li>A Tool is known by its <code>url</code>, so give each entry its own.</li>
288
+ </ul>
289
+
290
+ <h4>A badge with a popover</h4>
291
+ <p>A <code>countUrl</code> on the entry draws a count badge beside it. The route answers
292
+ <code>{count, label, tone, details}</code>. <code>tone</code> is <code>danger</code> (red, something is late),
293
+ <code>live</code> (a pulsing dot, happening now) or <code>info</code>. <code>details</code> fills the popover
294
+ shown on hover: <code>{title, rows: [{label, value}], items: [{text, meta}], empty}</code>, all plain
295
+ text. Counts refresh every minute. When your screen changes the number, tell the sidebar straight away with
296
+ <code>sidebarCount(url)</code> from the tool kit (below). With Dashboard Pro, the same answer is
297
+ offered as a Dashboard widget.</p>
298
+ <pre class="code-block"><code>fastify.get('/_count', { preHandler: [authenticate] }, async () =&gt; {
299
+ const overdue = await countOverdue();
300
+ return {
301
+ count: overdue,
302
+ label: `${overdue} overdue`,
303
+ tone: overdue ? 'danger' : 'info',
304
+ details: { title: 'Invoices', rows: [{ label: 'Overdue', value: String(overdue) }], empty: 'Nothing overdue.' }
305
+ };
306
+ });</code></pre>
307
+
308
+ <h4>The banner, help and the tool kit</h4>
309
+ <p>Every Tool screen sits under a banner showing its name, version, date and licence, drawn by the admin. Put
310
+ screen-level buttons (New, Import, a settings cog) <strong>in</strong> the banner with
311
+ <code>addBannerAction</code>. Add a (?) beside anything that needs explaining with a <code>data-help</code>
312
+ attribute (plain text) and <code>helpPopovers</code>. <code>tool-kit.js</code> has the rest:
313
+ <code>openMenuAt</code> for a ⋮ button that opens a row's right-click menu, <code>viewLifetime</code> to
314
+ tidy up listeners when the user leaves, <code>onceClosed</code> to remove a slideover once it closes, and
315
+ <code>sidebarCount</code>.</p>
316
+ <p>Load these <strong>at run time</strong> with <code>import()</code>, never a static import. The admin is
317
+ replaced on every CMS update, so your Tool must keep working, with less, on a CMS that lacks one of them:</p>
318
+ <pre class="code-block"><code>async onMount($container) {
319
+ try {
320
+ const chrome = await import('/admin/js/lib/plugin-chrome.js');
321
+ chrome.addBannerAction?.($container, { icon: 'plus', title: 'New invoice', onClick: () =&gt; openNew() });
322
+ const help = await import('/admin/js/lib/help-popover.js');
323
+ help.helpPopovers($container, { position: 'bottom' });
324
+ } catch {
325
+ // an older admin: put the button in your own toolbar instead
326
+ }
327
+ }</code></pre>
328
+ <p>Right-click menus come from <code>E.contextMenu</code> on your rows, and screens stay readable in every theme
329
+ if your CSS uses only the theme's <code>--dm-*</code> variables.</p>
330
+
331
+ <h4>Needing other Tools</h4>
332
+ <p><code>requires</code> lists Tools yours cannot work without: it is not loaded while one of them is off.
333
+ <code>uses</code> lists the ones it can do without, with what is lost
334
+ (<code>{"contacts": "Invite a contact group"}</code>). When an admin switches one of those off, the
335
+ Marketplace shows them that text first.</p>
336
+
337
+ <h4>Data and blocks</h4>
338
+ <p>A Tool that keeps records can ship collections (<code>collections/&lt;slug&gt;/schema.json</code>, created the
339
+ first time it is switched on), forms and pages. To give your public pages a design site owners can change, use
340
+ a <a href="#/docs/usage/blocks">block</a>. Create it in <code>onEnable</code>, which runs every time the Tool
341
+ is switched on, so only when it is not there yet - otherwise you would undo the site owner's changes in
342
+ <strong>Data &gt; Blocks</strong>:</p>
343
+ <pre class="code-block"><code>export async function onEnable({ services }) {
344
+ const exists = await services.blocks.getBlock('invoice-card').then(() =&gt; true, () =&gt; false);
345
+ if (!exists) await services.blocks.saveBlock('invoice-card', INVOICE_CARD_HTML, { css: INVOICE_CARD_CSS });
346
+ }</code></pre>
347
+
348
+ <hr>
349
+
350
+ <h3>6. Injection snippets (optional)</h3>
256
351
  <p>HTML snippets declared in <code>inject.head</code>, <code>inject.headLate</code> and
257
352
  <code>inject.bodyEnd</code> are read from the plugin's folder and inserted into every public page. Use
258
353
  this for analytics scripts, stylesheets, or widgets. They are read once at start-up.</p>
@@ -272,15 +367,16 @@ export const myPluginView = {
272
367
 
273
368
  <hr>
274
369
 
275
- <h3>6. User guide (optional)</h3>
370
+ <h3>7. User guide</h3>
276
371
  <p>Markdown files in the plugin's <code>docs/</code> folder become pages under <strong>Documentation &gt;
277
- Plugins</strong> while the plugin is loaded, shown only to users who can use the plugin.
372
+ Plugins</strong> while the Tool is on, shown only to users who can use it.
278
373
  <code>guide.md</code> is the first page; further files become tabs. Start with <code>##</code> sections,
279
- not a top-level heading, and write for site owners.</p>
374
+ not a top-level heading, and write for site owners: what it does, getting started, one section per
375
+ screen, settings, permissions. Update it in the same change as the feature.</p>
280
376
 
281
377
  <hr>
282
378
 
283
- <h3>7. Switching it on and testing</h3>
379
+ <h3>8. Switching it on and testing</h3>
284
380
  <ol>
285
381
  <li>Create the <code>plugins/my-plugin/</code> directory with all three required files (or use
286
382
  <strong>New plugin</strong>, which does this and switches it on).</li>
@@ -294,6 +390,13 @@ export const myPluginView = {
294
390
  <li>Verify your endpoint: <code>GET /api/plugins/my-plugin/hello</code></li>
295
391
  </ol>
296
392
 
393
+ <h3>9. Sharing it</h3>
394
+ <p>On the Marketplace screen, right-click your Tool and choose <strong>Export .dcmsplugin</strong>. On another
395
+ site, <strong>Install from file</strong> in the Marketplace banner takes that file. Installing a newer version
396
+ of a Tool that is already there updates it in place, keeping its <code>data/</code>, and if anything goes
397
+ wrong the old version is put back. An unsigned file, or an older version than the one installed, asks for
398
+ your confirmation first.</p>
399
+
297
400
  <p class="text-muted" style="font-size:.9rem">Tip: use <code>npm run dev</code> during development -
298
401
  the server restarts automatically on file changes. Server-side files (<code>plugin.js</code>,
299
402
  <code>config.js</code>, <code>plugin.json</code>, snippets) only apply after a restart; the in-admin code
@@ -0,0 +1,243 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="box"></span> Blocks</h1>
3
+ <a href="#/documentation" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> All usage topics</a>
4
+ </div>
5
+
6
+ <div class="row">
7
+ <div class="col-12">
8
+ <div class="docs-body">
9
+
10
+ <p>A <strong>block</strong> is a small piece of your own HTML, with an optional stylesheet, that you write once
11
+ and place anywhere. Its <strong>placeholders</strong> are filled in when the page is drawn: from a collection's
12
+ entries, from a view, or from values you type into the shortcode. Use a block when the built-in displays are
13
+ not quite the design you want. Team cards, product tiles, testimonials, a price table row and a contact panel
14
+ are all one block each, and the data behind them stays in a collection where anyone can edit it.</p>
15
+ <p>Blocks are kept in <strong>Data &gt; Blocks</strong>. Each one is a file in <code>content/blocks/</code>:
16
+ <code>&lt;name&gt;.html</code> holds the template, <code>&lt;name&gt;.css</code> its styles (optional) and
17
+ <code>&lt;name&gt;.meta.json</code> its project and bundled flag (optional).</p>
18
+
19
+ <h3>Three ways to place a block</h3>
20
+ <table class="table table-sm">
21
+ <thead>
22
+ <tr>
23
+ <th>You want</th>
24
+ <th>Write</th>
25
+ <th>Placeholders come from</th>
26
+ </tr>
27
+ </thead>
28
+ <tbody>
29
+ <tr>
30
+ <td>One copy per collection entry</td>
31
+ <td><code>[collection slug="team" display="block" block="team-card" cols="3" /]</code></td>
32
+ <td>Each entry's fields</td>
33
+ </tr>
34
+ <tr>
35
+ <td>One copy per row of a view (filtered, joined, sorted)</td>
36
+ <td><code>[view slug="open-jobs" display="block" block="job-card" /]</code></td>
37
+ <td>Each row of the view</td>
38
+ </tr>
39
+ <tr>
40
+ <td>One copy by itself, with values you type</td>
41
+ <td><code>[block template="cta-banner" heading="Book a call" /]</code> or the short tag
42
+ <code>[cta-banner heading="Book a call" /]</code></td>
43
+ <td>The shortcode's own attributes</td>
44
+ </tr>
45
+ </tbody>
46
+ </table>
47
+ <p>A block also draws each slide of a carousel or each row of a list group:
48
+ <code>[collection slug="team" display="carousel" block="team-card" /]</code> and
49
+ <code>display="listgroup" block="…"</code> work the same way.</p>
50
+
51
+ <hr>
52
+
53
+ <h3>Blocks with collections and views</h3>
54
+ <p>This is where blocks are most useful. The collection holds the data and the block holds the design, so a new
55
+ entry shows up on the page, styled, as soon as it is saved. There is no page to edit.</p>
56
+ <ul>
57
+ <li><code>display="block"</code> plus <code>block="&lt;name&gt;"</code> draws every entry through the block.</li>
58
+ <li>On a <code>[collection]</code>, <code>cols="2"</code> to <code>cols="6"</code> lays the copies out in a
59
+ responsive grid. Leave it out to stack them. A <code>[view]</code> always stacks them.</li>
60
+ <li>Everything else on <code>[collection]</code> still works: <code>limit</code>, <code>sort</code>,
61
+ <code>order</code>, the <code>where_&lt;field&gt;</code> filters, <code>scope="mine"</code>,
62
+ <code>empty</code>, <code>item-class</code> and the per-entry <code>cta</code> buttons (drawn under each
63
+ copy).</li>
64
+ <li>On a <code>[view]</code>, <code>block="…"</code> may be left out when the view's Display tab already names
65
+ a block.</li>
66
+ <li>Visitors can still right-click any copy to filter, sort, group, copy or export, as they can on the other
67
+ displays. Only the fields the block actually uses are offered.</li>
68
+ </ul>
69
+ <p>If the named block does not exist, the page shows <em>Block template "name" not found</em> where the entries
70
+ would be, so a typo is easy to spot.</p>
71
+
72
+ <h3>Blocks on their own</h3>
73
+ <p><code>[block template="name" … /]</code> draws one copy, with each other attribute filling the placeholder
74
+ of the same name. Every block can also be written with its name as the tag, so
75
+ <code>[contact-card full_name="Jane Doe" /]</code> means the same as
76
+ <code>[block template="contact-card" full_name="Jane Doe" /]</code>.</p>
77
+ <ul>
78
+ <li>Short tags are self-closing (end with <code>/]</code>). They work for any block whose name does not clash
79
+ with a built-in shortcode. A block called <code>card</code> or <code>menu</code> can only be placed with
80
+ <code>[block template="…"]</code>.</li>
81
+ <li><code>class</code> and <code>id</code> go on the wrapper around the copy.</li>
82
+ <li>A placeholder with no value comes out empty, and a block that does not exist is removed without a
83
+ message.</li>
84
+ <li>Values are written in double quotes. For a value that itself contains a double quote, use single quotes
85
+ around it instead.</li>
86
+ <li>Blocks are expanded even inside inline code and code fences on a page. To mention a block's shortcode in
87
+ ordinary text, write its opening bracket as <code>&amp;#91;</code>. Inside code that does not work (it shows
88
+ as written), so keep block shortcodes out of code samples on your pages.</li>
89
+ </ul>
90
+
91
+ <hr>
92
+
93
+ <h3>Writing the template</h3>
94
+ <p>A template is ordinary HTML. Domma classes (<code>card</code>, <code>badge</code>, <code>grid</code>,
95
+ <code>btn</code>) and icons (<code>&lt;span data-icon="mail"&gt;&lt;/span&gt;</code>) work as they do anywhere
96
+ else on the site. Placeholders are field names in double curly braces:</p>
97
+ <table class="table table-sm">
98
+ <thead>
99
+ <tr>
100
+ <th>Placeholder</th>
101
+ <th>What goes in</th>
102
+ </tr>
103
+ </thead>
104
+ <tbody>
105
+ <tr>
106
+ <td><code>&#123;&#123;field&#125;&#125;</code></td>
107
+ <td>The value, made safe as text (HTML is escaped). Use this almost everywhere.</td>
108
+ </tr>
109
+ <tr>
110
+ <td><code>&#123;&#123;field:DD MMM YYYY&#125;&#125;</code></td>
111
+ <td>The value formatted as a date. Tokens: <code>YYYY YY MMMM MMM MM M DD D dddd ddd HH H hh h mm m ss s A
112
+ a</code>; anything else passes through, so <code>DD/MM/YYYY</code> and <code>HH:mm</code> work. A value
113
+ that is not a date is shown as it is.</td>
114
+ </tr>
115
+ <tr>
116
+ <td><code>&#123;&#123;&#123;field&#125;&#125;&#125;</code></td>
117
+ <td>The value inserted <strong>as HTML</strong>, unescaped. Only for fields you trust, such as rich text
118
+ written by your own editors.</td>
119
+ </tr>
120
+ <tr>
121
+ <td><code>&#123;&#123;_id&#125;&#125;</code>, <code>&#123;&#123;_createdAt&#125;&#125;</code>,
122
+ <code>&#123;&#123;_updatedAt&#125;&#125;</code></td>
123
+ <td>The entry's id and its created and updated times (collections and views only). Combine them with a
124
+ format: <code>&#123;&#123;_createdAt:D MMMM YYYY&#125;&#125;</code>.</td>
125
+ </tr>
126
+ </tbody>
127
+ </table>
128
+ <p>The page is cleaned before it is sent, so anything in a block that could run code (a
129
+ <code>&lt;script&gt;</code>, an <code>onclick</code> or <code>onerror</code> handler, a
130
+ <code>javascript:</code> link) is removed, exactly as it would be in page content.</p>
131
+
132
+ <h4>Describe the block in a comment</h4>
133
+ <p>Start the template with an HTML comment saying what it is, what it expects and how to use it. The Blocks list
134
+ shows the first line as the block's description, and <strong>Copy shortcode</strong> copies the example.
135
+ Comments are stripped when the block is drawn, so they never reach the page.</p>
136
+ <pre class="code-block"><code>&lt;!--
137
+ team-card - One team member: photo, name, role and a short bio.
138
+
139
+ Parameters:
140
+ name - Full name
141
+ role - Job title
142
+ photo - Image URL
143
+ bio - One or two sentences
144
+
145
+ Example:
146
+ [collection slug="team" display="block" block="team-card" cols="3" /]
147
+ --&gt;
148
+ &lt;div class="card team-card"&gt;
149
+ &lt;img class="team-card-photo" src="&#123;&#123;photo&#125;&#125;" alt="&#123;&#123;name&#125;&#125;"&gt;
150
+ &lt;div class="card-body"&gt;
151
+ &lt;h3&gt;&#123;&#123;name&#125;&#125;&lt;/h3&gt;
152
+ &lt;span class="badge badge-secondary"&gt;&#123;&#123;role&#125;&#125;&lt;/span&gt;
153
+ &lt;p&gt;&#123;&#123;bio&#125;&#125;&lt;/p&gt;
154
+ &lt;/div&gt;
155
+ &lt;/div&gt;</code></pre>
156
+
157
+ <h4>Styles</h4>
158
+ <p>A block's <strong>Custom CSS</strong> applies to that block only: the CMS wraps it in
159
+ <code>[data-block="name"] { … }</code>, so plain rules like <code>.team-card-photo { … }</code> cannot leak
160
+ onto the rest of the page. Use CSS nesting for child rules, and the theme's <code>--dm-*</code> variables
161
+ (<code>var(--dm-primary)</code>, <code>var(--dm-border)</code>) so the block follows the site theme and dark
162
+ mode.</p>
163
+ <pre class="code-block"><code>.team-card-photo {
164
+ width: 100%;
165
+ aspect-ratio: 1;
166
+ object-fit: cover;
167
+ border-bottom: 3px solid var(--dm-primary);
168
+ }</code></pre>
169
+ <p><code>@keyframes</code>, <code>@font-face</code> and <code>@import</code> cannot be scoped. Put those in
170
+ <strong>Site Settings &gt; Custom CSS</strong>. A block's stylesheet may be up to 50 KB. It is sent once for
171
+ each shortcode that uses the block, however many entries that shortcode shows.</p>
172
+
173
+ <hr>
174
+
175
+ <h3>The Blocks screen</h3>
176
+ <p><strong>Data &gt; Blocks</strong> lists every block with its description, whether it has CSS, and how many
177
+ pages and views use it.</p>
178
+ <ul>
179
+ <li><strong>All</strong>, <strong>In use</strong> and <strong>Unused</strong> filter the list, and the search
180
+ box finds a block by name or description.</li>
181
+ <li><strong>Click</strong> a block to edit it. <strong>Right-click</strong> it (or use ⋮, or
182
+ <kbd>Shift</kbd>+<kbd>F10</kbd>) to copy its shortcode, see where it is used, duplicate, export or
183
+ delete it.</li>
184
+ <li><strong>New block</strong> and <strong>Import</strong> are in the banner at the top.</li>
185
+ <li>The sidebar badge counts your blocks. Hover over it for the unused ones and the latest changes.</li>
186
+ </ul>
187
+ <p><em>In use</em> is worked out from page sources and view settings. A Tool that draws a block from its own code
188
+ is not counted, so check before deleting a block marked unused.</p>
189
+
190
+ <h3>The block editor</h3>
191
+ <ul>
192
+ <li><strong>Details</strong>: the name (lowercase letters, digits and hyphens, fixed once the block is saved),
193
+ an optional project, and <strong>Bundled</strong>, which ships the block with fresh installs.</li>
194
+ <li><strong>Template HTML</strong>: the editor, with undo, cut, copy, paste and <strong>Format</strong> (tidies
195
+ the indentation). <strong>Edit</strong>, <strong>Split</strong> and <strong>Preview</strong> choose what you
196
+ see.</li>
197
+ <li><strong>Sample values</strong>: one box per placeholder the template uses, filled in for the preview only.
198
+ Nothing in them is saved. Right-click one to insert its placeholder into the template.</li>
199
+ <li><strong>Preview</strong>: the block drawn inside your own site page, with its theme, fonts and Custom CSS.
200
+ Try it at full, tablet or mobile width, as <strong>Three in a grid</strong> (as a collection with
201
+ <code>cols="3"</code> shows it), or in another theme. The site itself is not changed.</li>
202
+ <li><strong>Custom CSS</strong>: the block's scoped stylesheet (see Styles above).</li>
203
+ <li><strong>Placeholder reference</strong>: the table above, at the foot of the screen.</li>
204
+ </ul>
205
+ <p><kbd>Ctrl</kbd>+<kbd>S</kbd> saves from anywhere on the screen. If you leave with unsaved changes you are
206
+ asked first, and your work is kept as a draft for that block until you come back to it.</p>
207
+
208
+ <h3>Copying blocks between sites</h3>
209
+ <p><strong>Export</strong> on a block's right-click menu downloads it as a <code>&lt;name&gt;.dmblock.json</code>
210
+ file, with its HTML and CSS. On the other site, use <strong>Import</strong> in the Blocks banner. If a block
211
+ with that name already exists, you are asked before it is replaced.</p>
212
+
213
+ <hr>
214
+
215
+ <h3>Worked example: a team page</h3>
216
+ <ol>
217
+ <li>In <strong>Data &gt; Collections</strong>, create a <code>team</code> collection with the fields
218
+ <code>name</code>, <code>role</code>, <code>photo</code> and <code>bio</code>, and add a few people.</li>
219
+ <li>In <strong>Data &gt; Blocks</strong>, choose <strong>New block</strong>, call it <code>team-card</code> and
220
+ paste the template and CSS from above. Fill in the sample values and check the preview at mobile width and as
221
+ three in a grid. Save.</li>
222
+ <li>On your Team page, write
223
+ <code>[collection slug="team" display="block" block="team-card" cols="3" sort="name" order="asc" /]</code>.</li>
224
+ <li>From now on, adding a person to the collection adds a card to the page. Changing the block restyles every
225
+ card at once.</li>
226
+ </ol>
227
+
228
+ <h3>Who can use Blocks</h3>
229
+ <p>Blocks share the <strong>pages</strong> permission: <em>read</em> to see them, <em>update</em> to create, edit
230
+ and import, and <em>delete</em> to remove them. A block filed under a project is only shown to users who can see
231
+ that project.</p>
232
+
233
+ <h3>Blocks in your own Tools</h3>
234
+ <p>A Tool (plugin) can rely on blocks too. It can render one of its pages with a block shortcode, or
235
+ create the block it needs the first time it is switched on, so the site owner can restyle it here. See <a href="#/tutorials/plugin">Writing a Tool</a>.</p>
236
+
237
+ <p class="text-muted" style="font-size:.9rem;">Full shortcode syntax:
238
+ <a href="#/docs/usage/shortcodes">Shortcodes</a>. Collection displays:
239
+ <a href="#/docs/usage/collections">Collections &amp; Forms</a> and <a href="#/docs/usage/views">Views</a>.</p>
240
+
241
+ </div>
242
+ </div>
243
+ </div>
@@ -120,7 +120,7 @@
120
120
  what it does, its screens, settings and permissions.</li>
121
121
  <li><a href="#/docs/usage/editions">Editions &amp; Licences</a> - free and Pro, rental and
122
122
  perpetual licences, and what happens when one ends.</li>
123
- <li><a href="#/tutorials/plugin">Writing a Plugin</a> - build your own.</li>
123
+ <li><a href="#/tutorials/plugin">Writing a Tool</a> - build your own.</li>
124
124
  </ul>
125
125
 
126
126
  </div>
@@ -58,6 +58,10 @@
58
58
  <div class="card-body"><h3 style="margin-top:0;"><span data-icon="users"></span> Users &amp; Roles</h3>
59
59
  <p class="text-muted" style="margin-bottom:0;">Accounts, roles, the role ladder, confined roles and resets.</p></div>
60
60
  </a>
61
+ <a href="#/docs/usage/blocks" class="card" style="text-decoration:none;display:block;">
62
+ <div class="card-body"><h3 style="margin-top:0;"><span data-icon="box"></span> Blocks</h3>
63
+ <p class="text-muted" style="margin-bottom:0;">Your own HTML templates for collection entries, views and one-off pieces.</p></div>
64
+ </a>
61
65
  <a href="#/docs/usage/tools" class="card" style="text-decoration:none;display:block;">
62
66
  <div class="card-body"><h3 style="margin-top:0;"><span data-icon="list"></span> Built-in Tools</h3>
63
67
  <p class="text-muted" style="margin-bottom:0;">Contacts, Notes, Todo, Analytics, SEO, notifications and search.</p></div>
@@ -101,7 +105,7 @@
101
105
  <p class="text-muted" style="margin-bottom:0;">Emails, webhooks, Actions and what happens after a submission.</p></div>
102
106
  </a>
103
107
  <a href="#/tutorials/plugin" class="card" style="text-decoration:none;display:block;">
104
- <div class="card-body"><h3 style="margin-top:0;"><span data-icon="package"></span> Writing a Plugin</h3>
108
+ <div class="card-body"><h3 style="margin-top:0;"><span data-icon="package"></span> Writing a Tool</h3>
105
109
  <p class="text-muted" style="margin-bottom:0;">Server routes, admin screens and page snippets of your own.</p></div>
106
110
  </a>
107
111
  </div>