domma-cms 1.8.17 → 1.8.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.8.18] - 2026-10-17
4
+
5
+ - **See a block on its page while you edit it.** The block editor's preview has a new **Show on** list: the pages that use the block, with the page you came from first (and picked for you). Choose one and the preview is that whole page - its layout, theme and everything around the block - with the block as it is in the editor now, scrolled to it and outlined; it follows your typing. If that page has unsaved changes kept from the page editor, they are shown too. "A blank page" is the preview as before.
6
+ - **The page editor shows unsaved block changes too.** Edit a block, go back to the page without saving it, and the page's preview shows the block as you left it, with a note - "Preview shows unsaved changes to cta-banner" - until you save or discard it in the block editor. Saving the page saves the page only. `POST /api/pages/preview/full` takes `blocks` (`{name: {content, css}}`) for this; it changes that one render and nothing else.
7
+
3
8
  ## [1.8.17] - 2026-10-17
4
9
 
5
10
  - **Pages and blocks in one place.** The page editor and the block editor share a bar at the top: one search box that finds any page or block, the last five you edited, and - in the page editor - the blocks this page uses. Going from a page to a block (or a block to a page, or one page to another) this way no longer asks "Leave anyway?": your unsaved changes are kept, and when you go back they are simply there again ("Your unsaved changes to this page are back."). Anything with changes waiting shows a dot. If the page or block was saved by someone else in the meantime, you are asked as before, and leaving the editors for any other screen still asks. The old page and block switchers are gone - the search replaces both.
package/admin/js/api.js CHANGED
@@ -1 +1 @@
1
- import{claimDrafts as $}from"./lib/draft-owner.js";import{loginHashFor as O}from"./lib/login-arrange.js";const m="/api";function a(){return S.get("auth_token")}function u(){return S.get("auth_refresh_token")}function f(e){S.set("auth_token",e)}function y(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function g(){const e=u();if(!e)throw new Error("No refresh token");const o=await fetch(`${m}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw y(),sendToLogin({expired:!0}),new Error("Token refresh failed");const{token:n}=await o.json();return f(n),n}async function t(e,o={}){let n=a();const r=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let s=await fetch(`${m}${e}`,{...o,headers:r(n)});if(s.status===401&&u())try{n=await g(),s=await fetch(`${m}${e}`,{...o,headers:r(n)})}catch{return}if(!s.ok){const i=await s.json().catch(()=>({error:"Request failed"})),d=new Error(i.error||i.message||`HTTP ${s.status}`);throw Array.isArray(i.validation)&&(d.validation=i.validation),d.status=s.status,d.body=i,d}return s.status===204?null:s.json()}async function b(e,o){const n=a(),r=n?{Authorization:`Bearer ${n}`}:{},s=await fetch(`${m}${e}`,{method:"POST",headers:r,body:o});if(!s.ok){const i=await s.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${s.status}`)}return s.json()}export const api={auth:{setupStatus:()=>t("/auth/setup-status",{method:"GET"}),setup:e=>t("/auth/setup",{method:"POST",body:JSON.stringify(e)}),login:e=>t("/auth/login",{method:"POST",body:JSON.stringify(e)}),verifyLogin:e=>t("/auth/login/verify",{method:"POST",body:JSON.stringify(e)}),me:()=>t("/auth/me",{method:"GET"}),updateMe:e=>t("/auth/me",{method:"PUT",body:JSON.stringify(e)}),refresh:e=>t("/auth/refresh",{method:"POST",body:JSON.stringify({refreshToken:e})}),forgotPassword:e=>t("/auth/forgot-password",{method:"POST",body:JSON.stringify({email:e})}),checkReset:(e,o)=>t("/auth/reset-password/check",{method:"POST",body:JSON.stringify({token:e,...o&&{password:o}})}),resetPassword:(e,o)=>t("/auth/reset-password",{method:"POST",body:JSON.stringify({token:e,password:o})})},pages:{list:()=>t("/pages",{method:"GET"}),get:e=>t(`/pages${e}`,{method:"GET"}),create:e=>t("/pages",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/pages${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/pages${e}`,{method:"DELETE"}),preview:e=>t("/pages/preview",{method:"POST",body:JSON.stringify({markdown:e})}),previewFull:(e,o,n)=>t("/pages/preview/full",{method:"POST",body:JSON.stringify({urlPath:e,frontmatter:o,body:n})}),previewLinks:{list:e=>t(`/pages/preview-links${e?`?urlPath=${encodeURIComponent(e)}`:""}`,{method:"GET"}).then(o=>o.links||[]),create:(e,o,n)=>t("/pages/preview-links",{method:"POST",body:JSON.stringify({urlPath:e,expiresIn:o,label:n})}),revoke:e=>t(`/pages/preview-links/${e}`,{method:"DELETE"})},tags:()=>t("/pages/tags",{method:"GET"}).then(e=>e.tags||[])},listTags:{list:e=>t(`/list-tags/${e}`,{method:"GET"}).then(o=>o.tags||[]),add:(e,o)=>t(`/list-tags/${e}`,{method:"POST",body:JSON.stringify({name:o})}),changeItem:(e,o,n)=>t(`/list-tags/${e}/item`,{method:"PUT",body:JSON.stringify({id:o,...n})}).then(r=>r.tags||[]),rename:(e,o,n)=>t(`/list-tags/${e}/rename`,{method:"POST",body:JSON.stringify({from:o,to:n})}),remove:(e,o)=>t(`/list-tags/${e}/delete`,{method:"POST",body:JSON.stringify({name:o})})},settings:{get:()=>t("/settings",{method:"GET"}),save:e=>t("/settings",{method:"PUT",body:JSON.stringify(e)}),getCustomCss:()=>t("/settings/custom-css",{method:"GET"}),saveCustomCss:e=>t("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:e})})},theme:{get:()=>t("/theme",{method:"GET"}),save:e=>t("/theme",{method:"PUT",body:JSON.stringify(e)}),catalog:()=>t("/theme/catalog",{method:"GET"}),admin:e=>t(`/theme/admin${e?`?id=${encodeURIComponent(e)}`:""}`,{method:"GET"}),themeTokens:e=>t(`/theme/themes/${encodeURIComponent(e)}`,{method:"GET"}),compile:e=>t("/theme/compile",{method:"POST",body:JSON.stringify(e)})},navigation:{get:()=>t("/navigation",{method:"GET"}),save:e=>t("/navigation",{method:"PUT",body:JSON.stringify(e)})},menus:{list:()=>t("/menus",{method:"GET"}),get:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"})},contextMenus:{list:()=>t("/context-menus",{method:"GET"}),get:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/context-menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/context-menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"}),actionTypes:()=>t("/context-menus/action-types",{method:"GET"})},menuLocations:{get:()=>t("/menu-locations",{method:"GET"}),save:e=>t("/menu-locations",{method:"PUT",body:JSON.stringify(e)}),registry:()=>t("/menu-locations/registry",{method:"GET"})},projects:{list:()=>t("/projects",{method:"GET"}),listWithCounts:()=>t("/projects?counts=1",{method:"GET"}),get:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/projects",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/projects/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"DELETE"}),artefacts:e=>t(`/projects/${encodeURIComponent(e)}/artefacts`,{method:"GET"}),untagAll:e=>t(`/projects/${encodeURIComponent(e)}/untag-all`,{method:"POST"}),removeContents:(e,o)=>t(`/projects/${encodeURIComponent(e)}/remove-contents`,{method:"POST",body:JSON.stringify({confirm:o})})},apiTokens:{list:()=>t("/api-tokens",{method:"GET"}),create:e=>t("/api-tokens",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/api-tokens/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),revoke:e=>t(`/api-tokens/${encodeURIComponent(e)}`,{method:"DELETE"})},apiEndpoints:{list:()=>t("/api-endpoints",{method:"GET"}),get:e=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/api-endpoints",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"DELETE"})},layouts:{get:()=>t("/layouts",{method:"GET"}),save:e=>t("/layouts",{method:"PUT",body:JSON.stringify(e)}),create:e=>t("/layouts",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/layouts/${e}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/layouts/${e}`,{method:"DELETE"}),getOptions:()=>t("/layouts/options",{method:"GET"}),saveOptions:e=>t("/layouts/options",{method:"PUT",body:JSON.stringify(e)})},media:{list:()=>t("/media",{method:"GET"}),upload:e=>b("/media",e),delete:e=>t(`/media/${encodeURIComponent(e)}`,{method:"DELETE"}),rename:(e,o)=>t(`/media/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({newName:o})}),info:e=>t(`/media/${encodeURIComponent(e)}/info`,{method:"GET"}),transform:(e,o)=>t(`/media/${encodeURIComponent(e)}/transform`,{method:"POST",body:JSON.stringify(o)})},users:{list:()=>t("/users",{method:"GET"}),get:e=>t(`/users/${e}`,{method:"GET"}),create:e=>t("/users",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/users/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/users/${e}`,{method:"DELETE"}),passwordReset:(e,o)=>t(`/users/${encodeURIComponent(e)}/password-reset`,{method:"POST",body:JSON.stringify({via:o})}),cancelPasswordReset:e=>t(`/users/${encodeURIComponent(e)}/password-reset`,{method:"DELETE"})},tools:{list:()=>t("/tools",{method:"GET"}),update:(e,o)=>t(`/tools/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)})},plugins:{list:()=>t("/plugins",{method:"GET"}),scaffold:e=>t("/plugins/scaffold",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/plugins/${e}`,{method:"PUT",body:JSON.stringify(o)}),files:e=>t(`/plugins/${e}/files`,{method:"GET"}),readFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"GET"}),writeFile:(e,o,n)=>t(`/plugins/${e}/file`,{method:"PUT",body:JSON.stringify({path:o,content:n})}),deleteFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"DELETE"}),restart:()=>t("/plugins/restart",{method:"POST"}),policy:()=>t("/plugins/_policy",{method:"GET"}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"}),async exportBundle(e,{includeData:o=!1}={}){const n=a(),r=o?"?includeData=1":"",s=await fetch(`${m}/plugins/${encodeURIComponent(e)}/export${r}`,{headers:n?{Authorization:`Bearer ${n}`}:{}});if(!s.ok){const E=await s.json().catch(()=>({}));throw new Error(E.error||`Export failed (${s.status})`)}const i=s.headers.get("Content-Disposition")||"",d=/filename="([^"]+)"/.exec(i)?.[1],c=await s.blob(),h=URL.createObjectURL(c),l=document.createElement("a");l.href=h,l.download=d||`${e}.dcmsplugin`,document.body.appendChild(l),l.click(),l.remove(),URL.revokeObjectURL(h)},async installFile(e,{confirmUnsigned:o=!1,confirmUpgrade:n=!1,confirmDowngrade:r=!1}={}){const s=new FormData;o&&s.append("confirmUnsigned","true"),n&&s.append("confirmUpgrade","true"),r&&s.append("confirmDowngrade","true"),s.append("file",e);const i=a(),d=await fetch(`${m}/plugins/install-upload`,{method:"POST",headers:i?{Authorization:`Bearer ${i}`}:{},body:s}),c=await d.json().catch(()=>({}));if(d.status===409){const h=new Error(c.error||"Plugin already installed");throw h.code="CONFLICT",h.name=c.name,h.installedVersion=c.installedVersion,h.incomingVersion=c.incomingVersion,h}if(!d.ok)throw new Error(c.error||`Install failed (${d.status})`);return c}},marketplace:{catalogue:()=>t("/plugins/marketplace",{method:"GET"}),install:(e,o,{update:n=!1}={})=>t("/plugins/marketplace/install",{method:"POST",body:JSON.stringify({slug:e,version:o,update:n})}),uninstall:e=>t(`/plugins/marketplace/${encodeURIComponent(e)}`,{method:"DELETE"})},collections:{list:()=>t("/collections",{method:"GET"}),proStatus:()=>t("/collections/pro-status",{method:"GET"}),get:e=>t(`/collections/${e}`,{method:"GET"}),create:e=>t("/collections",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/collections/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/collections/${e}`,{method:"DELETE"}),listEntries:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${n?"?"+n:""}`,{method:"GET"})},getEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"GET"}),createEntry:(e,o)=>t(`/collections/${e}/entries`,{method:"POST",body:JSON.stringify({data:o})}),updateEntry:(e,o,n)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:n})}),deleteEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"DELETE"}),clearEntries:e=>t(`/collections/${e}/entries`,{method:"DELETE"}),import:(e,o)=>t(`/collections/${e}/import`,{method:"POST",body:JSON.stringify({entries:o})}),publicList:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/public${n?"?"+n:""}`,{method:"GET"})},getConnections:()=>t("/collections/connections",{method:"GET"}),saveConnections:e=>t("/collections/connections",{method:"PUT",body:JSON.stringify(e)}),migrateStorage:(e,o)=>t(`/collections/${e}/migrate-storage`,{method:"POST",body:JSON.stringify({storage:o})})},forms:{list:()=>t("/forms",{method:"GET"}),create:e=>t("/forms",{method:"POST",body:JSON.stringify(e)}),get:e=>t(`/forms/${e}`,{method:"GET"}),update:(e,o)=>t(`/forms/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/forms/${e}`,{method:"DELETE"}),listSubmissions:e=>t(`/forms/${e}/submissions`,{method:"GET"}),clearSubmissions:e=>t(`/forms/${e}/submissions`,{method:"DELETE"}),deleteSubmission:(e,o)=>t(`/forms/${e}/submissions/${o}`,{method:"DELETE"}),testEmail:e=>t("/forms/test-email",{method:"POST",body:JSON.stringify({to:e})})},views:{list:()=>t("/views",{method:"GET"}),get:e=>t(`/views/${e}`,{method:"GET"}),create:e=>t("/views",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/views/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/views/${e}`,{method:"DELETE"}),execute:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/views/${e}/execute${n?"?"+n:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},versions:{list:e=>t(`/versions/list${e}`),get:(e,o)=>t(`/versions/get/${encodeURIComponent(o)}${e}`),create:(e,o)=>t(`/versions/create${e}`,{method:"POST",body:JSON.stringify({label:o})}),restore:(e,o)=>t(`/versions/restore/${encodeURIComponent(o)}${e}`,{method:"POST"}),delete:(e,o)=>t(`/versions/delete/${encodeURIComponent(o)}${e}`,{method:"DELETE"}),bulkDelete:(e,o)=>t(`/versions/bulk-delete${e}`,{method:"POST",body:JSON.stringify({filenames:o})}),prune:(e,o)=>t(`/versions/prune${e}`,{method:"POST",body:JSON.stringify({keep:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),n=await fetch(`${m}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||`Export failed (${n.status})`)}const r=await n.blob(),s=URL.createObjectURL(r),i=document.createElement("a");i.href=s,i.download=`${e}.dmblock.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${m}/blocks/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const r=await n.json().catch(()=>({})),s=new Error(r.error||"Block already exists");throw s.code="CONFLICT",s.name=r.name,s}if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`Import failed (${n.status})`)}return n.json()}},components:{list:()=>t("/components",{method:"GET"}),get:e=>t(`/components/${encodeURIComponent(e)}`,{method:"GET"}),compile:(e,o)=>t("/components/compile",{method:"POST",body:JSON.stringify({name:e,source:o})}),put:(e,o)=>t(`/components/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/components/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),n=await fetch(`${m}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||`Export failed (${n.status})`)}const r=await n.blob(),s=URL.createObjectURL(r),i=document.createElement("a");i.href=s,i.download=`${e}.dmcomponent.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${m}/components/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const r=await n.json().catch(()=>({})),s=new Error(r.error||"Component already exists");throw s.code="CONFLICT",s.name=r.name,s}if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`Import failed (${n.status})`)}return n.json()}},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})},system:{notifications:{list:()=>t("/system/notifications",{method:"GET"}),unreadCount:()=>t("/system/notifications/unread-count",{method:"GET"}),markRead:e=>t(`/system/notifications/${e}/read`,{method:"POST"}),dismiss:e=>t(`/system/notifications/${e}/dismiss`,{method:"POST"}),remove:e=>t(`/system/notifications/${e}`,{method:"DELETE"})}},dashboard:{summary:()=>t("/dashboard/summary",{method:"GET"}),summaryLite:()=>t("/dashboard/summary?lite=1",{method:"GET"})}};export function isAuthenticated(){return!!a()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:n}){e&&f(e),o&&S.set("auth_refresh_token",o),n&&($(n.id),S.set("auth_user",n))}let p=!1,T=!1;export function sendToLogin({expired:e=!1}={}){/^#\/(login|reset-password)(\?|\/|$)/.test(location.hash)||(location.hash=O(location.hash,{expired:e}))}export function logout(){!T&&window.M?.subscribe&&(T=!0,M.subscribe("router:afterChange",({to:e}={})=>{/^\/login(\?|$)/.test(e?.path||"")||(p=!1)})),p=!0,R.navigate("/login")}export function takePendingLogout(){return p?(p=!1,P(),!0):!1}function P(){const e=u();e&&fetch(`${m}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),y()}export{t as apiRequest,g as refreshAccessToken,a as getToken,y as clearAuth};
1
+ import{claimDrafts as $}from"./lib/draft-owner.js";import{loginHashFor as O}from"./lib/login-arrange.js";const m="/api";function a(){return S.get("auth_token")}function u(){return S.get("auth_refresh_token")}function f(e){S.set("auth_token",e)}function y(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function g(){const e=u();if(!e)throw new Error("No refresh token");const o=await fetch(`${m}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw y(),sendToLogin({expired:!0}),new Error("Token refresh failed");const{token:n}=await o.json();return f(n),n}async function t(e,o={}){let n=a();const i=r=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...r?{Authorization:`Bearer ${r}`}:{}});let s=await fetch(`${m}${e}`,{...o,headers:i(n)});if(s.status===401&&u())try{n=await g(),s=await fetch(`${m}${e}`,{...o,headers:i(n)})}catch{return}if(!s.ok){const r=await s.json().catch(()=>({error:"Request failed"})),d=new Error(r.error||r.message||`HTTP ${s.status}`);throw Array.isArray(r.validation)&&(d.validation=r.validation),d.status=s.status,d.body=r,d}return s.status===204?null:s.json()}async function b(e,o){const n=a(),i=n?{Authorization:`Bearer ${n}`}:{},s=await fetch(`${m}${e}`,{method:"POST",headers:i,body:o});if(!s.ok){const r=await s.json().catch(()=>({error:"Upload failed"}));throw new Error(r.error||r.message||`HTTP ${s.status}`)}return s.json()}export const api={auth:{setupStatus:()=>t("/auth/setup-status",{method:"GET"}),setup:e=>t("/auth/setup",{method:"POST",body:JSON.stringify(e)}),login:e=>t("/auth/login",{method:"POST",body:JSON.stringify(e)}),verifyLogin:e=>t("/auth/login/verify",{method:"POST",body:JSON.stringify(e)}),me:()=>t("/auth/me",{method:"GET"}),updateMe:e=>t("/auth/me",{method:"PUT",body:JSON.stringify(e)}),refresh:e=>t("/auth/refresh",{method:"POST",body:JSON.stringify({refreshToken:e})}),forgotPassword:e=>t("/auth/forgot-password",{method:"POST",body:JSON.stringify({email:e})}),checkReset:(e,o)=>t("/auth/reset-password/check",{method:"POST",body:JSON.stringify({token:e,...o&&{password:o}})}),resetPassword:(e,o)=>t("/auth/reset-password",{method:"POST",body:JSON.stringify({token:e,password:o})})},pages:{list:()=>t("/pages",{method:"GET"}),get:e=>t(`/pages${e}`,{method:"GET"}),create:e=>t("/pages",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/pages${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/pages${e}`,{method:"DELETE"}),preview:e=>t("/pages/preview",{method:"POST",body:JSON.stringify({markdown:e})}),previewFull:(e,o,n,i)=>t("/pages/preview/full",{method:"POST",body:JSON.stringify({urlPath:e,frontmatter:o,body:n,...i&&Object.keys(i).length?{blocks:i}:{}})}),previewLinks:{list:e=>t(`/pages/preview-links${e?`?urlPath=${encodeURIComponent(e)}`:""}`,{method:"GET"}).then(o=>o.links||[]),create:(e,o,n)=>t("/pages/preview-links",{method:"POST",body:JSON.stringify({urlPath:e,expiresIn:o,label:n})}),revoke:e=>t(`/pages/preview-links/${e}`,{method:"DELETE"})},tags:()=>t("/pages/tags",{method:"GET"}).then(e=>e.tags||[])},listTags:{list:e=>t(`/list-tags/${e}`,{method:"GET"}).then(o=>o.tags||[]),add:(e,o)=>t(`/list-tags/${e}`,{method:"POST",body:JSON.stringify({name:o})}),changeItem:(e,o,n)=>t(`/list-tags/${e}/item`,{method:"PUT",body:JSON.stringify({id:o,...n})}).then(i=>i.tags||[]),rename:(e,o,n)=>t(`/list-tags/${e}/rename`,{method:"POST",body:JSON.stringify({from:o,to:n})}),remove:(e,o)=>t(`/list-tags/${e}/delete`,{method:"POST",body:JSON.stringify({name:o})})},settings:{get:()=>t("/settings",{method:"GET"}),save:e=>t("/settings",{method:"PUT",body:JSON.stringify(e)}),getCustomCss:()=>t("/settings/custom-css",{method:"GET"}),saveCustomCss:e=>t("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:e})})},theme:{get:()=>t("/theme",{method:"GET"}),save:e=>t("/theme",{method:"PUT",body:JSON.stringify(e)}),catalog:()=>t("/theme/catalog",{method:"GET"}),admin:e=>t(`/theme/admin${e?`?id=${encodeURIComponent(e)}`:""}`,{method:"GET"}),themeTokens:e=>t(`/theme/themes/${encodeURIComponent(e)}`,{method:"GET"}),compile:e=>t("/theme/compile",{method:"POST",body:JSON.stringify(e)})},navigation:{get:()=>t("/navigation",{method:"GET"}),save:e=>t("/navigation",{method:"PUT",body:JSON.stringify(e)})},menus:{list:()=>t("/menus",{method:"GET"}),get:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"})},contextMenus:{list:()=>t("/context-menus",{method:"GET"}),get:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/context-menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/context-menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"}),actionTypes:()=>t("/context-menus/action-types",{method:"GET"})},menuLocations:{get:()=>t("/menu-locations",{method:"GET"}),save:e=>t("/menu-locations",{method:"PUT",body:JSON.stringify(e)}),registry:()=>t("/menu-locations/registry",{method:"GET"})},projects:{list:()=>t("/projects",{method:"GET"}),listWithCounts:()=>t("/projects?counts=1",{method:"GET"}),get:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/projects",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/projects/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"DELETE"}),artefacts:e=>t(`/projects/${encodeURIComponent(e)}/artefacts`,{method:"GET"}),untagAll:e=>t(`/projects/${encodeURIComponent(e)}/untag-all`,{method:"POST"}),removeContents:(e,o)=>t(`/projects/${encodeURIComponent(e)}/remove-contents`,{method:"POST",body:JSON.stringify({confirm:o})})},apiTokens:{list:()=>t("/api-tokens",{method:"GET"}),create:e=>t("/api-tokens",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/api-tokens/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),revoke:e=>t(`/api-tokens/${encodeURIComponent(e)}`,{method:"DELETE"})},apiEndpoints:{list:()=>t("/api-endpoints",{method:"GET"}),get:e=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/api-endpoints",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"DELETE"})},layouts:{get:()=>t("/layouts",{method:"GET"}),save:e=>t("/layouts",{method:"PUT",body:JSON.stringify(e)}),create:e=>t("/layouts",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/layouts/${e}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/layouts/${e}`,{method:"DELETE"}),getOptions:()=>t("/layouts/options",{method:"GET"}),saveOptions:e=>t("/layouts/options",{method:"PUT",body:JSON.stringify(e)})},media:{list:()=>t("/media",{method:"GET"}),upload:e=>b("/media",e),delete:e=>t(`/media/${encodeURIComponent(e)}`,{method:"DELETE"}),rename:(e,o)=>t(`/media/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({newName:o})}),info:e=>t(`/media/${encodeURIComponent(e)}/info`,{method:"GET"}),transform:(e,o)=>t(`/media/${encodeURIComponent(e)}/transform`,{method:"POST",body:JSON.stringify(o)})},users:{list:()=>t("/users",{method:"GET"}),get:e=>t(`/users/${e}`,{method:"GET"}),create:e=>t("/users",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/users/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/users/${e}`,{method:"DELETE"}),passwordReset:(e,o)=>t(`/users/${encodeURIComponent(e)}/password-reset`,{method:"POST",body:JSON.stringify({via:o})}),cancelPasswordReset:e=>t(`/users/${encodeURIComponent(e)}/password-reset`,{method:"DELETE"})},tools:{list:()=>t("/tools",{method:"GET"}),update:(e,o)=>t(`/tools/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)})},plugins:{list:()=>t("/plugins",{method:"GET"}),scaffold:e=>t("/plugins/scaffold",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/plugins/${e}`,{method:"PUT",body:JSON.stringify(o)}),files:e=>t(`/plugins/${e}/files`,{method:"GET"}),readFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"GET"}),writeFile:(e,o,n)=>t(`/plugins/${e}/file`,{method:"PUT",body:JSON.stringify({path:o,content:n})}),deleteFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"DELETE"}),restart:()=>t("/plugins/restart",{method:"POST"}),policy:()=>t("/plugins/_policy",{method:"GET"}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"}),async exportBundle(e,{includeData:o=!1}={}){const n=a(),i=o?"?includeData=1":"",s=await fetch(`${m}/plugins/${encodeURIComponent(e)}/export${i}`,{headers:n?{Authorization:`Bearer ${n}`}:{}});if(!s.ok){const E=await s.json().catch(()=>({}));throw new Error(E.error||`Export failed (${s.status})`)}const r=s.headers.get("Content-Disposition")||"",d=/filename="([^"]+)"/.exec(r)?.[1],c=await s.blob(),h=URL.createObjectURL(c),l=document.createElement("a");l.href=h,l.download=d||`${e}.dcmsplugin`,document.body.appendChild(l),l.click(),l.remove(),URL.revokeObjectURL(h)},async installFile(e,{confirmUnsigned:o=!1,confirmUpgrade:n=!1,confirmDowngrade:i=!1}={}){const s=new FormData;o&&s.append("confirmUnsigned","true"),n&&s.append("confirmUpgrade","true"),i&&s.append("confirmDowngrade","true"),s.append("file",e);const r=a(),d=await fetch(`${m}/plugins/install-upload`,{method:"POST",headers:r?{Authorization:`Bearer ${r}`}:{},body:s}),c=await d.json().catch(()=>({}));if(d.status===409){const h=new Error(c.error||"Plugin already installed");throw h.code="CONFLICT",h.name=c.name,h.installedVersion=c.installedVersion,h.incomingVersion=c.incomingVersion,h}if(!d.ok)throw new Error(c.error||`Install failed (${d.status})`);return c}},marketplace:{catalogue:()=>t("/plugins/marketplace",{method:"GET"}),install:(e,o,{update:n=!1}={})=>t("/plugins/marketplace/install",{method:"POST",body:JSON.stringify({slug:e,version:o,update:n})}),uninstall:e=>t(`/plugins/marketplace/${encodeURIComponent(e)}`,{method:"DELETE"})},collections:{list:()=>t("/collections",{method:"GET"}),proStatus:()=>t("/collections/pro-status",{method:"GET"}),get:e=>t(`/collections/${e}`,{method:"GET"}),create:e=>t("/collections",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/collections/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/collections/${e}`,{method:"DELETE"}),listEntries:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${n?"?"+n:""}`,{method:"GET"})},getEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"GET"}),createEntry:(e,o)=>t(`/collections/${e}/entries`,{method:"POST",body:JSON.stringify({data:o})}),updateEntry:(e,o,n)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:n})}),deleteEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"DELETE"}),clearEntries:e=>t(`/collections/${e}/entries`,{method:"DELETE"}),import:(e,o)=>t(`/collections/${e}/import`,{method:"POST",body:JSON.stringify({entries:o})}),publicList:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/public${n?"?"+n:""}`,{method:"GET"})},getConnections:()=>t("/collections/connections",{method:"GET"}),saveConnections:e=>t("/collections/connections",{method:"PUT",body:JSON.stringify(e)}),migrateStorage:(e,o)=>t(`/collections/${e}/migrate-storage`,{method:"POST",body:JSON.stringify({storage:o})})},forms:{list:()=>t("/forms",{method:"GET"}),create:e=>t("/forms",{method:"POST",body:JSON.stringify(e)}),get:e=>t(`/forms/${e}`,{method:"GET"}),update:(e,o)=>t(`/forms/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/forms/${e}`,{method:"DELETE"}),listSubmissions:e=>t(`/forms/${e}/submissions`,{method:"GET"}),clearSubmissions:e=>t(`/forms/${e}/submissions`,{method:"DELETE"}),deleteSubmission:(e,o)=>t(`/forms/${e}/submissions/${o}`,{method:"DELETE"}),testEmail:e=>t("/forms/test-email",{method:"POST",body:JSON.stringify({to:e})})},views:{list:()=>t("/views",{method:"GET"}),get:e=>t(`/views/${e}`,{method:"GET"}),create:e=>t("/views",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/views/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/views/${e}`,{method:"DELETE"}),execute:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/views/${e}/execute${n?"?"+n:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},versions:{list:e=>t(`/versions/list${e}`),get:(e,o)=>t(`/versions/get/${encodeURIComponent(o)}${e}`),create:(e,o)=>t(`/versions/create${e}`,{method:"POST",body:JSON.stringify({label:o})}),restore:(e,o)=>t(`/versions/restore/${encodeURIComponent(o)}${e}`,{method:"POST"}),delete:(e,o)=>t(`/versions/delete/${encodeURIComponent(o)}${e}`,{method:"DELETE"}),bulkDelete:(e,o)=>t(`/versions/bulk-delete${e}`,{method:"POST",body:JSON.stringify({filenames:o})}),prune:(e,o)=>t(`/versions/prune${e}`,{method:"POST",body:JSON.stringify({keep:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),n=await fetch(`${m}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||`Export failed (${n.status})`)}const i=await n.blob(),s=URL.createObjectURL(i),r=document.createElement("a");r.href=s,r.download=`${e}.dmblock.json`,document.body.appendChild(r),r.click(),r.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${m}/blocks/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const i=await n.json().catch(()=>({})),s=new Error(i.error||"Block already exists");throw s.code="CONFLICT",s.name=i.name,s}if(!n.ok){const i=await n.json().catch(()=>({}));throw new Error(i.error||`Import failed (${n.status})`)}return n.json()}},components:{list:()=>t("/components",{method:"GET"}),get:e=>t(`/components/${encodeURIComponent(e)}`,{method:"GET"}),compile:(e,o)=>t("/components/compile",{method:"POST",body:JSON.stringify({name:e,source:o})}),put:(e,o)=>t(`/components/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/components/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),n=await fetch(`${m}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||`Export failed (${n.status})`)}const i=await n.blob(),s=URL.createObjectURL(i),r=document.createElement("a");r.href=s,r.download=`${e}.dmcomponent.json`,document.body.appendChild(r),r.click(),r.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${m}/components/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const i=await n.json().catch(()=>({})),s=new Error(i.error||"Component already exists");throw s.code="CONFLICT",s.name=i.name,s}if(!n.ok){const i=await n.json().catch(()=>({}));throw new Error(i.error||`Import failed (${n.status})`)}return n.json()}},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})},system:{notifications:{list:()=>t("/system/notifications",{method:"GET"}),unreadCount:()=>t("/system/notifications/unread-count",{method:"GET"}),markRead:e=>t(`/system/notifications/${e}/read`,{method:"POST"}),dismiss:e=>t(`/system/notifications/${e}/dismiss`,{method:"POST"}),remove:e=>t(`/system/notifications/${e}`,{method:"DELETE"})}},dashboard:{summary:()=>t("/dashboard/summary",{method:"GET"}),summaryLite:()=>t("/dashboard/summary?lite=1",{method:"GET"})}};export function isAuthenticated(){return!!a()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:n}){e&&f(e),o&&S.set("auth_refresh_token",o),n&&($(n.id),S.set("auth_user",n))}let p=!1,T=!1;export function sendToLogin({expired:e=!1}={}){/^#\/(login|reset-password)(\?|\/|$)/.test(location.hash)||(location.hash=O(location.hash,{expired:e}))}export function logout(){!T&&window.M?.subscribe&&(T=!0,M.subscribe("router:afterChange",({to:e}={})=>{/^\/login(\?|$)/.test(e?.path||"")||(p=!1)})),p=!0,R.navigate("/login")}export function takePendingLogout(){return p?(p=!1,P(),!0):!1}function P(){const e=u();e&&fetch(`${m}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),y()}export{t as apiRequest,g as refreshAccessToken,a as getToken,y as clearAuth};
@@ -1,4 +1,4 @@
1
- import{fmtDate as M}from"./dates.js";export const NAME_RE=/^[a-z0-9][a-z0-9-]*$/,MAX_CSS=50*1024,FORM_FIELDS=["name","project","bundled","content","css"];export function formFromBlock(e,{project:r=""}={}){return e?{name:String(e.name||""),project:String(e.meta?.project||""),bundled:e.bundled===!0,content:String(e.content??""),css:String(e.css??"")}:{name:"",project:String(r||""),bundled:!1,content:"",css:""}}export function snapshot(e){const r=e||{};return JSON.stringify(FORM_FIELDS.map(n=>n==="bundled"?r[n]===!0:String(r[n]??"")))}export function checkForm(e,{isNew:r,taken:n=[]}={}){const t=String(e?.name||"").trim(),o={};r&&(t?NAME_RE.test(t)?n.includes(t)&&(o.name=`"${t}" is already a block - open it from the list, or choose another name.`):o.name="Lowercase letters, digits and hyphens, starting with a letter or digit.":o.name="A block needs a name.");const i=String(e?.css??"");return i.length>MAX_CSS&&(o.css=`The CSS is ${Math.ceil(i.length/1024)} KB - a block's CSS is limited to 50 KB.`),{errors:o,name:t}}export function payloadFromForm(e,r={}){const{adminTags:n,...t}=r||{};return{content:String(e?.content??""),bundled:e?.bundled===!0,css:String(e?.css??""),meta:{...t,project:String(e?.project||"")||null}}}export function extractPlaceholders(e){const r=new Set;for(const[,n]of y(e).matchAll(/\{\{([\w_]+)(?::[^{}]+)?\}\}/g))r.add(n);return[...r]}export function rawPlaceholders(e){return new Set([...y(e).matchAll(/\{\{\{(\w+)\}\}\}/g)].map(r=>r[1]))}const y=e=>String(e??"").replace(/<!--[\s\S]*?-->/g,""),d=e=>String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");export{d as escapeHtml};export function formatDate(e,r){if(e==null||e==="")return null;const n=e instanceof Date?e:new Date(e);if(Number.isNaN(n.getTime()))return null;const t=(a,l=2)=>String(a).padStart(l,"0"),o=["January","February","March","April","May","June","July","August","September","October","November","December"],i=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],s=()=>(n.getHours()+11)%12+1,c={YYYY:()=>n.getFullYear(),YY:()=>t(n.getFullYear()%100),MMMM:()=>o[n.getMonth()],MMM:()=>o[n.getMonth()].slice(0,3),MM:()=>t(n.getMonth()+1),M:()=>n.getMonth()+1,DD:()=>t(n.getDate()),D:()=>n.getDate(),dddd:()=>i[n.getDay()],ddd:()=>i[n.getDay()].slice(0,3),HH:()=>t(n.getHours()),H:()=>n.getHours(),hh:()=>t(s()),h:()=>s(),mm:()=>t(n.getMinutes()),m:()=>n.getMinutes(),ss:()=>t(n.getSeconds()),s:()=>n.getSeconds(),A:()=>n.getHours()<12?"AM":"PM",a:()=>n.getHours()<12?"am":"pm"};return String(r).replace(/YYYY|YY|MMMM|MMM|MM|M|DD|D|dddd|ddd|HH|H|hh|h|mm|m|ss|s|A|a/g,a=>String(c[a]()))}const S={icon:"box","facebook-icon":"facebook","instagram-icon":"instagram"};export const SAMPLE_IMAGE="data:image/svg+xml,"+encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="800" height="500" viewBox="0 0 800 500"><rect width="800" height="500" fill="#94a3b8"/><path d="M0 500 L260 230 L420 390 L540 290 L800 500 Z" fill="#64748b"/><circle cx="610" cy="140" r="52" fill="#cbd5e1"/></svg>');const v=/(image|img|photo|picture|avatar|logo|thumb|thumbnail|cover|banner)(url|src)?$/;export function sampleValue(e,r=new Date,{raw:n=!1}={}){const t=String(e).toLowerCase();return S[t]?S[t]:/(class|style)$/.test(t)?"":t==="_id"?"a1b2c3d4-e5f6-7890-abcd-ef1234567890":t==="_createdat"||t==="_updatedat"?r.toISOString():t.replace(/update(?!d)/g,"").includes("date")||/(published|created|updated)at$/.test(t)?M(r):v.test(t)?n?t.includes("avatar")?`<img src="${SAMPLE_IMAGE}" alt="" width="40" height="40">`:`<img src="${SAMPLE_IMAGE}" alt="">`:SAMPLE_IMAGE:/(url|href|link)$/.test(t)?"#":t.includes("email")?"user@example.com":t.includes("phone")?"+44 7700 900000":t.includes("name")?"Jane Smith":t.includes("title")?"Sample Title":["message","content","description","summary","excerpt"].some(o=>t.includes(o))?"This is a sample value for preview purposes.":t.includes("rating")?"excellent":t.includes("status")?"active":t.includes("priority")?"high":t.includes("tag")?"tag1, tag2":t.includes("subject")?"Sample Subject":t.includes("categor")?n?'<a href="#">General</a>':"general":`Sample ${e}`}export const scopeName=e=>String(e||"").trim()||"preview",PREVIEW_REPEATS=[1,3];export function renderPreview(e,r={},{repeat:n=1,sanitize:t=null}={}){if(typeof t!="function")return"";const o=u=>r[u]??`[${u}]`,i=String(e?.content??"").replace(/<!--[\s\S]*?-->/g,"").replace(/\{\{\{(\w+)\}\}\}/g,(u,g)=>String(o(g))).replace(/\{\{([\w_]+)(?::([^{}]+))?\}\}/g,(u,g,p)=>{const f=o(g);if(p){const h=formatDate(f,p);if(h!==null)return d(h)}return d(f)}),s=d(scopeName(e?.name)),c=String(e?.css??""),a=c.trim()?`<style>[data-block="${s}"] { ${c.replace(/<\/style/gi,"<\\/style")} }</style>`:"",l=PREVIEW_REPEATS.includes(Number(n))?Number(n):1,m=String(t(i)??"");if(l===1)return`${a}<div class="dm-static-block" data-block="${s}">${m}</div>`;const b=`<div data-block="${s}">${m}</div>`;return`${a}<div class="dm-collection-display dm-collection-blocks grid grid-cols-1 grid-cols-md-2 grid-cols-lg-3 gap-4">`+Array.from({length:l},()=>b).join("")+"</div>"}export function previewBridge(e,r){const n=JSON.stringify(Number(e)||0),t=JSON.stringify(String(r)).replace(/</g,"\\u003c");return`<script>(function () {
1
+ import{fmtDate as v}from"./dates.js";export const NAME_RE=/^[a-z0-9][a-z0-9-]*$/,MAX_CSS=50*1024,FORM_FIELDS=["name","project","bundled","content","css"];export function formFromBlock(e,{project:r=""}={}){return e?{name:String(e.name||""),project:String(e.meta?.project||""),bundled:e.bundled===!0,content:String(e.content??""),css:String(e.css??"")}:{name:"",project:String(r||""),bundled:!1,content:"",css:""}}export function snapshot(e){const r=e||{};return JSON.stringify(FORM_FIELDS.map(n=>n==="bundled"?r[n]===!0:String(r[n]??"")))}export function checkForm(e,{isNew:r,taken:n=[]}={}){const t=String(e?.name||"").trim(),o={};r&&(t?NAME_RE.test(t)?n.includes(t)&&(o.name=`"${t}" is already a block - open it from the list, or choose another name.`):o.name="Lowercase letters, digits and hyphens, starting with a letter or digit.":o.name="A block needs a name.");const i=String(e?.css??"");return i.length>MAX_CSS&&(o.css=`The CSS is ${Math.ceil(i.length/1024)} KB - a block's CSS is limited to 50 KB.`),{errors:o,name:t}}export function payloadFromForm(e,r={}){const{adminTags:n,...t}=r||{};return{content:String(e?.content??""),bundled:e?.bundled===!0,css:String(e?.css??""),meta:{...t,project:String(e?.project||"")||null}}}export function extractPlaceholders(e){const r=new Set;for(const[,n]of y(e).matchAll(/\{\{([\w_]+)(?::[^{}]+)?\}\}/g))r.add(n);return[...r]}export function rawPlaceholders(e){return new Set([...y(e).matchAll(/\{\{\{(\w+)\}\}\}/g)].map(r=>r[1]))}const y=e=>String(e??"").replace(/<!--[\s\S]*?-->/g,""),d=e=>String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");export{d as escapeHtml};export function formatDate(e,r){if(e==null||e==="")return null;const n=e instanceof Date?e:new Date(e);if(Number.isNaN(n.getTime()))return null;const t=(c,l=2)=>String(c).padStart(l,"0"),o=["January","February","March","April","May","June","July","August","September","October","November","December"],i=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],s=()=>(n.getHours()+11)%12+1,a={YYYY:()=>n.getFullYear(),YY:()=>t(n.getFullYear()%100),MMMM:()=>o[n.getMonth()],MMM:()=>o[n.getMonth()].slice(0,3),MM:()=>t(n.getMonth()+1),M:()=>n.getMonth()+1,DD:()=>t(n.getDate()),D:()=>n.getDate(),dddd:()=>i[n.getDay()],ddd:()=>i[n.getDay()].slice(0,3),HH:()=>t(n.getHours()),H:()=>n.getHours(),hh:()=>t(s()),h:()=>s(),mm:()=>t(n.getMinutes()),m:()=>n.getMinutes(),ss:()=>t(n.getSeconds()),s:()=>n.getSeconds(),A:()=>n.getHours()<12?"AM":"PM",a:()=>n.getHours()<12?"am":"pm"};return String(r).replace(/YYYY|YY|MMMM|MMM|MM|M|DD|D|dddd|ddd|HH|H|hh|h|mm|m|ss|s|A|a/g,c=>String(a[c]()))}const b={icon:"box","facebook-icon":"facebook","instagram-icon":"instagram"};export const SAMPLE_IMAGE="data:image/svg+xml,"+encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="800" height="500" viewBox="0 0 800 500"><rect width="800" height="500" fill="#94a3b8"/><path d="M0 500 L260 230 L420 390 L540 290 L800 500 Z" fill="#64748b"/><circle cx="610" cy="140" r="52" fill="#cbd5e1"/></svg>');const w=/(image|img|photo|picture|avatar|logo|thumb|thumbnail|cover|banner)(url|src)?$/;export function sampleValue(e,r=new Date,{raw:n=!1}={}){const t=String(e).toLowerCase();return b[t]?b[t]:/(class|style)$/.test(t)?"":t==="_id"?"a1b2c3d4-e5f6-7890-abcd-ef1234567890":t==="_createdat"||t==="_updatedat"?r.toISOString():t.replace(/update(?!d)/g,"").includes("date")||/(published|created|updated)at$/.test(t)?v(r):w.test(t)?n?t.includes("avatar")?`<img src="${SAMPLE_IMAGE}" alt="" width="40" height="40">`:`<img src="${SAMPLE_IMAGE}" alt="">`:SAMPLE_IMAGE:/(url|href|link)$/.test(t)?"#":t.includes("email")?"user@example.com":t.includes("phone")?"+44 7700 900000":t.includes("name")?"Jane Smith":t.includes("title")?"Sample Title":["message","content","description","summary","excerpt"].some(o=>t.includes(o))?"This is a sample value for preview purposes.":t.includes("rating")?"excellent":t.includes("status")?"active":t.includes("priority")?"high":t.includes("tag")?"tag1, tag2":t.includes("subject")?"Sample Subject":t.includes("categor")?n?'<a href="#">General</a>':"general":`Sample ${e}`}export const scopeName=e=>String(e||"").trim()||"preview",PREVIEW_REPEATS=[1,3];export function renderPreview(e,r={},{repeat:n=1,sanitize:t=null}={}){if(typeof t!="function")return"";const o=u=>r[u]??`[${u}]`,i=String(e?.content??"").replace(/<!--[\s\S]*?-->/g,"").replace(/\{\{\{(\w+)\}\}\}/g,(u,p)=>String(o(p))).replace(/\{\{([\w_]+)(?::([^{}]+))?\}\}/g,(u,p,g)=>{const f=o(p);if(g){const h=formatDate(f,g);if(h!==null)return d(h)}return d(f)}),s=d(scopeName(e?.name)),a=String(e?.css??""),c=a.trim()?`<style>[data-block="${s}"] { ${a.replace(/<\/style/gi,"<\\/style")} }</style>`:"",l=PREVIEW_REPEATS.includes(Number(n))?Number(n):1,m=String(t(i)??"");if(l===1)return`${c}<div class="dm-static-block" data-block="${s}">${m}</div>`;const S=`<div data-block="${s}">${m}</div>`;return`${c}<div class="dm-collection-display dm-collection-blocks grid grid-cols-1 grid-cols-md-2 grid-cols-lg-3 gap-4">`+Array.from({length:l},()=>S).join("")+"</div>"}export function previewBridge(e,r){const n=JSON.stringify(Number(e)||0),t=JSON.stringify(String(r)).replace(/</g,"\\u003c");return`<script>(function () {
2
2
  var P = window.parent, TICK = ${n}, HOST = ${t};
3
3
  function host() { return document.getElementById(HOST); }
4
4
  function up(msg) { msg.source = 'dm-block-preview'; msg.tick = TICK; P.postMessage(msg, '*'); }
@@ -33,5 +33,14 @@ function ready() {
33
33
  up({type: 'ready', host: !!host()});
34
34
  }
35
35
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', ready); else ready();
36
- })();<\/script>`}export function previewShell(e,r,n){const t=`<style>body.dm-cloaked{opacity:1 !important;transition:none !important}#${n}{padding:1.5rem 0}.dm-breadcrumbs{display:none !important}body.dm-vnav-left,body.dm-vnav-right{padding-left:0 !important;padding-right:0 !important}</style>`;let o=String(e??"");o=/<\/head>/i.test(o)?o.replace(/<\/head>/i,`${t}</head>`):t+o;const i=previewBridge(r,n),s=o.toLowerCase().lastIndexOf("</body>");return s>=0?o.slice(0,s)+i+o.slice(s):o+i}export function previewMessage(e,r,n){if(!e||!r||e.source!==r)return null;const t=e.data;return!t||t.source!=="dm-block-preview"||t.tick!==n?null:["ready","save"].includes(t.type)?t:null}const w=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);export function formatHtml(e){const r=String(e??"").match(/(<[^>]+>|[^<]+)/g)||[],n=[];let t=0;for(const o of r){const i=o.trim();if(i)if(i.startsWith("</"))t=Math.max(0,t-1),n.push(" ".repeat(t)+i);else if(i.startsWith("<")&&!i.startsWith("<!")&&!i.endsWith("/>")){n.push(" ".repeat(t)+i);const s=(i.match(/^<(\w+)/)||[])[1]||"";w.has(s.toLowerCase())||t++}else n.push(" ".repeat(t)+i)}return n.join(`
37
- `)}export const draftKey=e=>`block-editor.draft.${e||"new"}`;export function draftOffer(e,r){return!e||typeof e!="object"||!e.form||snapshot(e.form)===r?null:{form:e.form,stale:!!e.base&&e.base!==r,at:e.at||""}}export const shortcodeFor=e=>`[collection slug="\u2026" display="block" block="${e}" /]`;
36
+ })();<\/script>`}export function previewShell(e,r,n){const t=`<style>body.dm-cloaked{opacity:1 !important;transition:none !important}#${n}{padding:1.5rem 0}.dm-breadcrumbs{display:none !important}body.dm-vnav-left,body.dm-vnav-right{padding-left:0 !important;padding-right:0 !important}</style>`;let o=String(e??"");o=/<\/head>/i.test(o)?o.replace(/<\/head>/i,`${t}</head>`):t+o;const i=previewBridge(r,n),s=o.toLowerCase().lastIndexOf("</body>");return s>=0?o.slice(0,s)+i+o.slice(s):o+i}export function previewMessage(e,r,n){if(!e||!r||e.source!==r)return null;const t=e.data;return!t||t.source!=="dm-block-preview"||t.tick!==n?null:["ready","save"].includes(t.type)?t:null}const M=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);export function formatHtml(e){const r=String(e??"").match(/(<[^>]+>|[^<]+)/g)||[],n=[];let t=0;for(const o of r){const i=o.trim();if(i)if(i.startsWith("</"))t=Math.max(0,t-1),n.push(" ".repeat(t)+i);else if(i.startsWith("<")&&!i.startsWith("<!")&&!i.endsWith("/>")){n.push(" ".repeat(t)+i);const s=(i.match(/^<(\w+)/)||[])[1]||"";M.has(s.toLowerCase())||t++}else n.push(" ".repeat(t)+i)}return n.join(`
37
+ `)}export const draftKey=e=>`block-editor.draft.${e||"new"}`;export function draftOffer(e,r){return!e||typeof e!="object"||!e.form||snapshot(e.form)===r?null:{form:e.form,stale:!!e.base&&e.base!==r,at:e.at||""}}export const shortcodeFor=e=>`[collection slug="\u2026" display="block" block="${e}" /]`;const x=new Set(["urlPath","slug","content","html","cacheTags","perViewer","hasExpiry","usedComponents","updatedAt"]);export function pageFrontmatter(e){return Object.fromEntries(Object.entries(e||{}).filter(([r])=>!x.has(r)))}export function previewPageOptions(e,r,n=""){const t=new Map((Array.isArray(r)?r:[]).map(a=>[a.urlPath,a])),o=Array.isArray(e?.pages)?e.pages.filter(a=>t.has(a)):[],i=e?.footer?[...new Set([n,"/",...o].filter(a=>a&&t.has(a)))]:o;return(n&&i.includes(n)?[n,...i.filter(a=>a!==n)]:i).map(a=>({value:a,label:`${t.get(a)?.title||"Untitled"} (${a})`}))}export function pageInFrame(e,r,n=0){const t="<style>body.dm-cloaked{opacity:1 !important;transition:none !important}"+(/^[a-z0-9-]+$/.test(r)?`[data-block="${r}"]{outline:2px dashed var(--dm-primary,#4f6bed);outline-offset:6px}`:"")+"</style>",i=`<script>(function () {
38
+ var P = window.parent, TICK = ${JSON.stringify(Number(n)||0)};
39
+ document.addEventListener('click', function (e) {
40
+ if (e.target.closest && e.target.closest('a[href], button, [type="submit"]')) e.preventDefault();
41
+ }, true);
42
+ document.addEventListener('submit', function (e) { e.preventDefault(); }, true);
43
+ document.addEventListener('keydown', function (e) {
44
+ if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) { e.preventDefault(); P.postMessage({source: 'dm-block-preview', tick: TICK, type: 'save'}, '*'); }
45
+ }, true);
46
+ })();<\/script>`;let s=String(e??"");s=/<\/head>/i.test(s)?s.replace(/<\/head>/i,`${t}</head>`):t+s;const a=s.toLowerCase().lastIndexOf("</body>");return a>=0?s.slice(0,a)+i+s.slice(a):s+i}export function draftOlderThan(e,r){const n=Date.parse(e?.at||""),t=Date.parse(r||"");return Number.isFinite(n)&&Number.isFinite(t)&&t>n}
@@ -1,4 +1,4 @@
1
- import{draftKey as C}from"./page-editor-arrange.js";import{draftKey as N}from"./block-editor-arrange.js";import{hopItems as T,hopMatches as L,hopTarget as w,keyOf as r,orderHop as A,pathFor as m,pushRecent as H,recentChips as $}from"./editor-hop-arrange.js";const f="editor-hop.recent",p=new Set;let k=!1;function F(){k||!window.M?.subscribe||(k=!0,M.subscribe("router:afterChange",({to:e}={})=>{w(e?.path)||p.clear()}))}export const hold=e=>{p.add(r(e))},takeHeld=e=>p.delete(r(e));export function hasDraft(e){try{return!!S.get(e.kind==="page"?C(e.id):N(e.id))}catch{return!1}}function v(){try{const e=S.get(f);return Array.isArray(e)?e:[]}catch{return[]}}export function noteRecent(e){const t=H(v(),e);try{S.set(f,t)}catch{}return t}const K=`
1
+ import{draftKey as N}from"./page-editor-arrange.js";import{draftKey as g,draftOlderThan as L}from"./block-editor-arrange.js";import{hopItems as w,hopMatches as H,hopTarget as O,keyOf as l,orderHop as $,pathFor as k,pushRecent as A,recentChips as F}from"./editor-hop-arrange.js";const b="editor-hop.recent",p=new Set;let y=!1;function K(){y||!window.M?.subscribe||(y=!0,M.subscribe("router:afterChange",({to:e}={})=>{O(e?.path)||p.clear()}))}export const hold=e=>{p.add(l(e))},takeHeld=e=>p.delete(l(e));export function hasDraft(e){try{return!!S.get(e.kind==="page"?N(e.id):g(e.id))}catch{return!1}}function h(){try{const e=S.get(b);return Array.isArray(e)?e:[]}catch{return[]}}export function noteRecent(e){const t=A(h(),e);try{S.set(b,t)}catch{}return t}const B=`
2
2
  <div class="dm-hop">
3
3
  <div class="dm-hop-find">
4
4
  <span class="dm-hop-find-icon" data-icon="search" data-icon-size="14"></span>
@@ -13,7 +13,7 @@ import{draftKey as C}from"./page-editor-arrange.js";import{draftKey as N}from"./
13
13
  <span class="dm-hop-label">On this page</span>
14
14
  <div class="dm-hop-chips dm-hop-related" data-each="related.value key=key"></div>
15
15
  </div>
16
- </div>`,O=`
16
+ </div>`,D=`
17
17
  <button type="button" class="dm-hop-chip is-{{kind}}" data-path="{{path}}" title="{{title}}">
18
18
  <span class="dm-hop-kind">{{kindText}}</span><span class="dm-hop-name">{{label}}</span><span class="dm-hop-dot" aria-label="{{dotLabel}}">{{dot}}</span>
19
- </button>`,l=e=>e==="page"?"Page":"Block";function b(e){const t=e.unsaved??hasDraft(e);return{...e,key:r(e),path:m(e),kindText:l(e.kind),title:`${l(e.kind)}: ${e.kind==="page"?`${e.label} (${e.id})`:e.id}${t?" - unsaved changes kept":""}`,dot:t?"\u2022":"",dotLabel:t?"unsaved changes":""}}function B(e){const t=document.createElement("div");t.className="dm-hop-result";const d=document.createElement("span");d.className=`dm-hop-kind is-${e.kind}`,d.textContent=l(e.kind);const c=document.createElement("span");c.className="dm-hop-name",c.textContent=e.label;const s=document.createElement("small");if(s.className="dm-hop-meta",s.textContent=e.meta,t.append(d,c,s),hasDraft(e)){const n=document.createElement("span");n.className="dm-hop-dot",n.textContent="\u2022",n.title="Unsaved changes kept",t.appendChild(n)}return t}export function mountEditorHop(e,{current:t,pages:d=[],blocks:c=[]}){F(),e.innerHTML=K,e.querySelectorAll("[data-each]").forEach(a=>{a.innerHTML=O});const s=t?.id?noteRecent(t):v(),n=T(d,c),g=new Set(n.map(r)),i={recent:M.observable($(s.filter(a=>g.has(r(a))),t,hasDraft).map(b)),related:M.observable([])},y=M.applyBindings(i,e),h=e.querySelector(".dm-hop-input"),x=E.autocomplete?E.autocomplete(h,{data:n,minChars:0,maxResults:0,debounce:60,highlightMatches:!1,clearOnSelect:!0,emptyMessage:"No page or block matches",filterFn:L,onFilter:(a,o)=>A(a,o,{recent:s,current:t}),renderItem:a=>B(a),onSelect:a=>{h.blur(),R.navigate(m(a))}}):null,u=a=>{const o=a.target.closest(".dm-hop-chip");o?.dataset.path&&R.navigate(o.dataset.path)};return e.addEventListener("click",u),I.scan(e),{setRelated(a){const o=(Array.isArray(a)?a:[]).map(b);JSON.stringify(o)!==JSON.stringify(i.related.peek())&&(i.related.value=o)},dispose(){e.removeEventListener("click",u);try{x?.destroy?.()}catch{}y.dispose()}}}
19
+ </button>`,u=e=>e==="page"?"Page":"Block";function v(e){const t=e.unsaved??hasDraft(e);return{...e,key:l(e),path:k(e),kindText:u(e.kind),title:`${u(e.kind)}: ${e.kind==="page"?`${e.label} (${e.id})`:e.id}${t?" - unsaved changes kept":""}`,dot:t?"\u2022":"",dotLabel:t?"unsaved changes":""}}function P(e){const t=document.createElement("div");t.className="dm-hop-result";const d=document.createElement("span");d.className=`dm-hop-kind is-${e.kind}`,d.textContent=u(e.kind);const s=document.createElement("span");s.className="dm-hop-name",s.textContent=e.label;const a=document.createElement("small");if(a.className="dm-hop-meta",a.textContent=e.meta,t.append(d,s,a),hasDraft(e)){const o=document.createElement("span");o.className="dm-hop-dot",o.textContent="\u2022",o.title="Unsaved changes kept",t.appendChild(o)}return t}export function mountEditorHop(e,{current:t,pages:d=[],blocks:s=[]}){K(),e.innerHTML=B,e.querySelectorAll("[data-each]").forEach(n=>{n.innerHTML=D});const a=t?.id?noteRecent(t):h(),o=w(d,s),c=new Set(o.map(l)),i={recent:M.observable(F(a.filter(n=>c.has(l(n))),t,hasDraft).map(v)),related:M.observable([])},C=M.applyBindings(i,e),m=e.querySelector(".dm-hop-input"),T=E.autocomplete?E.autocomplete(m,{data:o,minChars:0,maxResults:0,debounce:60,highlightMatches:!1,clearOnSelect:!0,emptyMessage:"No page or block matches",filterFn:H,onFilter:(n,r)=>$(n,r,{recent:a,current:t}),renderItem:n=>P(n),onSelect:n=>{m.blur(),R.navigate(k(n))}}):null,f=n=>{const r=n.target.closest(".dm-hop-chip");r?.dataset.path&&R.navigate(r.dataset.path)};return e.addEventListener("click",f),I.scan(e),{setRelated(n){const r=(Array.isArray(n)?n:[]).map(v);JSON.stringify(r)!==JSON.stringify(i.related.peek())&&(i.related.value=r)},dispose(){e.removeEventListener("click",f);try{T?.destroy?.()}catch{}C.dispose()}}}export function lastPage(){return h().find(e=>e.kind==="page")?.id||""}const x=2e5;export function heldBlockEdits(e,t={}){const d=a=>t instanceof Map?t.get(a):t[a],s={};for(const a of e){let o=null;try{o=S.get(g(a))}catch{}const c=o?.form;if(!c||typeof c.content!="string"||L(o,d(a)))continue;const i=typeof c.css=="string"?c.css:"";c.content.length>x||i.length>x||(s[a]={content:c.content,css:i})}return s}
@@ -92,7 +92,11 @@
92
92
  <button type="button" class="live-preview-width" title="Mobile (390px)" aria-label="Mobile width" data-on-click="setWidth('390')"
93
93
  data-bind-class="pvWidth.value === '390' && 'active'"><span data-icon="smartphone" data-icon-size="14"></span></button>
94
94
  </div>
95
- <select class="form-select form-select-sm bex-pv-select" aria-label="How many copies" data-model="pvRepeat.value">
95
+ <select id="block-preview-on" class="form-select form-select-sm bex-pv-select bex-pv-on" aria-label="Show the block on"
96
+ title="Show the block on a page that uses it" data-model="pvOn.value" data-bind-hidden="noPages">
97
+ <option value="">A blank page</option>
98
+ </select>
99
+ <select class="form-select form-select-sm bex-pv-select" aria-label="How many copies" data-model="pvRepeat.value" data-bind-hidden="pvOn.value">
96
100
  <option value="1">One</option>
97
101
  <option value="3">Three in a grid</option>
98
102
  </select>
@@ -100,7 +104,7 @@
100
104
  <option value="">Site theme</option>
101
105
  </select>
102
106
  <small class="bex-pv-status" data-bind-text="pvStatus.value"></small>
103
- <span data-help-title="Preview" data-help="The block drawn inside your site's own page - its theme, fonts, site styles and Custom CSS - exactly as a page would show it, from what is in the editor now. Try it at tablet or mobile width, as three entries in a collection grid, or in another theme. Links in the preview do not navigate."></span>
107
+ <span data-help-title="Preview" data-help="The block drawn inside your site's own page - its theme, fonts, site styles and Custom CSS - exactly as a page would show it, from what is in the editor now. Try it at tablet or mobile width, as three entries in a collection grid, or in another theme. Show on puts it on a page that uses it - the page you came from first - drawn whole, with your changes to the block (and any unsaved text on that page) and the block outlined. Links, buttons and forms in the preview do nothing."></span>
104
108
  </div>
105
109
  <div class="bex-stage">
106
110
  <iframe id="block-preview-frame" class="page-live-preview-frame" src="about:blank" title="Block preview"
@@ -230,6 +234,7 @@
230
234
  .bex-output { flex: 1; min-height: 0; display: flex; flex-direction: column; }
231
235
  .bex-output-bar { flex: none; display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; padding: .4rem .75rem; border-bottom: 1px solid var(--dm-border); }
232
236
  .bex-pv-select { width: auto; }
237
+ .bex-pv-on { max-width: 15rem; }
233
238
  .bex-pv-status { flex: 1 1 auto; color: var(--dm-text-muted); font-size: var(--dm-text-xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
234
239
  .bex-stage { flex: 1; min-height: 0; display: flex; justify-content: center; overflow: auto; background: var(--dm-background-alt, var(--dm-surface)); }
235
240
  .bex-stage .page-live-preview-frame { border-radius: 0; }
@@ -11,6 +11,8 @@
11
11
  <span data-help-title="Pages and blocks" data-help="Find any page or block in the box above, or pick a recent one. Your unsaved changes here are kept while you go - come back and they are still in place. A dot marks anything with changes not saved yet."></span>
12
12
  <span class="pex-chip" hidden data-bind-hidden="!dirty.value" title="Changes not saved yet">Unsaved</span>
13
13
  <small class="pex-problem" hidden data-bind-hidden="!(dirty.value && problem.value)" data-bind-text="problem.value"></small>
14
+ <small class="pex-held" hidden data-bind-hidden="!heldNote.value" data-bind-text="heldNote.value"
15
+ title="Those blocks' changes are kept in the block editor - save them there"></small>
14
16
  <span class="pex-spacer"></span>
15
17
  <a class="btn btn-ghost btn-sm" target="_blank" rel="noopener" hidden data-bind-hidden="!isEdit" data-bind-href="savedPath">
16
18
  <span data-icon="external-link"></span> View page
@@ -289,6 +291,7 @@
289
291
  .pex-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px;
290
292
  background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
291
293
  .pex-problem { color: var(--dm-danger); font-size: var(--dm-text-sm); }
294
+ .pex-held { color: var(--dm-warning); font-size: var(--dm-text-sm); }
292
295
  .pex-bar [hidden], .editor-card [hidden], #editor-meta-tabs [hidden] { display: none !important; }
293
296
  .pe-hist-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
294
297
  .pe-hist-head h3 { margin: 0; font-size: 1rem; }
@@ -1,14 +1,14 @@
1
- import{api as T}from"../api.js";import{addBannerAction as J}from"../lib/plugin-chrome.js";import{helpPopovers as st}from"../lib/help-popover.js";import{openMenuAt as qe,sidebarCount as rt}from"../lib/tool-kit.js";import{createSimpleEditor as Fe}from"../lib/simple-editor.js";import{checkForm as ct,draftKey as O,draftOffer as lt,escapeHtml as He,extractPlaceholders as it,formFromBlock as ut,formatHtml as dt,payloadFromForm as pt,previewMessage as mt,previewShell as vt,rawPlaceholders as Ie,renderPreview as ft,sampleValue as Ne,scopeName as ht,shortcodeFor as bt,snapshot as pe}from"../lib/block-editor-arrange.js";import{fmtDateTime as We}from"../lib/dates.js";import{hold as yt,mountEditorHop as kt,takeHeld as wt}from"../lib/editor-hop.js";import{isHop as gt}from"../lib/editor-hop-arrange.js";const St=/^\/blocks\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,G=u=>String(u||"").split("?")[0],me="block-editor.mode",Et=["write","split","preview"],_e="block-editor.preview",Be=["full","1024","390"],Mt="/__block-preview",ve="dm-block-preview-root",Ve={title:"Block preview",layout:"blank",breadcrumbs:!1},Ct={ADD_ATTR:["target","loading"],ALLOW_DATA_ATTR:!0,FORCE_BODY:!0},xt=()=>window.DOMPurify?.sanitize?u=>window.DOMPurify.sanitize(u,Ct):null,At=`
1
+ import{api as g}from"../api.js";import{addBannerAction as ee}from"../lib/plugin-chrome.js";import{helpPopovers as kt}from"../lib/help-popover.js";import{openMenuAt as ze,sidebarCount as gt}from"../lib/tool-kit.js";import{createSimpleEditor as Je}from"../lib/simple-editor.js";import{checkForm as wt,draftKey as B,draftOffer as St,escapeHtml as Ge,extractPlaceholders as Et,formFromBlock as Mt,formatHtml as Ct,draftOlderThan as xt,pageFrontmatter as At,pageInFrame as Tt,payloadFromForm as Lt,previewMessage as $t,previewPageOptions as Ot,previewShell as Rt,rawPlaceholders as Qe,renderPreview as Dt,sampleValue as Xe,scopeName as jt,shortcodeFor as Ft,snapshot as Ee}from"../lib/block-editor-arrange.js";import{draftKey as Pt,readDraft as qt}from"../lib/page-editor-arrange.js";import{fmtDateTime as Ze}from"../lib/dates.js";import{hold as It,lastPage as Bt,mountEditorHop as Ht,takeHeld as Nt}from"../lib/editor-hop.js";import{isHop as Wt}from"../lib/editor-hop-arrange.js";const _t=/^\/blocks\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,te=u=>String(u||"").split("?")[0],Me="block-editor.mode",Kt=["write","split","preview"],et="block-editor.preview",tt=["full","1024","390"],Vt="/__block-preview",Ce="dm-block-preview-root",ot={title:"Block preview",layout:"blank",breadcrumbs:!1},Yt={ADD_ATTR:["target","loading"],ALLOW_DATA_ATTR:!0,FORCE_BODY:!0},Ut=()=>window.DOMPurify?.sanitize?u=>window.DOMPurify.sanitize(u,Yt):null,zt=`
2
2
  <div class="bex-sample" data-key="{{key}}" tabindex="0">
3
3
  <label class="bex-sample-label" for="bex-sample-{{key}}">{{label}}</label>
4
4
  <input id="bex-sample-{{key}}" type="text" class="form-input form-input--sm" data-bind-value="value" data-sample="{{key}}" placeholder="Sample {{key}}">
5
5
  <button type="button" class="bex-kebab" aria-label="Actions for {{label}}" title="Actions">\u22EE</button>
6
- </div>`;let f=null,Ye=!1;function Lt(){Ye||!window.R?.use||(Ye=!0,R.use((u,p,s)=>{if(!(St.test(p?.path||"")&&G(u?.path)!==G(p?.path))||!f?.dirty())return s();const r=f;return gt(p?.path,u?.path)?(yt(r.item),s()):E.confirm(`You have unsaved changes to ${He(r.label())}. Leave anyway? They are kept as a draft for your next visit.`).then(_=>{_?s():history.replaceState(null,"",r.hash)})}),M.subscribe("router:afterChange",({to:u}={})=>{if(f&&G(u?.path)!==f.path){const p=f;f=null,p.dispose()}}))}function Tt(u,p){const s=u.querySelector(".dm-plugin-banner-name"),C=s?.lastChild;C&&C.nodeType===3&&s.querySelector(".dm-plugin-banner-sep")&&(C.textContent=p)}function Q(u){return navigator.clipboard?.writeText(u).then(()=>!0,()=>!1)??Promise.resolve(!1)}export const blockEditorView={templateUrl:"/admin/js/templates/block-editor.html",async onMount(u){if(Lt(),f){const e=f;f=null,e.dispose()}const p=u.get(0),s=p.querySelector(".bex")||p,C=[],r=e=>(C.push(e),e);let _=!1;const x=()=>!_&&s.isConnected,fe=location.hash.match(/^#\/blocks\/edit\/([^/?#]+)/),l=fe?decodeURIComponent(fe[1]):null,d=!l,Ke=location.hash,Ue=G(location.hash.slice(1)),ze=d&&new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",he=E.loader(p,{type:"dots"});let y=null,B=[],X=[],be=[];try{[y,B,X,be]=await Promise.all([l?T.blocks.get(l):Promise.resolve(null),T.blocks.list().catch(()=>[]),T.projects.list().catch(()=>[]),T.pages.list().catch(()=>[])])}catch(e){he.destroy(),E.toast(e.message||"Block not found.",{type:"error"}),R.navigate("/blocks");return}if(he.destroy(),!s.isConnected)return;const Je=(Array.isArray(B)?B:[]).map(e=>e.name).sort((e,t)=>e.localeCompare(t));let ye=y?.meta||{};const v=ut(y?{...y,name:l}:null,{project:ze}),a={};for(const[e,t]of Object.entries(v))a[e]=M.observable(t);const Z=()=>Object.fromEntries(Object.keys(a).map(e=>[e,a[e].peek()])),ke=()=>Object.fromEntries(Object.keys(a).map(e=>[e,a[e].value])),D=M.observable(pe(v)),P=M.computed(()=>pe(ke())!==D.value),q=M.observable(!1),V=M.observable(!1),Y=M.computed(()=>ct(ke(),{isNew:d,taken:Je}));let we="split";try{we=Et.includes(S.get(me))?S.get(me):"split"}catch{}const k=M.observable(we);r(M.effect(()=>{const e=k.value;try{S.set(me,e)}catch{}}));const ge=M.observable("Ln 1, Col 1");let $={};try{$=S.get(_e)||{}}catch{}const ee=M.observable(Be.includes($.width)?$.width:"full"),K=M.observable(String($.repeat)==="3"?"3":"1"),m=M.observable(typeof $.theme=="string"?$.theme:""),A=M.observable("");r(M.effect(()=>{const e={width:ee.value,repeat:K.value,theme:m.value};try{S.set(_e,e)}catch{}}));const h={},F={},U=M.observable(0),te=M.observable([]),Se=()=>Ie(a.content.peek());r(M.effect(()=>{const e=it(a.content.value),t=Ie(a.content.value);let n=!1;for(const o of e){const g=t.has(o);if(!(o in h)||o in F&&F[o]!==g){const W=o in h;if(h[o]=Ne(o,new Date,{raw:g}),F[o]=g,W){n=!0;const i=s.querySelector(`.bex-samples [data-sample="${CSS.escape(o)}"]`);i&&(i.value=h[o])}}}te.value=e.map(o=>({key:o,label:t.has(o)?`{{{${o}}}}`:`{{${o}}}`,value:h[o]})),n&&queueMicrotask(()=>U.value++)}));const Ge={...a,dirty:P,saving:q,mode:k,cursor:ge,sampleRows:te,isNew:d,notNew:!d,pvWidth:ee,pvRepeat:K,pvTheme:m,pvStatus:A,titleText:d?"New block":l,metaText:M.computed(()=>{if(d)return"Not saved yet";const e=[];return(y?.updatedAt||y?.meta?.updatedAt)&&e.push(`changed ${We(y.updatedAt||y.meta.updatedAt)}`),e.push(`${a.content.value.split(`
7
- `).length} lines`),a.css.value.trim()&&e.push(`${Math.ceil(a.css.value.length/1024)} KB CSS`),e.join(" \xB7 ")}),nameErr:M.computed(()=>(V.value||a.name.value)&&Y.value.errors.name||""),cssErr:M.computed(()=>Y.value.errors.css||""),scopeHint:M.computed(()=>`[data-block="${ht(a.name.value)}"]`),noSamples:M.computed(()=>te.value.length===0),noSamplesText:"No {{placeholders}} in the template yet.",modeCls:M.computed(()=>`bex-body is-${k.value}`),cannotSave:M.computed(()=>q.value||V.value&&Object.keys(Y.value.errors).length>0)},oe=s.querySelector("#block-project");for(const e of Array.isArray(X)?X:[]){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,oe.appendChild(t)}if(v.project&&![...oe.options].some(e=>e.value===v.project)){const e=document.createElement("option");e.value=v.project,e.textContent=`${v.project} (no longer a project)`,oe.appendChild(e)}const ae={kind:"block",id:l||"",label:l||""},b=s.querySelector(".bex-samples");b.innerHTML=At;let L=null,H=null,c=null;function ne(e){for(const[t,n]of Object.entries(e))a[t]&&(a[t].value=n);L.setValue(String(e.content??"")),H.setValue(String(e.css??""))}const se=s.querySelector("#block-preview-frame");let j="none",N=0;const Qe="As your site shows it \xB7 not saved",Ee=()=>{if(!x()||k.peek()==="write"||j!=="ready")return;const e={content:a.content.peek(),css:a.css.peek(),name:a.name.peek()},t=xt();if(!t){A.value="The preview needs the HTML cleaner, which did not load - reload the admin.";return}const n=ft(e,h,{repeat:Number(K.peek()),sanitize:t});try{se.contentWindow?.postMessage({source:"dm-block-editor",type:"paint",tick:N,html:n},"*")}catch{}},Me=e=>{const t=mt(e,se.contentWindow,N);if(!(!t||!x())){if(t.type==="save"){z();return}if(j=t.host?"ready":"failed",j==="failed"){A.value="Your site's page came back without a place for the block - the preview cannot show it.";return}A.peek()==="Loading your site\u2026"&&(A.value=Qe),Ee()}};window.addEventListener("message",Me),r(()=>window.removeEventListener("message",Me));async function Ce(){const e=++N;j="loading",A.value="Loading your site\u2026";let t;try{const n=m.peek()?{...Ve,theme:m.peek()}:Ve;({html:t}=await T.pages.previewFull(Mt,n,`<div id="${ve}"></div>`))}catch(n){t=`<!doctype html><html><head><meta charset="utf-8"></head><body style="font:14px system-ui;margin:0"><div id="${ve}" style="padding:1rem"></div></body></html>`,A.value=`Without your site's styles - ${n?.message||"the page did not render"}`}!x()||e!==N||(se.srcdoc=vt(t,e,ve))}let re=0;r(M.effect(()=>{if(a.content.value,a.css.value,a.name.value,U.value,K.value,k.value!=="write"){if(j==="none"){Ce();return}cancelAnimationFrame(re),re=requestAnimationFrame(Ee)}}));let xe=m.peek();r(M.effect(()=>{const e=m.value;e!==xe&&(xe=e,j!=="none"&&Ce())})),r(()=>{cancelAnimationFrame(re),N++}),T.theme.catalog().then(({themes:e=[],custom:t=[]}={})=>{if(!x())return;const n=s.querySelector("#block-preview-theme"),o=i=>String(i).replace(/-/g," ").replace(/\b\w/g,ue=>ue.toUpperCase()),g=[...e.filter(i=>!String(i).startsWith("admin-")).map(i=>({id:i,label:o(i)})),...(Array.isArray(t)?t:[]).filter(i=>i?.id).map(i=>({id:i.id,label:i.label||o(i.id)}))],W=g.map(i=>i.id);for(const{id:i,label:ue}of g){const de=document.createElement("option");de.value=i,de.textContent=ue,n.appendChild(de)}m.peek()&&!W.includes(m.peek())?m.value="":n.value=m.peek()}).catch(()=>{s.querySelector("#block-preview-theme")?.remove(),x()&&m.peek()&&(m.value="")});const Ae=e=>{const t=e.target?.dataset?.sample;t&&(h[t]=e.target.value,delete F[t],U.value++)};b.addEventListener("input",Ae),r(()=>b.removeEventListener("input",Ae));function ce(e){c.focus(),c.setRangeText(e,c.selectionStart,c.selectionEnd,"end"),c.dispatchEvent(new Event("input",{bubbles:!0}))}async function Le(e){switch(e){case"undo":c.focus(),document.execCommand("undo");break;case"redo":c.focus(),document.execCommand("redo");break;case"cut":c.selectionStart!==c.selectionEnd&&(await Q(c.value.slice(c.selectionStart,c.selectionEnd)),ce(""));break;case"copy":c.selectionStart!==c.selectionEnd&&await Q(c.value.slice(c.selectionStart,c.selectionEnd));break;case"paste":try{ce(await navigator.clipboard.readText())}catch{E.toast("Use Ctrl+V to paste.",{type:"info"})}break;case"select-all":c.focus(),c.select();break;case"format":{const t=dt(c.value);L.setValue(t),a.content.value=t;break}default:break}}async function z(){if(q.peek())return;V.value=!0;const{errors:e,name:t}=Y.peek();if(Object.keys(e).length){E.toast(e.name||e.css,{type:"warning"});return}const n=d?t:l;q.value=!0;try{const o=pt(Z(),ye);if(await T.blocks.put(n,o),!x())return;ye=o.meta,D.value=pe(Z());try{S.remove?S.remove(O(l)):S.set(O(l),null)}catch{}rt("#/blocks"),E.toast(d?`Created "${n}".`:"Block saved.",{type:"success"}),d&&(location.hash=`#/blocks/edit/${encodeURIComponent(n)}`)}catch(o){E.toast(`Not saved: ${o.message||o}`,{type:"error"})}finally{q.value=!1}}async function Xe(){P.peek()&&await E.confirm("Discard the changes you have not saved?")&&(V.value=!1,ne(Ze(D.peek())))}function Ze(e){try{const[t,n,o,g,W]=JSON.parse(e);return{name:t,project:n,bundled:o,content:g,css:W}}catch{return v}}const et=M.applyBindings(Ge,s,{methods:{save:()=>z(),discard:()=>Xe(),setWrite:()=>{k.value="write"},setSplit:()=>{k.value="split"},setPreview:()=>{k.value="preview"},setWidth:e=>{Be.includes(String(e))&&(ee.value=String(e))},act:e=>Le(e)}});r(()=>et.dispose());const tt=kt(s.querySelector("#block-hop"),{current:ae,pages:be,blocks:B});r(()=>tt.dispose()),r(st(s,{position:"bottom"})),L=Fe(s.querySelector(".bex-write"),{initialValue:v.content,language:"html",autoIndent:!0,onChange:e=>{a.content.value=e},onCursor:({line:e,col:t})=>{ge.value=`Ln ${e}, Col ${t}`}}),H=Fe(s.querySelector(".bex-css"),{initialValue:v.css,language:"css",autoIndent:!0,onChange:e=>{a.css.value=e}}),L.textarea.setAttribute("aria-label","Template HTML"),L.textarea.placeholder=`<div class="card mb-3">
6
+ </div>`;let w=null,at=!1;function Jt(){at||!window.R?.use||(at=!0,R.use((u,m,l)=>{if(!(_t.test(m?.path||"")&&te(u?.path)!==te(m?.path))||!w?.dirty())return l();const s=w;return Wt(m?.path,u?.path)?(It(s.item),l()):E.confirm(`You have unsaved changes to ${Ge(s.label())}. Leave anyway? They are kept as a draft for your next visit.`).then(Y=>{Y?l():history.replaceState(null,"",s.hash)})}),M.subscribe("router:afterChange",({to:u}={})=>{if(w&&te(u?.path)!==w.path){const m=w;w=null,m.dispose()}}))}function Gt(u,m){const l=u.querySelector(".dm-plugin-banner-name"),O=l?.lastChild;O&&O.nodeType===3&&l.querySelector(".dm-plugin-banner-sep")&&(O.textContent=m)}function oe(u){return navigator.clipboard?.writeText(u).then(()=>!0,()=>!1)??Promise.resolve(!1)}export const blockEditorView={templateUrl:"/admin/js/templates/block-editor.html",async onMount(u){if(Jt(),w){const e=w;w=null,e.dispose()}const m=u.get(0),l=m.querySelector(".bex")||m,O=[],s=e=>(O.push(e),e);let Y=!1;const f=()=>!Y&&l.isConnected,xe=location.hash.match(/^#\/blocks\/edit\/([^/?#]+)/),r=xe?decodeURIComponent(xe[1]):null,d=!r,nt=location.hash,st=te(location.hash.slice(1)),rt=d&&new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",Ae=E.loader(m,{type:"dots"});let T=null,P=[],ae=[],ne=[],Te=null;const U=Bt();try{[T,P,ae,ne]=await Promise.all([r?g.blocks.get(r):Promise.resolve(null),g.get("/blocks?usage=1").catch(()=>g.blocks.list()).catch(()=>[]),g.projects.list().catch(()=>[]),g.pages.list().catch(()=>[])]),Te=(Array.isArray(P)?P:[]).find(e=>e.name===r)?.usage||null}catch(e){Ae.destroy(),E.toast(e.message||"Block not found.",{type:"error"}),R.navigate("/blocks");return}if(Ae.destroy(),!l.isConnected)return;const lt=(Array.isArray(P)?P:[]).map(e=>e.name).sort((e,t)=>e.localeCompare(t));let Le=T?.meta||{};const v=Mt(T?{...T,name:r}:null,{project:rt}),n={};for(const[e,t]of Object.entries(v))n[e]=M.observable(t);const se=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].peek()])),$e=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),H=M.observable(Ee(v)),N=M.computed(()=>Ee($e())!==H.value),W=M.observable(!1),z=M.observable(!1),J=M.computed(()=>wt($e(),{isNew:d,taken:lt}));let Oe="split";try{Oe=Kt.includes(S.get(Me))?S.get(Me):"split"}catch{}const h=M.observable(Oe);s(M.effect(()=>{const e=h.value;try{S.set(Me,e)}catch{}}));const Re=M.observable("Ln 1, Col 1");let q={};try{q=S.get(et)||{}}catch{}const re=M.observable(tt.includes(q.width)?q.width:"full"),G=M.observable(String(q.repeat)==="3"?"3":"1"),p=M.observable(typeof q.theme=="string"?q.theme:""),b=M.observable(""),le=Ot(Te,ne,U),y=M.observable(le[0]?.value===U&&U?U:"");s(M.effect(()=>{const e={width:re.value,repeat:G.value,theme:p.value};try{S.set(et,e)}catch{}}));const C={},_={},Q=M.observable(0),ce=M.observable([]),De=()=>Qe(n.content.peek());s(M.effect(()=>{const e=Et(n.content.value),t=Qe(n.content.value);let a=!1;for(const o of e){const k=t.has(o);if(!(o in C)||o in _&&_[o]!==k){const F=o in C;if(C[o]=Xe(o,new Date,{raw:k}),_[o]=k,F){a=!0;const c=l.querySelector(`.bex-samples [data-sample="${CSS.escape(o)}"]`);c&&(c.value=C[o])}}}ce.value=e.map(o=>({key:o,label:t.has(o)?`{{{${o}}}}`:`{{${o}}}`,value:C[o]})),a&&queueMicrotask(()=>Q.value++)}));const ct={...n,dirty:N,saving:W,mode:h,cursor:Re,sampleRows:ce,isNew:d,notNew:!d,pvWidth:re,pvRepeat:G,pvTheme:p,pvStatus:b,pvOn:y,noPages:le.length===0,titleText:d?"New block":r,metaText:M.computed(()=>{if(d)return"Not saved yet";const e=[];return(T?.updatedAt||T?.meta?.updatedAt)&&e.push(`changed ${Ze(T.updatedAt||T.meta.updatedAt)}`),e.push(`${n.content.value.split(`
7
+ `).length} lines`),n.css.value.trim()&&e.push(`${Math.ceil(n.css.value.length/1024)} KB CSS`),e.join(" \xB7 ")}),nameErr:M.computed(()=>(z.value||n.name.value)&&J.value.errors.name||""),cssErr:M.computed(()=>J.value.errors.css||""),scopeHint:M.computed(()=>`[data-block="${jt(n.name.value)}"]`),noSamples:M.computed(()=>ce.value.length===0),noSamplesText:"No {{placeholders}} in the template yet.",modeCls:M.computed(()=>`bex-body is-${h.value}`),cannotSave:M.computed(()=>W.value||z.value&&Object.keys(J.value.errors).length>0)},ie=l.querySelector("#block-project");for(const e of Array.isArray(ae)?ae:[]){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,ie.appendChild(t)}if(v.project&&![...ie.options].some(e=>e.value===v.project)){const e=document.createElement("option");e.value=v.project,e.textContent=`${v.project} (no longer a project)`,ie.appendChild(e)}const ue={kind:"block",id:r||"",label:r||""},it=l.querySelector("#block-preview-on");for(const e of le)it.appendChild(new Option(e.label,e.value));const x=l.querySelector(".bex-samples");x.innerHTML=zt;let D=null,K=null,i=null;function de(e){for(const[t,a]of Object.entries(e))n[t]&&(n[t].value=a);D.setValue(String(e.content??"")),K.setValue(String(e.css??""))}const A=l.querySelector("#block-preview-frame");let j="none",L=0;const ut="As your site shows it \xB7 not saved",je=()=>{if(!f()||h.peek()==="write"||j!=="ready"||y.peek())return;const e={content:n.content.peek(),css:n.css.peek(),name:n.name.peek()},t=Ut();if(!t){b.value="The preview needs the HTML cleaner, which did not load - reload the admin.";return}const a=Dt(e,C,{repeat:Number(G.peek()),sanitize:t});try{A.contentWindow?.postMessage({source:"dm-block-editor",type:"paint",tick:L,html:a},"*")}catch{}},Fe=e=>{const t=$t(e,A.contentWindow,L);if(!(!t||!f())){if(t.type==="save"){Z();return}if(!y.peek()){if(j=t.host?"ready":"failed",j==="failed"){b.value="Your site's page came back without a place for the block - the preview cannot show it.";return}b.peek()==="Loading your site\u2026"&&(b.value=ut),je()}}};window.addEventListener("message",Fe),s(()=>window.removeEventListener("message",Fe));async function pe(){const e=++L;j="loading",b.value="Loading your site\u2026";let t;try{const a=p.peek()?{...ot,theme:p.peek()}:ot;({html:t}=await g.pages.previewFull(Vt,a,`<div id="${Ce}"></div>`))}catch(a){t=`<!doctype html><html><head><meta charset="utf-8"></head><body style="font:14px system-ui;margin:0"><div id="${Ce}" style="padding:1rem"></div></body></html>`,f()&&e===L&&(b.value=`Without your site's styles - ${a?.message||"the page did not render"}`)}!f()||e!==L||(A.srcdoc=Rt(t,e,Ce))}let V=null,me=null,fe=0,ve=!0,X=null;const Pe=()=>{const e=X;if(X=null,!(!e||!y.peek()))try{const t=e.toBlock?A.contentDocument?.querySelector(`[data-block="${CSS.escape(r)}"]`):null;t?t.scrollIntoView({block:"center"}):A.contentWindow.scrollTo(0,e.y)}catch{}};A.addEventListener("load",Pe),s(()=>A.removeEventListener("load",Pe));async function dt(e){if(V?.path===e)return V;const t=await g.pages.get(e);let a=null;try{a=qt(S.get(Pt(e)))}catch{}a&&xt(a,t?.updatedAt)&&(a=null);const o=typeof a?.state?.body=="string"?a.state.body:String(t?.content??"");return V={path:e,frontmatter:At(t),body:o,withDraft:typeof a?.state?.body=="string"},V}const he=(e=!1)=>{clearTimeout(me),me=setTimeout(async()=>{const t=y.peek();if(!t||!f()||h.peek()==="write")return;const a=++fe;try{const o=await dt(t),k=p.peek()?{...o.frontmatter,theme:p.peek()}:o.frontmatter,{html:F}=await g.pages.previewFull(t,k,o.body,{[r]:{content:n.content.peek(),css:n.css.peek()}});if(!f()||a!==fe||y.peek()!==t)return;let c=0;try{c=A.contentWindow.scrollY||0}catch{}X={toBlock:ve||!!X?.toBlock,y:c},ve=!1,A.srcdoc=Tt(F,r,L),b.value=`On ${t} \xB7 not saved${o.withDraft?" \xB7 with the page's unsaved text":""}`}catch(o){f()&&a===fe&&(b.value=`Could not show ${t} - ${o?.message||"the page did not render"}`)}},e?0:450)};s(()=>clearTimeout(me));let qe=y.peek();s(M.effect(()=>{const e=y.value;e!==qe&&(qe=e,ve=!0,V=null,L++,j="none",e?(b.value=`Loading ${e}\u2026`,he(!0)):h.peek()!=="write"&&pe())}));let be=0;s(M.effect(()=>{if(n.content.value,n.css.value,n.name.value,Q.value,G.value,h.value!=="write"){if(y.peek()){he();return}if(j==="none"){pe();return}cancelAnimationFrame(be),be=requestAnimationFrame(je)}}));let Ie=p.peek();s(M.effect(()=>{const e=p.value;e!==Ie&&(Ie=e,y.peek()?he(!0):j!=="none"&&pe())})),s(()=>{cancelAnimationFrame(be),L++}),g.theme.catalog().then(({themes:e=[],custom:t=[]}={})=>{if(!f())return;const a=l.querySelector("#block-preview-theme"),o=c=>String(c).replace(/-/g," ").replace(/\b\w/g,we=>we.toUpperCase()),k=[...e.filter(c=>!String(c).startsWith("admin-")).map(c=>({id:c,label:o(c)})),...(Array.isArray(t)?t:[]).filter(c=>c?.id).map(c=>({id:c.id,label:c.label||o(c.id)}))],F=k.map(c=>c.id);for(const{id:c,label:we}of k){const Se=document.createElement("option");Se.value=c,Se.textContent=we,a.appendChild(Se)}p.peek()&&!F.includes(p.peek())?p.value="":a.value=p.peek()}).catch(()=>{l.querySelector("#block-preview-theme")?.remove(),f()&&p.peek()&&(p.value="")});const Be=e=>{const t=e.target?.dataset?.sample;t&&(C[t]=e.target.value,delete _[t],Q.value++)};x.addEventListener("input",Be),s(()=>x.removeEventListener("input",Be));function ye(e){i.focus(),i.setRangeText(e,i.selectionStart,i.selectionEnd,"end"),i.dispatchEvent(new Event("input",{bubbles:!0}))}async function He(e){switch(e){case"undo":i.focus(),document.execCommand("undo");break;case"redo":i.focus(),document.execCommand("redo");break;case"cut":i.selectionStart!==i.selectionEnd&&(await oe(i.value.slice(i.selectionStart,i.selectionEnd)),ye(""));break;case"copy":i.selectionStart!==i.selectionEnd&&await oe(i.value.slice(i.selectionStart,i.selectionEnd));break;case"paste":try{ye(await navigator.clipboard.readText())}catch{E.toast("Use Ctrl+V to paste.",{type:"info"})}break;case"select-all":i.focus(),i.select();break;case"format":{const t=Ct(i.value);D.setValue(t),n.content.value=t;break}default:break}}async function Z(){if(W.peek())return;z.value=!0;const{errors:e,name:t}=J.peek();if(Object.keys(e).length){E.toast(e.name||e.css,{type:"warning"});return}const a=d?t:r;W.value=!0;try{const o=Lt(se(),Le);if(await g.blocks.put(a,o),!f())return;Le=o.meta,H.value=Ee(se());try{S.remove?S.remove(B(r)):S.set(B(r),null)}catch{}gt("#/blocks"),E.toast(d?`Created "${a}".`:"Block saved.",{type:"success"}),d&&(location.hash=`#/blocks/edit/${encodeURIComponent(a)}`)}catch(o){E.toast(`Not saved: ${o.message||o}`,{type:"error"})}finally{W.value=!1}}async function pt(){N.peek()&&await E.confirm("Discard the changes you have not saved?")&&(z.value=!1,de(mt(H.peek())))}function mt(e){try{const[t,a,o,k,F]=JSON.parse(e);return{name:t,project:a,bundled:o,content:k,css:F}}catch{return v}}const ft=M.applyBindings(ct,l,{methods:{save:()=>Z(),discard:()=>pt(),setWrite:()=>{h.value="write"},setSplit:()=>{h.value="split"},setPreview:()=>{h.value="preview"},setWidth:e=>{tt.includes(String(e))&&(re.value=String(e))},act:e=>He(e)}});s(()=>ft.dispose());const vt=Ht(l.querySelector("#block-hop"),{current:ue,pages:ne,blocks:P});s(()=>vt.dispose()),s(kt(l,{position:"bottom"})),D=Je(l.querySelector(".bex-write"),{initialValue:v.content,language:"html",autoIndent:!0,onChange:e=>{n.content.value=e},onCursor:({line:e,col:t})=>{Re.value=`Ln ${e}, Col ${t}`}}),K=Je(l.querySelector(".bex-css"),{initialValue:v.css,language:"css",autoIndent:!0,onChange:e=>{n.css.value=e}}),D.textarea.setAttribute("aria-label","Template HTML"),D.textarea.placeholder=`<div class="card mb-3">
8
8
  <div class="card-body">
9
9
  <h3>{{title}}</h3>
10
10
  <p>{{message}}</p>
11
11
  </div>
12
- </div>`,H.textarea.setAttribute("aria-label","Custom CSS"),H.textarea.placeholder=`/* Scoped to this block - e.g.
12
+ </div>`,K.textarea.setAttribute("aria-label","Custom CSS"),K.textarea.placeholder=`/* Scoped to this block - e.g.
13
13
  color: var(--dm-primary);
14
- h3 { font-size: 1.25rem; } */`,r(()=>{L.destroy(),H.destroy()}),c=L.textarea;const Te=s.querySelector(".bex-toolbar"),Re=e=>{const t=e.target.closest("[data-action]");t&&Le(t.dataset.action)};Te.addEventListener("click",Re),r(()=>Te.removeEventListener("click",Re));const le=e=>Se().has(e)?`{{{${e}}}}`:`{{${e}}}`,ot=e=>[{header:le(e)},{label:"Insert into the template",icon:"corner-down-right",action:()=>ce(le(e))},{label:"Copy placeholder",icon:"copy",action:async()=>{const t=le(e);E.toast(await Q(t)?"Copied.":t,{type:"info"})}},{label:"Reset sample value",icon:"rotate-ccw",action:()=>{const t=Se().has(e);h[e]=Ne(e,new Date,{raw:t}),F[e]=t;const n=b.querySelector(`[data-sample="${CSS.escape(e)}"]`);n&&(n.value=h[e]),U.value++}}],at=E.contextMenu(b,{cursor:"glyph",match:".bex-sample",inherit:!1,density:"compact",items:e=>ot(e.dataset.key)});r(()=>at?.destroy?.());const $e=e=>{const t=e.target.closest(".bex-kebab");t&&qe(t)},je=e=>{const t=e.target.closest(".bex-sample");t&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),qe(t))};b.addEventListener("click",$e),b.addEventListener("keydown",je),r(()=>{b.removeEventListener("click",$e),b.removeEventListener("keydown",je)}),Tt(p,d?"New block":l),J(u,{id:"block-editor-list",icon:"arrow-left",title:"All blocks",onClick:()=>{location.hash="#/blocks"}}),d?p.querySelector('[data-banner-action="block-editor-shortcode"]')?.remove():J(u,{id:"block-editor-shortcode",icon:"copy",title:"Copy shortcode",onClick:async()=>{const e=bt(l);E.toast(await Q(e)?"Shortcode copied - set the collection slug.":e,{type:"info",duration:5e3})}}),J(u,{id:"block-editor-guide",icon:"book",title:"Blocks guide",onClick:()=>{location.hash="#/docs/usage/blocks"}}),J(u,{id:"block-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>z()});const Oe=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),z())};document.addEventListener("keydown",Oe,!0),r(()=>document.removeEventListener("keydown",Oe,!0));const De=()=>{if(P.peek())try{S.set(O(l),{form:Z(),base:D.peek(),at:new Date().toISOString()})}catch{}},Pe=e=>{P.peek()&&(De(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Pe),r(()=>window.removeEventListener("beforeunload",Pe)),f={hash:Ke,path:Ue,item:ae,dirty:()=>P.peek(),label:()=>d?"the new block":`"${l}"`,dispose(){_=!0,De(),C.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(s);let ie=null;try{ie=S.get(O(l))}catch{}const w=lt(ie,D.peek()),nt=wt(ae);if(ie)try{S.remove?S.remove(O(l)):S.set(O(l),null)}catch{}if(w&&nt&&!w.stale){const e={...v,...w.form};d||(e.name=l),ne(e),E.toast("Your unsaved changes to this block are back.",{type:"info"})}else if(w){const e=w.at?` (${We(w.at)})`:"",t=d?"a new block":`"${He(l)}"`,n=w.stale?`You left unsaved changes to ${t}${e}, but the block has been saved since. Restore your version anyway?`:`You left unsaved changes to ${t}${e}. Restore them?`;if(await E.confirm(n)&&x()){const o={...v,...w.form};d||(o.name=l),ne(o)}}}};
14
+ h3 { font-size: 1.25rem; } */`,s(()=>{D.destroy(),K.destroy()}),i=D.textarea;const Ne=l.querySelector(".bex-toolbar"),We=e=>{const t=e.target.closest("[data-action]");t&&He(t.dataset.action)};Ne.addEventListener("click",We),s(()=>Ne.removeEventListener("click",We));const ke=e=>De().has(e)?`{{{${e}}}}`:`{{${e}}}`,ht=e=>[{header:ke(e)},{label:"Insert into the template",icon:"corner-down-right",action:()=>ye(ke(e))},{label:"Copy placeholder",icon:"copy",action:async()=>{const t=ke(e);E.toast(await oe(t)?"Copied.":t,{type:"info"})}},{label:"Reset sample value",icon:"rotate-ccw",action:()=>{const t=De().has(e);C[e]=Xe(e,new Date,{raw:t}),_[e]=t;const a=x.querySelector(`[data-sample="${CSS.escape(e)}"]`);a&&(a.value=C[e]),Q.value++}}],bt=E.contextMenu(x,{cursor:"glyph",match:".bex-sample",inherit:!1,density:"compact",items:e=>ht(e.dataset.key)});s(()=>bt?.destroy?.());const _e=e=>{const t=e.target.closest(".bex-kebab");t&&ze(t)},Ke=e=>{const t=e.target.closest(".bex-sample");t&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),ze(t))};x.addEventListener("click",_e),x.addEventListener("keydown",Ke),s(()=>{x.removeEventListener("click",_e),x.removeEventListener("keydown",Ke)}),Gt(m,d?"New block":r),ee(u,{id:"block-editor-list",icon:"arrow-left",title:"All blocks",onClick:()=>{location.hash="#/blocks"}}),d?m.querySelector('[data-banner-action="block-editor-shortcode"]')?.remove():ee(u,{id:"block-editor-shortcode",icon:"copy",title:"Copy shortcode",onClick:async()=>{const e=Ft(r);E.toast(await oe(e)?"Shortcode copied - set the collection slug.":e,{type:"info",duration:5e3})}}),ee(u,{id:"block-editor-guide",icon:"book",title:"Blocks guide",onClick:()=>{location.hash="#/docs/usage/blocks"}}),ee(u,{id:"block-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>Z()});const Ve=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),Z())};document.addEventListener("keydown",Ve,!0),s(()=>document.removeEventListener("keydown",Ve,!0));const Ye=()=>{if(N.peek())try{S.set(B(r),{form:se(),base:H.peek(),at:new Date().toISOString()})}catch{}},Ue=e=>{N.peek()&&(Ye(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Ue),s(()=>window.removeEventListener("beforeunload",Ue)),w={hash:nt,path:st,item:ue,dirty:()=>N.peek(),label:()=>d?"the new block":`"${r}"`,dispose(){Y=!0,Ye(),O.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(l);let ge=null;try{ge=S.get(B(r))}catch{}const $=St(ge,H.peek()),yt=Nt(ue);if(ge)try{S.remove?S.remove(B(r)):S.set(B(r),null)}catch{}if($&&yt&&!$.stale){const e={...v,...$.form};d||(e.name=r),de(e),E.toast("Your unsaved changes to this block are back.",{type:"info"})}else if($){const e=$.at?` (${Ze($.at)})`:"",t=d?"a new block":`"${Ge(r)}"`,a=$.stale?`You left unsaved changes to ${t}${e}, but the block has been saved since. Restore your version anyway?`:`You left unsaved changes to ${t}${e}. Restore them?`;if(await E.confirm(a)&&f()){const o={...v,...$.form};d||(o.name=r),de(o)}}}};
@@ -1,4 +1,4 @@
1
- import{api as v}from"../api.js";import{CardBuilder as Mt}from"../lib/card-builder.js";import{TimelineBuilder as St}from"../lib/timeline-builder.js";import{attachEditorKeybindings as Tt,createToolbar as Ct,insertAtCursor as xt}from"../lib/markdown-toolbar.js";import{EffectsBuilder as Pt}from"../lib/effects-builder.js";import{attachContextMenu as Lt}from"../lib/shortcode-context-menu.js";import{registerCustomInserters as At}from"../lib/shortcode-insert.js";import{populateThemeSelect as Dt}from"../lib/themes.js";import{createFoldingManager as Ot}from"../lib/editor-folding.js";import{openScribeComposer as Rt}from"../lib/scribe-composer.js";import{openCrudTutorial as Nt}from"../lib/crud-tutorial.js";import{addBannerAction as V}from"../lib/plugin-chrome.js";import{helpPopovers as _e}from"../lib/help-popover.js";import{onceClosed as we,openMenuAt as Je}from"../lib/tool-kit.js";import{pickMedia as It}from"../lib/media-picker.js";import{openPageClone as Ft}from"../lib/page-clone.js";import{openEditorReference as We}from"../lib/page-editor-reference.js";import{cleanType as Z,DEFAULT_TYPE as Q,EDITOR_FONTS as Bt,fontHref as zt,isDefaultType as $t,LINE_HEIGHTS as jt,SIZES as Vt,typeVars as Ye}from"../lib/editor-type.js";import{getProjectFromHash as qt}from"../lib/project-context.js";import{attachSmoothCaret as Ge,getCaretPreference as Ht,setCaretPreference as Kt}from"../lib/smooth-caret.js";import{SCREEN as Ut,bannerSection as _t,draftIsStale as Xe,draftKey as Jt,initialProject as Wt,makeDraft as Yt,problems as Ee,readDraft as Gt,snapshot as $}from"../lib/page-editor-arrange.js";import{fmtDateTime as ee}from"../lib/dates.js";import{hold as Ze,mountEditorHop as Xt,noteRecent as Zt,takeHeld as Qt}from"../lib/editor-hop.js";import{isHop as ea}from"../lib/editor-hop-arrange.js";import{blockTagDefs as ta,blocksInBody as aa}from"../lib/block-tags.js";function te(o){const l=document.createElement("template");return l.innerHTML=o.trim(),l.content.firstElementChild}const sa=ee,Qe=o=>ee(o.createdAt)+(o.label?` - ${o.label}`:""),oa=`
1
+ import{api as v}from"../api.js";import{CardBuilder as xt}from"../lib/card-builder.js";import{TimelineBuilder as Pt}from"../lib/timeline-builder.js";import{attachEditorKeybindings as At,createToolbar as Lt,insertAtCursor as Dt}from"../lib/markdown-toolbar.js";import{EffectsBuilder as Ot}from"../lib/effects-builder.js";import{attachContextMenu as Rt}from"../lib/shortcode-context-menu.js";import{registerCustomInserters as Nt}from"../lib/shortcode-insert.js";import{populateThemeSelect as Bt}from"../lib/themes.js";import{createFoldingManager as It}from"../lib/editor-folding.js";import{openScribeComposer as Ft}from"../lib/scribe-composer.js";import{openCrudTutorial as zt}from"../lib/crud-tutorial.js";import{addBannerAction as V}from"../lib/plugin-chrome.js";import{helpPopovers as We}from"../lib/help-popover.js";import{onceClosed as Se,openMenuAt as Ye}from"../lib/tool-kit.js";import{pickMedia as $t}from"../lib/media-picker.js";import{openPageClone as jt}from"../lib/page-clone.js";import{openEditorReference as Ge}from"../lib/page-editor-reference.js";import{cleanType as Q,DEFAULT_TYPE as ee,EDITOR_FONTS as Vt,fontHref as qt,isDefaultType as Ht,LINE_HEIGHTS as Kt,SIZES as Ut,typeVars as Xe}from"../lib/editor-type.js";import{getProjectFromHash as _t}from"../lib/project-context.js";import{attachSmoothCaret as Ze,getCaretPreference as Jt,setCaretPreference as Wt}from"../lib/smooth-caret.js";import{SCREEN as Yt,bannerSection as Gt,draftIsStale as Qe,draftKey as Xt,initialProject as Zt,makeDraft as Qt,problems as Te,readDraft as ea,snapshot as j}from"../lib/page-editor-arrange.js";import{fmtDateTime as te}from"../lib/dates.js";import{heldBlockEdits as ta,hold as et,mountEditorHop as aa,noteRecent as sa,takeHeld as oa}from"../lib/editor-hop.js";import{isHop as na}from"../lib/editor-hop-arrange.js";import{blockTagDefs as ra,blocksInBody as ia}from"../lib/block-tags.js";function ae(o){const l=document.createElement("template");return l.innerHTML=o.trim(),l.content.firstElementChild}const la=te,tt=o=>te(o.createdAt)+(o.label?` - ${o.label}`:""),ca=`
2
2
  <div class="pe-share">
3
3
  <p class="form-hint" style="margin-top:0">Anyone with the link can view this page even while it is unpublished, without
4
4
  signing in. The link works for this page only, expires on its own, and can be revoked at any time.</p>
@@ -30,7 +30,7 @@ import{api as v}from"../api.js";import{CardBuilder as Mt}from"../lib/card-builde
30
30
  .pe-share [hidden] { display: none !important; }
31
31
  .pe-share-row { display: flex; align-items: center; gap: .6rem; padding: .5rem 0; border-bottom: 1px solid var(--dm-border-light); }
32
32
  .pe-share-text { flex: 1 1 auto; min-width: 0; font-size: .82rem; }
33
- </style>`;function et(o){const l=zt(o);if(!l||document.querySelector(`link[data-pe-font="${CSS.escape(o)}"]`))return;const c=Object.assign(document.createElement("link"),{rel:"stylesheet",href:l});c.dataset.peFont=o,document.head.appendChild(c)}const na=`
33
+ </style>`;function at(o){const l=qt(o);if(!l||document.querySelector(`link[data-pe-font="${CSS.escape(o)}"]`))return;const c=Object.assign(document.createElement("link"),{rel:"stylesheet",href:l});c.dataset.peFont=o,document.head.appendChild(c)}const da=`
34
34
  <div class="pe-settings">
35
35
  <p class="pe-settings-note">How the editor looks to you - kept in this browser, for every page.</p>
36
36
  <label class="form-label" for="pe-set-font">Font <span data-help="The lettering in the editor (not on the site). Monospace keeps every character the same width, so shortcodes and Markdown line up."></span></label>
@@ -68,17 +68,17 @@ The quick brown fox - 0O 1lI {} [] () =&gt; ==
68
68
  .pe-settings-sample { white-space: pre-wrap; padding: .75rem 1rem; border-radius: var(--dm-radius-md); border: 1px solid var(--dm-border);
69
69
  background: var(--dm-background-alt); font-family: var(--pe-font); font-size: var(--pe-size); line-height: var(--pe-line); }
70
70
  .pe-settings-actions { display: flex; gap: .5rem; border-top: 1px solid var(--dm-border); padding-top: .75rem; }
71
- </style>`;function ra(o){const l=E.slideover({title:"Editor settings",size:"sm",position:"right"}),c=te(`<div>${na}</div>`),s=Z(o.peek()),i={font:M.observable(s.font),size:M.observable(String(s.size)),line:M.observable(String(s.line)),wrap:M.observable(s.wrap),caret:M.observable(Ht())},u=M.computed(()=>Z({font:i.font.value,size:i.size.value,line:i.line.value,wrap:i.wrap.value}));i.isDefault=M.computed(()=>$t(u.value)&&i.caret.value==="auto");const p=M.effect(()=>Kt(i.caret.value)),r=c.querySelector("#pe-set-font");for(const[g,A]of[["mono","Monospace"],["sans","Other"],["serif",null]]){const j=Bt.filter(D=>D.kind===g),f=A?Object.assign(document.createElement("optgroup"),{label:A}):r.querySelector("optgroup:last-of-type");f.append(...j.map(D=>new Option(D.label,D.id))),A&&r.appendChild(f)}c.querySelector("#pe-set-size").append(...Vt.map(g=>new Option(`${g}px`,String(g)))),c.querySelector("#pe-set-line").append(...jt.map(g=>new Option({1.3:"Tight",1.45:"Snug",1.6:"Normal",1.8:"Relaxed",2:"Double"}[g]||String(g),String(g))));const d=c.querySelector(".pe-settings-sample"),C=M.effect(()=>{const g=u.value;et(g.font);for(const[A,j]of Object.entries(Ye(g)))d.style.setProperty(A,j);d.style.whiteSpace=g.wrap?"pre-wrap":"pre",d.style.overflowX=g.wrap?"":"auto",JSON.stringify(g)!==JSON.stringify(Z(o.peek()))&&(o.value=g,ae("type",g))});i.reset=()=>{i.font.value=Q.font,i.size.value=String(Q.size),i.line.value=String(Q.line),i.wrap.value=Q.wrap,i.caret.value="auto"},i.done=()=>l.close(),l.element.appendChild(c);const b=M.applyBindings(i,c),h=_e(c);return we(l,()=>{b.dispose(),h(),C?.(),p?.()}),Domma.icons?.scan?.(c),l.open(),l}async function ia(o){const l=E.slideover({title:"Share preview link",size:"md",position:"right"}),c=te(`<div>${oa}</div>`),s={expiry:M.observable("7d"),note:M.observable(""),creating:M.observable(!1),url:M.observable(""),links:M.observable([]),loaded:M.observable(!1),listError:M.observable("")},i=async()=>{try{const p=await v.pages.previewLinks.list(o);s.links.value=p.filter(r=>!r.revokedAt&&!r.expired).map(r=>({id:r.id,title:r.label||"Untitled link",meta:`Expires ${sa(r.expiresAt)} - ${r.useCount===1?"1 view":`${r.useCount||0} views`}`})),s.listError.value=""}catch(p){s.listError.value="Could not load existing links: "+(p.message||"unknown error")}finally{s.loaded.value=!0}};s.create=async()=>{s.creating.value=!0;try{const p=await v.pages.previewLinks.create(o,s.expiry.peek(),s.note.peek().trim());s.url.value=p.url,s.note.value="",E.toast("Share link created.",{type:"success"}),await i(),c.querySelector(".pe-share-url")?.select()}catch(p){E.toast("Could not create link: "+(p.message||"unknown error"),{type:"error"})}finally{s.creating.value=!1}},s.copy=async()=>{try{await navigator.clipboard.writeText(s.url.peek()),E.toast("Link copied to clipboard.",{type:"success"})}catch{c.querySelector(".pe-share-url")?.select(),E.toast("Press Ctrl+C to copy the selected link.",{type:"info"})}},s.revoke=async p=>{if(await E.confirm("Revoke this link? Anyone holding it loses access immediately."))try{await v.pages.previewLinks.revoke(p),E.toast("Link revoked.",{type:"success"}),await i()}catch(r){E.toast("Could not revoke: "+(r.message||"unknown error"),{type:"error"})}};const u=M.applyBindings(s,c);we(l,()=>u.dispose()),l.element.appendChild(c),l.open(),await i()}function la(o){return new Promise(l=>{const c=E.slideover({title:"Save Named Version",size:"sm",position:"right"}),s=te(`
71
+ </style>`;function pa(o){const l=E.slideover({title:"Editor settings",size:"sm",position:"right"}),c=ae(`<div>${da}</div>`),s=Q(o.peek()),i={font:M.observable(s.font),size:M.observable(String(s.size)),line:M.observable(String(s.line)),wrap:M.observable(s.wrap),caret:M.observable(Jt())},m=M.computed(()=>Q({font:i.font.value,size:i.size.value,line:i.line.value,wrap:i.wrap.value}));i.isDefault=M.computed(()=>Ht(m.value)&&i.caret.value==="auto");const p=M.effect(()=>Wt(i.caret.value)),r=c.querySelector("#pe-set-font");for(const[g,L]of[["mono","Monospace"],["sans","Other"],["serif",null]]){const D=Vt.filter(O=>O.kind===g),f=L?Object.assign(document.createElement("optgroup"),{label:L}):r.querySelector("optgroup:last-of-type");f.append(...D.map(O=>new Option(O.label,O.id))),L&&r.appendChild(f)}c.querySelector("#pe-set-size").append(...Ut.map(g=>new Option(`${g}px`,String(g)))),c.querySelector("#pe-set-line").append(...Kt.map(g=>new Option({1.3:"Tight",1.45:"Snug",1.6:"Normal",1.8:"Relaxed",2:"Double"}[g]||String(g),String(g))));const d=c.querySelector(".pe-settings-sample"),C=M.effect(()=>{const g=m.value;at(g.font);for(const[L,D]of Object.entries(Xe(g)))d.style.setProperty(L,D);d.style.whiteSpace=g.wrap?"pre-wrap":"pre",d.style.overflowX=g.wrap?"":"auto",JSON.stringify(g)!==JSON.stringify(Q(o.peek()))&&(o.value=g,se("type",g))});i.reset=()=>{i.font.value=ee.font,i.size.value=String(ee.size),i.line.value=String(ee.line),i.wrap.value=ee.wrap,i.caret.value="auto"},i.done=()=>l.close(),l.element.appendChild(c);const b=M.applyBindings(i,c),h=We(c);return Se(l,()=>{b.dispose(),h(),C?.(),p?.()}),Domma.icons?.scan?.(c),l.open(),l}async function ua(o){const l=E.slideover({title:"Share preview link",size:"md",position:"right"}),c=ae(`<div>${ca}</div>`),s={expiry:M.observable("7d"),note:M.observable(""),creating:M.observable(!1),url:M.observable(""),links:M.observable([]),loaded:M.observable(!1),listError:M.observable("")},i=async()=>{try{const p=await v.pages.previewLinks.list(o);s.links.value=p.filter(r=>!r.revokedAt&&!r.expired).map(r=>({id:r.id,title:r.label||"Untitled link",meta:`Expires ${la(r.expiresAt)} - ${r.useCount===1?"1 view":`${r.useCount||0} views`}`})),s.listError.value=""}catch(p){s.listError.value="Could not load existing links: "+(p.message||"unknown error")}finally{s.loaded.value=!0}};s.create=async()=>{s.creating.value=!0;try{const p=await v.pages.previewLinks.create(o,s.expiry.peek(),s.note.peek().trim());s.url.value=p.url,s.note.value="",E.toast("Share link created.",{type:"success"}),await i(),c.querySelector(".pe-share-url")?.select()}catch(p){E.toast("Could not create link: "+(p.message||"unknown error"),{type:"error"})}finally{s.creating.value=!1}},s.copy=async()=>{try{await navigator.clipboard.writeText(s.url.peek()),E.toast("Link copied to clipboard.",{type:"success"})}catch{c.querySelector(".pe-share-url")?.select(),E.toast("Press Ctrl+C to copy the selected link.",{type:"info"})}},s.revoke=async p=>{if(await E.confirm("Revoke this link? Anyone holding it loses access immediately."))try{await v.pages.previewLinks.revoke(p),E.toast("Link revoked.",{type:"success"}),await i()}catch(r){E.toast("Could not revoke: "+(r.message||"unknown error"),{type:"error"})}};const m=M.applyBindings(s,c);Se(l,()=>m.dispose()),l.element.appendChild(c),l.open(),await i()}function ma(o){return new Promise(l=>{const c=E.slideover({title:"Save Named Version",size:"sm",position:"right"}),s=ae(`
72
72
  <div style="padding:1rem">
73
73
  <label class="form-label" for="pe-version-label">Version Label</label>
74
74
  <input id="pe-version-label" type="text" class="form-input" style="margin-bottom:.75rem"
75
75
  placeholder="e.g. Initial design, Before restructure" data-model="label.value" data-on-keydown="key">
76
76
  <button type="button" class="btn btn-primary" data-on-click="save" data-bind-disabled="saving.value">Save Version</button>
77
- </div>`);let i=!1;const u={label:M.observable(""),saving:M.observable(!1)};u.save=async()=>{u.saving.value=!0;try{await v.versions.create(o,u.label.peek().trim()||null),E.toast("Named version saved.",{type:"success"}),i=!0,c.close()}catch(r){E.toast("Failed to save version: "+r.message,{type:"error"})}finally{u.saving.value=!1}},u.key=r=>{r.key==="Enter"&&(r.preventDefault(),u.save())};const p=M.applyBindings(u,s);we(c,()=>{p.dispose(),l(i)}),c.element.appendChild(s),c.open(),setTimeout(()=>s.querySelector("input")?.focus(),100)})}let k=null,tt=!1;function ca(){tt||!window.R?.use||(tt=!0,R.use((o,l,c)=>{const s=l?.path||"",i=o?.path||"",u=Ut.test(s)&&i!==s;return u&&k?.path===s&&k.cloneTarget===i?(k.cloneTarget=null,k.keepDraft(),Ze(k.item),c()):!u||!k||k.path!==s||!k.dirty()?c():ea(s,i)?(k.keepDraft(),Ze(k.item),c()):E.confirm("You have unsaved changes to this page. Leave anyway? They are kept as a draft for your next visit.").then(p=>{p?(k?.keepDraft(),c()):history.replaceState(null,"","#"+s)})}),M.subscribe("router:afterChange",({to:o}={})=>{if(k&&(o?.path||"")!==k.path){const l=k;k=null,l.dispose()}}))}function da(o){return o.split(`
77
+ </div>`);let i=!1;const m={label:M.observable(""),saving:M.observable(!1)};m.save=async()=>{m.saving.value=!0;try{await v.versions.create(o,m.label.peek().trim()||null),E.toast("Named version saved.",{type:"success"}),i=!0,c.close()}catch(r){E.toast("Failed to save version: "+r.message,{type:"error"})}finally{m.saving.value=!1}},m.key=r=>{r.key==="Enter"&&(r.preventDefault(),m.save())};const p=M.applyBindings(m,s);Se(c,()=>{p.dispose(),l(i)}),c.element.appendChild(s),c.open(),setTimeout(()=>s.querySelector("input")?.focus(),100)})}let k=null,st=!1;function fa(){st||!window.R?.use||(st=!0,R.use((o,l,c)=>{const s=l?.path||"",i=o?.path||"",m=Yt.test(s)&&i!==s;return m&&k?.path===s&&k.cloneTarget===i?(k.cloneTarget=null,k.keepDraft(),et(k.item),c()):!m||!k||k.path!==s||!k.dirty()?c():na(s,i)?(k.keepDraft(),et(k.item),c()):E.confirm("You have unsaved changes to this page. Leave anyway? They are kept as a draft for your next visit.").then(p=>{p?(k?.keepDraft(),c()):history.replaceState(null,"","#"+s)})}),M.subscribe("router:afterChange",({to:o}={})=>{if(k&&(o?.path||"")!==k.path){const l=k;k=null,l.dispose()}}))}function va(o){return o.split(`
78
78
  `).map(l=>l.trimEnd()).join(`
79
79
  `).replace(/\n{3,}/g,`
80
80
 
81
- `)}const at="editor_prefs";function q(){return S.get(at)||{viewMode:"split",fullscreen:!1}}function ae(o,l){const c=q();c[o]=l,S.set(at,c)}const pa=["edit","details","seo","dconfig","live","history"],st=["split","write","preview"],ua=o=>o.replace(/<\/head>/i,"<style>body.dm-cloaked{opacity:1 !important;transition:none !important}</style></head>");function ot(o,l,c=()=>{}){let s=0;try{s=o.contentWindow.scrollY||0}catch{}o.addEventListener("load",()=>{try{o.contentWindow.scrollTo(0,s)}catch{}c()},{once:!0}),o.srcdoc=l}export const pageEditorView={templateUrl:"/admin/js/templates/page-editor.html",async onMount(o){if(ca(),k){const e=k;k=null,e.dispose()}const l=window.location.hash.replace(/^#/,""),c=[],s=e=>(c.push(e),e);let i=()=>!1,u=()=>{};const p=window.location.hash.match(/#\/pages\/edit(\/[^?]*)/),r=p?p[1]:null,d=!!r,C={path:l,item:{kind:"page",id:r||""},dirty:()=>i(),keepDraft:()=>u(),dispose:()=>{c.splice(0).forEach(e=>{try{e()}catch{}})}};k=C;const b=o.get(0),h=()=>k===C&&b.isConnected,g=E.loader(b,{type:"dots"}),[A,j,f,D,Me,nt,rt,Se]=await Promise.all([v.layouts.get().catch(()=>({})),v.settings.get().catch(()=>({})),d?v.pages.get(r).catch(()=>null):null,v.pages.list().catch(()=>[]),v.projects.list().catch(()=>[]),v.pages.tags().catch(()=>[]),v.listTags.list("pages").then(e=>e.map(t=>t.name)).catch(()=>[]),v.blocks.list().catch(()=>[])]);if(!h()){g.destroy();return}if(g.destroy(),d&&!f){E.toast("Page not found.",{type:"error"}),R.navigate("/pages");return}const w=e=>b.querySelector(e);if(Dt(w("#field-theme"),{includeDefault:!0}),await ga(w("#field-visibility"),f?.visibility),!h())return;const it=ba(w("#field-layout"),A,f?.layout||"default");for(const e of Me)w("#field-project").append(new Option(e.name||e.slug,e.slug));const se=ta(Se),H=q(),a={urlPath:M.observable(r||""),title:M.observable(f?.title||""),description:M.observable(f?.description||""),layout:M.observable(it),status:M.observable(f?.status||"draft"),sortOrder:M.observable(String(f?.sortOrder??99)),showInNav:M.observable(!!f?.showInNav),sidebar:M.observable(!!f?.sidebar),breadcrumbs:M.observable(f?f.breadcrumbs!==!1:!0),bundled:M.observable(!!f?.bundled),tags:M.observable(Array.isArray(f?.tags)?f.tags:[]),adminTags:M.observable(Array.isArray(f?.adminTags)?f.adminTags:[]),category:M.observable(f?.category||""),visibility:M.observable(typeof f?.visibility=="string"&&f.visibility?f.visibility:"public"),project:M.observable(Wt({isEdit:d,page:f,projects:Me,fromHash:qt()||"",urlPath:r||""})),theme:M.observable(f?.theme||""),seoTitle:M.observable(f?.seo?.title||""),seoDesc:M.observable(f?.seo?.description||""),dconfig:M.observable(f?.dconfig?JSON.stringify(f.dconfig,null,2):""),css:M.observable(""),cssOpened:M.observable(!1)},n={bodyRev:M.observable(0),base:M.observable(""),saving:M.observable(!1),tab:M.observable("edit"),viewMode:M.observable(st.includes(H.viewMode)?H.viewMode:"split"),fullscreen:M.observable(!!H.fullscreen),cssMode:M.observable(!1),folded:M.observable(!1),liveStatus:M.observable("Showing unsaved changes"),liveWidth:M.observable("full"),versions:M.observable([]),histLoading:M.observable(!1),histError:M.observable(""),compare:M.observable(null)},T=w("#markdown-editor"),F=w("#css-editor");T.value=f?.content||"";const lt=Ge(T),ct=Ge(F);s(()=>lt.destroy()),s(()=>ct.destroy());const O=document.createElement("div");O.className="editor-line-numbers editor-line-numbers--foldable",T.parentElement.insertBefore(O,T);const x=Ot(T,O,{onFoldChange:()=>{}});let B=!1;x.setGuard(e=>{B=e});const z=()=>x.getUnfoldedContent(),Te=M.observable(Z(H.type));s(M.effect(()=>{const e=Te.value,t=w("#editor-body");if(t){et(e.font);for(const[m,y]of Object.entries(Ye(e)))t.style.setProperty(m,y);t.classList.toggle("pe-nowrap",!e.wrap),T.wrap=e.wrap?"soft":"off",F.wrap=e.wrap?"soft":"off"}}));let Ce=null;s(()=>{try{Ce?.close()}catch{}});const N=()=>(n.bodyRev.value,{urlPath:a.urlPath.value.trim(),title:a.title.value,description:a.description.value,layout:a.layout.value,status:a.status.value,sortOrder:a.sortOrder.value,showInNav:a.showInNav.value,sidebar:a.sidebar.value,breadcrumbs:a.breadcrumbs.value,bundled:a.bundled.value,tags:a.tags.value,adminTags:a.adminTags.value,category:a.category.value,visibility:a.visibility.value,project:a.project.value,theme:a.theme.value,seoTitle:a.seoTitle.value,seoDesc:a.seoDesc.value,dconfig:a.dconfig.value,body:z(),css:a.cssOpened.value?a.css.value:null}),dt=M.computed(()=>$(N())!==n.base.value),pt=M.computed(()=>Ee({urlPath:a.urlPath.value,dconfig:a.dconfig.value})[0]?.text||""),ut=M.computed(()=>n.versions.value.some(e=>e.checked.value)),mt=M.computed(()=>n.versions.value.length>0&&n.versions.value.every(e=>e.checked.value)),ft=M.computed(()=>{const e=n.versions.value.filter(t=>t.checked.value).length;return e>0&&e<n.versions.value.length});i=()=>$(N())!==n.base.peek();let xe=JSON.stringify(a.adminTags.peek());const K=e=>({title:a.title.peek().trim()||"Untitled",description:a.description.peek().trim(),layout:a.layout.peek(),status:a.status.peek(),sortOrder:parseInt(String(a.sortOrder.peek()),10)||99,showInNav:a.showInNav.peek(),sidebar:a.sidebar.peek(),...a.breadcrumbs.peek()?{}:{breadcrumbs:!1},...a.bundled.peek()?{bundled:!0}:{},tags:a.tags.peek(),...d&&JSON.stringify(a.adminTags.peek())===xe?{}:{adminTags:a.adminTags.peek()},category:a.category.peek().trim()||null,visibility:a.visibility.peek()||"public",project:a.project.peek()||"",seo:{title:a.seoTitle.peek().trim(),description:a.seoDesc.peek().trim()},dconfig:e,...a.theme.peek()?{theme:a.theme.peek()}:{}}),oe=()=>{const e=a.dconfig.peek().trim();if(!e)return null;try{return JSON.parse(e)}catch{return null}},Pe=()=>a.urlPath.peek().trim()||r||"/",U=w("#markdown-preview");let Le="";v.settings.getCustomCss().then(({css:e})=>{Le=e||""}).catch(()=>{});const Ae=()=>{const e=U?.contentDocument;if(!e?.head)return;const t=a.cssOpened.peek()?a.css.peek():"";if(!t)return;let m=e.getElementById("preview-custom-css");m||(m=e.createElement("style"),m.id="preview-custom-css",e.head.appendChild(m)),m.textContent=t};let ne=null;const _=()=>{clearTimeout(ne),ne=setTimeout(async()=>{if(!(!U||n.viewMode.peek()==="write"))try{const{html:e}=await v.pages.previewFull(Pe(),K(oe()),z());h()&&ot(U,ua(e),Ae)}catch(e){U.srcdoc='<!doctype html><meta charset="utf-8"><body style="font:14px system-ui;padding:1rem;color:#b91c1c;">Preview failed: '+String(e?.message||"unknown error").replace(/[<&]/g,t=>t==="<"?"&lt;":"&amp;")+"</body>"}},500)},De=w("#live-preview-frame");let re=!0,ie=null,le=!1;const ce=async()=>{if(!(le||!De)){le=!0,n.liveStatus.value="Rendering\u2026";try{const{html:e}=await v.pages.previewFull(Pe(),K(oe()),z());if(!h())return;ot(De,e),re=!1,n.liveStatus.value="Showing unsaved changes"}catch(e){n.liveStatus.value="Preview failed - "+(e?.message||"unknown error")}finally{le=!1}}},Oe=()=>{re=!0,n.tab.peek()==="live"&&(clearTimeout(ie),ie=setTimeout(ce,600))};s(()=>{clearTimeout(ne),clearTimeout(ie)});const de=Ct(o.find("#markdown-editor"),o.find("#editor-toolbar"),{spacerDefault:j?.layoutOptions?.spacerSize??40});Lt(T,{blocks:()=>se});const pe=b.querySelector(".editor-toolbar-right");pe.querySelectorAll(".editor-view-btn[data-mode]").forEach(e=>{const t=e.getAttribute("data-mode");e.classList.remove("active"),e.setAttribute("data-bind-class",`viewMode.value === '${t}' && 'active'`),e.setAttribute("data-on-click",`setMode('${t}')`)}),w("#fullscreen-btn")?.setAttribute("data-on-click","toggleFullscreen");const vt=te(`
81
+ `)}const ot="editor_prefs";function q(){return S.get(ot)||{viewMode:"split",fullscreen:!1}}function se(o,l){const c=q();c[o]=l,S.set(ot,c)}const ba=["edit","details","seo","dconfig","live","history"],nt=["split","write","preview"],ga=o=>o.replace(/<\/head>/i,"<style>body.dm-cloaked{opacity:1 !important;transition:none !important}</style></head>");function rt(o,l,c=()=>{}){let s=0;try{s=o.contentWindow.scrollY||0}catch{}o.addEventListener("load",()=>{try{o.contentWindow.scrollTo(0,s)}catch{}c()},{once:!0}),o.srcdoc=l}export const pageEditorView={templateUrl:"/admin/js/templates/page-editor.html",async onMount(o){if(fa(),k){const e=k;k=null,e.dispose()}const l=window.location.hash.replace(/^#/,""),c=[],s=e=>(c.push(e),e);let i=()=>!1,m=()=>{};const p=window.location.hash.match(/#\/pages\/edit(\/[^?]*)/),r=p?p[1]:null,d=!!r,C={path:l,item:{kind:"page",id:r||""},dirty:()=>i(),keepDraft:()=>m(),dispose:()=>{c.splice(0).forEach(e=>{try{e()}catch{}})}};k=C;const b=o.get(0),h=()=>k===C&&b.isConnected,g=E.loader(b,{type:"dots"}),[L,D,f,O,Ce,it,lt,H]=await Promise.all([v.layouts.get().catch(()=>({})),v.settings.get().catch(()=>({})),d?v.pages.get(r).catch(()=>null):null,v.pages.list().catch(()=>[]),v.projects.list().catch(()=>[]),v.pages.tags().catch(()=>[]),v.listTags.list("pages").then(e=>e.map(t=>t.name)).catch(()=>[]),v.blocks.list().catch(()=>[])]);if(!h()){g.destroy();return}if(g.destroy(),d&&!f){E.toast("Page not found.",{type:"error"}),R.navigate("/pages");return}const w=e=>b.querySelector(e);if(Bt(w("#field-theme"),{includeDefault:!0}),await Ea(w("#field-visibility"),f?.visibility),!h())return;const ct=wa(w("#field-layout"),L,f?.layout||"default");for(const e of Ce)w("#field-project").append(new Option(e.name||e.slug,e.slug));const oe=ra(H),K=q(),a={urlPath:M.observable(r||""),title:M.observable(f?.title||""),description:M.observable(f?.description||""),layout:M.observable(ct),status:M.observable(f?.status||"draft"),sortOrder:M.observable(String(f?.sortOrder??99)),showInNav:M.observable(!!f?.showInNav),sidebar:M.observable(!!f?.sidebar),breadcrumbs:M.observable(f?f.breadcrumbs!==!1:!0),bundled:M.observable(!!f?.bundled),tags:M.observable(Array.isArray(f?.tags)?f.tags:[]),adminTags:M.observable(Array.isArray(f?.adminTags)?f.adminTags:[]),category:M.observable(f?.category||""),visibility:M.observable(typeof f?.visibility=="string"&&f.visibility?f.visibility:"public"),project:M.observable(Zt({isEdit:d,page:f,projects:Ce,fromHash:_t()||"",urlPath:r||""})),theme:M.observable(f?.theme||""),seoTitle:M.observable(f?.seo?.title||""),seoDesc:M.observable(f?.seo?.description||""),dconfig:M.observable(f?.dconfig?JSON.stringify(f.dconfig,null,2):""),css:M.observable(""),cssOpened:M.observable(!1)},n={bodyRev:M.observable(0),base:M.observable(""),saving:M.observable(!1),tab:M.observable("edit"),viewMode:M.observable(nt.includes(K.viewMode)?K.viewMode:"split"),fullscreen:M.observable(!!K.fullscreen),cssMode:M.observable(!1),folded:M.observable(!1),liveStatus:M.observable("Showing unsaved changes"),liveWidth:M.observable("full"),versions:M.observable([]),histLoading:M.observable(!1),histError:M.observable(""),compare:M.observable(null),heldNote:M.observable("")},T=w("#markdown-editor"),F=w("#css-editor");T.value=f?.content||"";const dt=Ze(T),pt=Ze(F);s(()=>dt.destroy()),s(()=>pt.destroy());const N=document.createElement("div");N.className="editor-line-numbers editor-line-numbers--foldable",T.parentElement.insertBefore(N,T);const x=It(T,N,{onFoldChange:()=>{}});let z=!1;x.setGuard(e=>{z=e});const $=()=>x.getUnfoldedContent(),xe=M.observable(Q(K.type));s(M.effect(()=>{const e=xe.value,t=w("#editor-body");if(t){at(e.font);for(const[u,y]of Object.entries(Xe(e)))t.style.setProperty(u,y);t.classList.toggle("pe-nowrap",!e.wrap),T.wrap=e.wrap?"soft":"off",F.wrap=e.wrap?"soft":"off"}}));let Pe=null;s(()=>{try{Pe?.close()}catch{}});const B=()=>(n.bodyRev.value,{urlPath:a.urlPath.value.trim(),title:a.title.value,description:a.description.value,layout:a.layout.value,status:a.status.value,sortOrder:a.sortOrder.value,showInNav:a.showInNav.value,sidebar:a.sidebar.value,breadcrumbs:a.breadcrumbs.value,bundled:a.bundled.value,tags:a.tags.value,adminTags:a.adminTags.value,category:a.category.value,visibility:a.visibility.value,project:a.project.value,theme:a.theme.value,seoTitle:a.seoTitle.value,seoDesc:a.seoDesc.value,dconfig:a.dconfig.value,body:$(),css:a.cssOpened.value?a.css.value:null}),ut=M.computed(()=>j(B())!==n.base.value),mt=M.computed(()=>Te({urlPath:a.urlPath.value,dconfig:a.dconfig.value})[0]?.text||""),ft=M.computed(()=>n.versions.value.some(e=>e.checked.value)),vt=M.computed(()=>n.versions.value.length>0&&n.versions.value.every(e=>e.checked.value)),bt=M.computed(()=>{const e=n.versions.value.filter(t=>t.checked.value).length;return e>0&&e<n.versions.value.length});i=()=>j(B())!==n.base.peek();let Ae=JSON.stringify(a.adminTags.peek());const U=e=>({title:a.title.peek().trim()||"Untitled",description:a.description.peek().trim(),layout:a.layout.peek(),status:a.status.peek(),sortOrder:parseInt(String(a.sortOrder.peek()),10)||99,showInNav:a.showInNav.peek(),sidebar:a.sidebar.peek(),...a.breadcrumbs.peek()?{}:{breadcrumbs:!1},...a.bundled.peek()?{bundled:!0}:{},tags:a.tags.peek(),...d&&JSON.stringify(a.adminTags.peek())===Ae?{}:{adminTags:a.adminTags.peek()},category:a.category.peek().trim()||null,visibility:a.visibility.peek()||"public",project:a.project.peek()||"",seo:{title:a.seoTitle.peek().trim(),description:a.seoDesc.peek().trim()},dconfig:e,...a.theme.peek()?{theme:a.theme.peek()}:{}}),ne=()=>{const e=a.dconfig.peek().trim();if(!e)return null;try{return JSON.parse(e)}catch{return null}},Le=()=>a.urlPath.peek().trim()||r||"/";let re=[];const gt=Array.isArray(D?.footer?.blocks)?D.footer.blocks.filter(e=>typeof e=="string"):[],ht=new Map((Array.isArray(H)?H:[]).map(e=>[e.name,e.updatedAt])),ie=()=>{const e=ta([...new Set([...re,...gt])],ht),t=Object.keys(e),u=t.length?`Preview shows unsaved changes to ${t.join(", ")}`:"";return u!==n.heldNote.peek()&&(n.heldNote.value=u),e},_=w("#markdown-preview");let De="";v.settings.getCustomCss().then(({css:e})=>{De=e||""}).catch(()=>{});const Oe=()=>{const e=_?.contentDocument;if(!e?.head)return;const t=a.cssOpened.peek()?a.css.peek():"";if(!t)return;let u=e.getElementById("preview-custom-css");u||(u=e.createElement("style"),u.id="preview-custom-css",e.head.appendChild(u)),u.textContent=t};let le=null;const J=()=>{clearTimeout(le),le=setTimeout(async()=>{if(!(!_||n.viewMode.peek()==="write"))try{const{html:e}=await v.pages.previewFull(Le(),U(ne()),$(),ie());h()&&rt(_,ga(e),Oe)}catch(e){_.srcdoc='<!doctype html><meta charset="utf-8"><body style="font:14px system-ui;padding:1rem;color:#b91c1c;">Preview failed: '+String(e?.message||"unknown error").replace(/[<&]/g,t=>t==="<"?"&lt;":"&amp;")+"</body>"}},500)},Re=w("#live-preview-frame");let ce=!0,de=null,pe=!1;const ue=async()=>{if(!(pe||!Re)){pe=!0,n.liveStatus.value="Rendering\u2026";try{const{html:e}=await v.pages.previewFull(Le(),U(ne()),$(),ie());if(!h())return;rt(Re,e),ce=!1,n.liveStatus.value="Showing unsaved changes"}catch(e){n.liveStatus.value="Preview failed - "+(e?.message||"unknown error")}finally{pe=!1}}},Ne=()=>{ce=!0,n.tab.peek()==="live"&&(clearTimeout(de),de=setTimeout(ue,600))};s(()=>{clearTimeout(le),clearTimeout(de)});const me=Lt(o.find("#markdown-editor"),o.find("#editor-toolbar"),{spacerDefault:D?.layoutOptions?.spacerSize??40});Rt(T,{blocks:()=>oe});const fe=b.querySelector(".editor-toolbar-right");fe.querySelectorAll(".editor-view-btn[data-mode]").forEach(e=>{const t=e.getAttribute("data-mode");e.classList.remove("active"),e.setAttribute("data-bind-class",`viewMode.value === '${t}' && 'active'`),e.setAttribute("data-on-click",`setMode('${t}')`)}),w("#fullscreen-btn")?.setAttribute("data-on-click","toggleFullscreen");const yt=ae(`
82
82
  <span class="pe-toolbar-extras" style="display:contents">
83
83
  <button type="button" class="editor-view-btn" data-tip="Edit Custom CSS" data-on-click="toggleCss"
84
84
  data-bind-class="cssMode.value && 'active'"><span>CSS</span></button>
@@ -90,5 +90,5 @@ The quick brown fox - 0O 1lI {} [] () =&gt; ==
90
90
  <span class="pe-fold-icon" hidden data-bind-hidden="!folded.value"><span data-icon="chevrons-right" data-icon-size="10"></span></span>
91
91
  </button>
92
92
  <span class="editor-toolbar-sep"></span>
93
- </span>`);pe.prepend(vt);const Re=e=>(Array.isArray(e)?e:[]).map(t=>({value:t,label:t})),Ne=w("#field-tags");let J=null;J=Ne?E.pillbox(Ne,{data:Re(nt),value:a.tags.peek(),creatable:!0,searchable:!0,placeholder:"Add tag\u2026",onChange:()=>{J&&(a.tags.value=[...J.getValue()])}}):null;const Ie=w("#field-admin-tags");let W=null;W=Ie?E.pillbox(Ie,{data:Re(rt),value:a.adminTags.peek(),creatable:!0,searchable:!0,placeholder:"Add admin tag\u2026",onChange:()=>{W&&(a.adminTags.value=[...W.getValue()])}}):null;let ue=!1,me=null;const Y=async()=>{ue=!0,n.histLoading.value=!0,n.histError.value="";try{const e=(await v.versions.list(r)).versions||[];if(!h())return;n.versions.value=e.map(t=>({...t,when:ee(t.createdAt),author:t.author||"",label:t.label||"",checked:M.observable(!1)}))}catch(e){n.histError.value="Failed to load versions: "+e.message}finally{n.histLoading.value=!1}},fe=e=>n.versions.peek().find(t=>t.filename===e),ve=async e=>{try{const t=await v.versions.get(r,e.filename);me=e.filename,n.compare.value={label:Qe(e),old:t.content,current:z()}}catch(t){E.toast("Failed to load version: "+t.message,{type:"error"})}},Fe=async e=>{if(!(!e||!await E.confirm("Restore this version? The current page content will be overwritten (a pre-restore snapshot will be saved automatically).")))try{await v.versions.restore(r,e),E.toast("Page restored. Reloading editor\u2026",{type:"success"}),R.navigate("/pages"),setTimeout(()=>R.navigate(`/pages/edit${r}`),300)}catch(t){E.toast("Restore failed: "+t.message,{type:"error"})}},bt=async e=>{if(await E.confirm("Delete this version? This cannot be undone."))try{await v.versions.delete(r,e.filename),E.toast("Version deleted.",{type:"success"}),await Y()}catch(t){E.toast("Failed to delete: "+t.message,{type:"error"})}},Be=()=>{n.bodyRev.value=n.bodyRev.peek()+1},ze=()=>{a.cssOpened.peek()||(a.css.value=Le,a.cssOpened.value=!0,Tt(F))},be=e=>{for(const t of["urlPath","title","description","layout","status","sortOrder","category","visibility","project","theme","seoTitle","seoDesc","dconfig"])a[t].value=e[t]==null?"":String(e[t]);for(const t of["showInNav","sidebar","breadcrumbs","bundled"])a[t].value=!!e[t];a.tags.value=Array.isArray(e.tags)?e.tags:[],J?.setValue(a.tags.peek()),Array.isArray(e.adminTags)&&(a.adminTags.value=e.adminTags),W?.setValue(a.adminTags.peek()),x.unfoldAll(),B=!0,T.value=e.body==null?"":String(e.body),B=!1,x.refresh(),e.css!=null&&(ze(),a.css.value=String(e.css)),Be()};n.base.value=$(N());const G=Jt(r||"");u=()=>{if(i())try{S.set(G,Yt(N(),n.base.peek()))}catch{}};const ge=()=>{try{S.remove?S.remove(G):S.set(G,null)}catch{}},$e=e=>{i()&&(u(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",$e),s(()=>window.removeEventListener("beforeunload",$e));const je=async()=>{if(n.saving.peek())return;const e=N(),t=Ee(e);if(t.length){E.toast(t[0].text,{type:"warning"}),b.querySelectorAll("#editor-meta-tabs .tab-item")[pa.indexOf(t[0].field==="dconfig"?"dconfig":"details")]?.click();return}const m=(e.dconfig||"").trim(),y=K(m?JSON.parse(m):null),L=a.cssOpened.peek();n.saving.value=!0;try{if(d){const I={frontmatter:y,body:e.body};e.urlPath!==r&&(I.newUrlPath=e.urlPath),await v.pages.update(r,I)}else await v.pages.create({urlPath:e.urlPath,frontmatter:y,body:e.body});if(L&&await v.settings.saveCustomCss(a.css.peek()),E.toast(L?"Page and CSS saved.":"Page saved.",{type:"success"}),!h())return;ge(),d&&Zt({kind:"page",id:e.urlPath,label:a.title.peek().trim()||"Untitled"}),xe=JSON.stringify(a.adminTags.peek()),n.base.value=$(N()),d?e.urlPath!==r&&R.navigate(`/pages/edit${e.urlPath}`):R.navigate("/pages")}catch(I){E.toast(`Save failed: ${I.message||"Unknown error"}`,{type:"error"})}finally{n.saving.value=!1}},Ve=b.querySelector(".dm-plugin-body")||b,gt={...a,...n,dirty:dt,problem:pt,anySelected:ut,allSelected:mt,someSelected:ft,isEdit:d,savedPath:r||""},ht={save:()=>je(),discard:async()=>{!i()||!await E.confirm("Discard the changes you have not saved?")||(be(JSON.parse(n.base.peek())),ge())},showTab:e=>{n.tab.value=e,e==="live"&&re&&ce(),e==="history"&&d&&!ue&&Y()},setMode:e=>{n.cssMode.peek()&&(n.cssMode.value=!1),n.viewMode.value=e,ae("viewMode",e),e!=="write"&&_()},toggleFullscreen:()=>{n.fullscreen.value=!n.fullscreen.peek(),ae("fullscreen",n.fullscreen.peek())},toggleCss:()=>{n.cssMode.peek()?(n.cssMode.value=!1,n.viewMode.value=st.includes(q().viewMode)?q().viewMode:"split",x.refresh()):(ze(),n.cssMode.value=!0,n.viewMode.value="split")},formatMarkdown:()=>{const e=T.selectionStart,t=da(z());x.unfoldAll(),B=!0,T.value=t,B=!1,T.selectionStart=T.selectionEnd=Math.min(e,t.length),T.dispatchEvent(new Event("input")),x.refresh(),E.toast("Markdown formatted.",{type:"success"})},toggleFold:()=>{n.folded.peek()?x.unfoldAll():x.foldAll(),n.folded.value=!n.folded.peek()},setWidth:e=>{n.liveWidth.value=e;const t=w("#live-preview-stage");t&&(t.scrollTop=0)},refreshLive:()=>ce(),share:async()=>{if(!d){E.toast("Save the page first - a share link needs a page to point at.",{type:"warning"});return}await ia(r)},toggleAll:e=>{const t=!!e?.target?.checked;n.versions.peek().forEach(m=>{m.checked.value=t})},deleteSelected:async()=>{const e=n.versions.peek().filter(m=>m.checked.peek()).map(m=>m.filename),t=e.length;if(!(!t||!await E.confirm(`Delete ${t} version${t>1?"s":""}? This cannot be undone.`)))try{await v.versions.bulkDelete(r,e),E.toast(`${t} version${t>1?"s":""} deleted.`,{type:"success"}),await Y()}catch(m){E.toast("Delete failed: "+m.message,{type:"error"})}},saveNamed:async()=>{await la(r)&&h()&&(n.tab.peek()==="history"?await Y():ue=!1)},closeCompare:()=>{n.compare.value=null,me=null},restoreCompared:()=>Fe(me)};pe.querySelectorAll("[data-tip]").forEach(e=>E.tooltip(e,{content:e.getAttribute("data-tip"),position:"top"}));const yt=M.applyBindings(gt,Ve,{methods:ht});s(()=>yt.dispose());const qe=Xt(w("#page-hop"),{current:{kind:"page",id:r||"",label:f?.title||r||""},pages:D,blocks:Se});s(()=>qe.dispose()),E.tabs(w("#editor-meta-tabs"));let he=null;s(()=>clearTimeout(he)),T.addEventListener("input",()=>{x.refresh(),O.scrollTop=T.scrollTop,!B&&(clearTimeout(he),he=setTimeout(Be,120),_(),Oe())}),T.addEventListener("scroll",()=>{O.scrollTop=T.scrollTop}),F.addEventListener("scroll",()=>{O.scrollTop=F.scrollTop});let He=!0;s(M.effect(()=>{for(const e of Object.keys(a))e!=="css"&&e!=="cssOpened"&&a[e].value;if(n.bodyRev.value,He){He=!1,_();return}_(),Oe()}));const kt=se.map(e=>e.name),wt=se.filter(e=>e.shortTag).map(e=>e.name);s(M.effect(()=>{n.bodyRev.value,qe.setRelated(aa(z(),kt,wt).map(e=>({kind:"block",id:e,label:e})))}));let ye=null;if(s(()=>clearTimeout(ye)),s(M.effect(()=>{const e=a.css.value;n.cssMode.value&&(ma(O,e,F),clearTimeout(ye),ye=setTimeout(Ae,400))})),d){const e=w(".pe-version-list"),t=E.contextMenu(e,{cursor:"glyph",match:"tr[data-file]",inherit:!1,density:"compact",items:m=>{const y=fe(m.dataset.file);return y?[{header:Qe(y)},{label:"Compare with the editor",icon:"columns",action:()=>ve(y)},{label:"Restore this version\u2026",icon:"rotate-ccw",action:()=>Fe(y.filename)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>bt(y)}]:[]}});s(()=>t?.destroy?.()),e.addEventListener("click",m=>{const y=m.target.closest("tr[data-file]");if(!y||m.target.matches("input"))return;const L=m.target.closest(".pe-kebab");if(L){Je(L);return}const I=fe(y.dataset.file);I&&ve(I)}),e.addEventListener("keydown",m=>{const y=m.target.closest("tr[data-file]");if(y){if(m.key==="F10"&&m.shiftKey)m.preventDefault(),Je(y);else if(m.key==="Enter"&&m.target===y){const L=fe(y.dataset.file);L&&ve(L)}}})}fa(w("#editor-body"),w("#editor-divider"),s),At({image:e=>(n.cssMode.value=!1,va(e)),scribe:e=>Rt(e),card:e=>Mt.open(e,{onMediaBrowse:()=>v.media.list().catch(()=>[])}),timeline:e=>St.open(e,{onPreview:async t=>{try{return await v.pages.preview(t)}catch{return null}},onListCollections:()=>v.collections.list().catch(()=>[]),onGetCollection:t=>v.collections.get(t).catch(()=>null)})}),de.onEffects(e=>Pt.open(e)),de.onHelp(()=>We()),de.onTutorial(e=>Nt(e));const Ke=e=>{(e.ctrlKey||e.metaKey)&&!e.shiftKey&&!e.altKey&&e.key.toLowerCase()==="s"&&h()&&(e.preventDefault(),je())};document.addEventListener("keydown",Ke),s(()=>document.removeEventListener("keydown",Ke));const X=b.querySelector(".dm-plugin-banner-name");s(M.effect(()=>{const e=_t(d,a.title.value,r);if(!h()||(document.title=`${e} - Domma CMS`,!X))return;const t=X.lastChild;t&&t.nodeType===3&&X.querySelector(".dm-plugin-banner-sep")?t.textContent=e:X.textContent=`Pages \u203A ${e}`})),V(o,{id:"page-editor-back",icon:"arrow-left",title:"Back to Pages",onClick:()=>R.navigate("/pages")}),V(o,{id:"page-editor-help",icon:"help-circle",title:"Editor reference",onClick:()=>We()}),d&&V(o,{id:"page-editor-view",icon:"external-link",title:"View on the site",onClick:()=>window.open(a.urlPath.peek()||r,"_blank","noopener")});const ke=V(o,{id:"page-editor-clone",icon:"copy",title:"Clone page",onClick:()=>{if(!d||n.saving.peek()||!h())return;const e=N(),t=Ee({...e,urlPath:r});if(t.length){E.toast(t[0].text,{type:"warning"});return}const m=Ft({page:f,frontmatter:{...K(oe()),adminTags:a.adminTags.peek()},body:e.body,source:r,pages:D,onCloned:y=>{h()&&(C.cloneTarget=`/pages/edit${y}`,R.navigate(C.cloneTarget))}});s(()=>m.close())}});ke&&(ke.disabled=!d,d||(ke.title="Save the page before cloning it")),V(o,{id:"page-editor-settings",icon:"settings",title:"Editor settings - font, size, spacing",onClick:()=>{Ce=ra(Te)}}),s(_e(Ve,{position:"bottom"})),Domma.icons.scan();let Ue=null;try{Ue=S.get(G)}catch{}const P=Gt(Ue),Et=Qt(C.item);if(P){if(ge(),$(P.state)!==n.base.peek()&&Et&&!Xe(P,n.base.peek()))be(P.state),E.toast("Your unsaved changes to this page are back.",{type:"info"});else if($(P.state)!==n.base.peek()){const e=P.at?` (${ee(P.at)})`:"",t=Xe(P,n.base.peek())?" The saved page has changed since - restoring puts your version back over it (the saved one stays in History).":"";await E.confirm(`You left unsaved changes to this page${e}. Restore them?${t}`)&&h()&&be(P.state)}}}};function ma(o,l,c){o.replaceChildren(...String(l).split(`
94
- `).map((s,i)=>{const u=document.createElement("span");u.className="editor-line-number-row";const p=document.createElement("span");p.className="fold-toggle fold-toggle--empty";const r=document.createElement("span");return r.className="editor-line-num",r.textContent=String(i+1),u.append(p,r),u})),o.scrollTop=c.scrollTop}function fa(o,l,c){if(!o||!l)return;const s=15,i=85,u=b=>Math.min(i,Math.max(s,b));let p=u(parseFloat(q().splitRatio)||50);const r=b=>{o.style.setProperty("--dm-editor-split",p+"%"),l.setAttribute("aria-valuenow",String(Math.round(p))),b&&ae("splitRatio",p)};r(!1);const d=b=>{const h=o.getBoundingClientRect();h.width&&(p=u((b.clientX-h.left)/h.width*100)),r(!1)},C=()=>{document.removeEventListener("pointermove",d),document.removeEventListener("pointerup",C),o.classList.remove("is-resizing"),l.classList.remove("is-dragging"),r(!0)};c(C),l.addEventListener("pointerdown",b=>{window.matchMedia("(max-width: 768px)").matches||(b.preventDefault(),o.classList.add("is-resizing"),l.classList.add("is-dragging"),document.addEventListener("pointermove",d),document.addEventListener("pointerup",C))}),l.addEventListener("dblclick",()=>{p=50,r(!0)}),l.addEventListener("keydown",b=>{const h=b.shiftKey?10:2;let g=p;if(b.key==="ArrowLeft")g=p-h;else if(b.key==="ArrowRight")g=p+h;else if(b.key==="Home")g=s;else if(b.key==="End")g=i;else if(b.key==="Enter"||b.key===" ")g=50;else return;b.preventDefault(),p=u(g),r(!0)})}async function va(o){const l=o.selectionStart,c=o.selectionEnd,s=await It({title:"Upload or insert an image"});if(!s||!o.isConnected)return;let i=s.split("/").pop().split("?")[0];try{i=decodeURIComponent(i)}catch{}const u=i.replace(/[\\[\]]/g,"\\$&"),p=s.replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/ /g,"%20");o.setSelectionRange(l,c),xt(o,`![${u}](${p})`),o.dispatchEvent(new Event("input",{bubbles:!0}))}function ba(o,l,c){const s=Object.entries(l||{});return o?(o.replaceChildren(...s.map(([i,u])=>new Option(u?.label||i,i))),s.some(([i])=>i===c)?c:s[0]?.[0]||c):c}async function ga(o,l){if(!o)return;let c=[];try{const d=await v.get("/collections/roles/entries?limit=100");c=Array.isArray(d)?d:d?.entries||[]}catch{}const s=c.map(d=>({name:d.data?.name,label:d.data?.label||d.data?.name,level:d.data?.level??99})).filter(d=>d.name&&d.level!==0).sort((d,C)=>C.level-d.level),i=(d,C)=>Object.assign(document.createElement("option"),{value:d,textContent:C}),u=()=>{typeof l=="string"&&l&&![...o.options].some(d=>d.value===l)&&o.append(i(l,`${l} (custom)`))};if(!s.length)return u();const p=document.createElement("optgroup");p.label="Role and above",s.forEach(d=>p.append(i(d.name,`${d.label} and above`)));const r=document.createElement("optgroup");r.label="Role only",s.forEach(d=>r.append(i(`=${d.name}`,`${d.label} only`))),o.replaceChildren(i("public","Public - everyone"),p,r,i("private","Private - super-admin only")),u()}
93
+ </span>`);fe.prepend(yt);const Be=e=>(Array.isArray(e)?e:[]).map(t=>({value:t,label:t})),Ie=w("#field-tags");let W=null;W=Ie?E.pillbox(Ie,{data:Be(it),value:a.tags.peek(),creatable:!0,searchable:!0,placeholder:"Add tag\u2026",onChange:()=>{W&&(a.tags.value=[...W.getValue()])}}):null;const Fe=w("#field-admin-tags");let Y=null;Y=Fe?E.pillbox(Fe,{data:Be(lt),value:a.adminTags.peek(),creatable:!0,searchable:!0,placeholder:"Add admin tag\u2026",onChange:()=>{Y&&(a.adminTags.value=[...Y.getValue()])}}):null;let ve=!1,be=null;const G=async()=>{ve=!0,n.histLoading.value=!0,n.histError.value="";try{const e=(await v.versions.list(r)).versions||[];if(!h())return;n.versions.value=e.map(t=>({...t,when:te(t.createdAt),author:t.author||"",label:t.label||"",checked:M.observable(!1)}))}catch(e){n.histError.value="Failed to load versions: "+e.message}finally{n.histLoading.value=!1}},ge=e=>n.versions.peek().find(t=>t.filename===e),he=async e=>{try{const t=await v.versions.get(r,e.filename);be=e.filename,n.compare.value={label:tt(e),old:t.content,current:$()}}catch(t){E.toast("Failed to load version: "+t.message,{type:"error"})}},ze=async e=>{if(!(!e||!await E.confirm("Restore this version? The current page content will be overwritten (a pre-restore snapshot will be saved automatically).")))try{await v.versions.restore(r,e),E.toast("Page restored. Reloading editor\u2026",{type:"success"}),R.navigate("/pages"),setTimeout(()=>R.navigate(`/pages/edit${r}`),300)}catch(t){E.toast("Restore failed: "+t.message,{type:"error"})}},kt=async e=>{if(await E.confirm("Delete this version? This cannot be undone."))try{await v.versions.delete(r,e.filename),E.toast("Version deleted.",{type:"success"}),await G()}catch(t){E.toast("Failed to delete: "+t.message,{type:"error"})}},$e=()=>{n.bodyRev.value=n.bodyRev.peek()+1},je=()=>{a.cssOpened.peek()||(a.css.value=De,a.cssOpened.value=!0,At(F))},ye=e=>{for(const t of["urlPath","title","description","layout","status","sortOrder","category","visibility","project","theme","seoTitle","seoDesc","dconfig"])a[t].value=e[t]==null?"":String(e[t]);for(const t of["showInNav","sidebar","breadcrumbs","bundled"])a[t].value=!!e[t];a.tags.value=Array.isArray(e.tags)?e.tags:[],W?.setValue(a.tags.peek()),Array.isArray(e.adminTags)&&(a.adminTags.value=e.adminTags),Y?.setValue(a.adminTags.peek()),x.unfoldAll(),z=!0,T.value=e.body==null?"":String(e.body),z=!1,x.refresh(),e.css!=null&&(je(),a.css.value=String(e.css)),$e()};n.base.value=j(B());const X=Xt(r||"");m=()=>{if(i())try{S.set(X,Qt(B(),n.base.peek()))}catch{}};const ke=()=>{try{S.remove?S.remove(X):S.set(X,null)}catch{}},Ve=e=>{i()&&(m(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Ve),s(()=>window.removeEventListener("beforeunload",Ve));const qe=async()=>{if(n.saving.peek())return;const e=B(),t=Te(e);if(t.length){E.toast(t[0].text,{type:"warning"}),b.querySelectorAll("#editor-meta-tabs .tab-item")[ba.indexOf(t[0].field==="dconfig"?"dconfig":"details")]?.click();return}const u=(e.dconfig||"").trim(),y=U(u?JSON.parse(u):null),A=a.cssOpened.peek();n.saving.value=!0;try{if(d){const I={frontmatter:y,body:e.body};e.urlPath!==r&&(I.newUrlPath=e.urlPath),await v.pages.update(r,I)}else await v.pages.create({urlPath:e.urlPath,frontmatter:y,body:e.body});if(A&&await v.settings.saveCustomCss(a.css.peek()),E.toast(A?"Page and CSS saved.":"Page saved.",{type:"success"}),!h())return;ke(),d&&sa({kind:"page",id:e.urlPath,label:a.title.peek().trim()||"Untitled"}),Ae=JSON.stringify(a.adminTags.peek()),n.base.value=j(B()),d?e.urlPath!==r&&R.navigate(`/pages/edit${e.urlPath}`):R.navigate("/pages")}catch(I){E.toast(`Save failed: ${I.message||"Unknown error"}`,{type:"error"})}finally{n.saving.value=!1}},He=b.querySelector(".dm-plugin-body")||b,wt={...a,...n,dirty:ut,problem:mt,anySelected:ft,allSelected:vt,someSelected:bt,isEdit:d,savedPath:r||""},Et={save:()=>qe(),discard:async()=>{!i()||!await E.confirm("Discard the changes you have not saved?")||(ye(JSON.parse(n.base.peek())),ke())},showTab:e=>{n.tab.value=e,e==="live"&&ce&&ue(),e==="history"&&d&&!ve&&G()},setMode:e=>{n.cssMode.peek()&&(n.cssMode.value=!1),n.viewMode.value=e,se("viewMode",e),e!=="write"&&J()},toggleFullscreen:()=>{n.fullscreen.value=!n.fullscreen.peek(),se("fullscreen",n.fullscreen.peek())},toggleCss:()=>{n.cssMode.peek()?(n.cssMode.value=!1,n.viewMode.value=nt.includes(q().viewMode)?q().viewMode:"split",x.refresh()):(je(),n.cssMode.value=!0,n.viewMode.value="split")},formatMarkdown:()=>{const e=T.selectionStart,t=va($());x.unfoldAll(),z=!0,T.value=t,z=!1,T.selectionStart=T.selectionEnd=Math.min(e,t.length),T.dispatchEvent(new Event("input")),x.refresh(),E.toast("Markdown formatted.",{type:"success"})},toggleFold:()=>{n.folded.peek()?x.unfoldAll():x.foldAll(),n.folded.value=!n.folded.peek()},setWidth:e=>{n.liveWidth.value=e;const t=w("#live-preview-stage");t&&(t.scrollTop=0)},refreshLive:()=>ue(),share:async()=>{if(!d){E.toast("Save the page first - a share link needs a page to point at.",{type:"warning"});return}await ua(r)},toggleAll:e=>{const t=!!e?.target?.checked;n.versions.peek().forEach(u=>{u.checked.value=t})},deleteSelected:async()=>{const e=n.versions.peek().filter(u=>u.checked.peek()).map(u=>u.filename),t=e.length;if(!(!t||!await E.confirm(`Delete ${t} version${t>1?"s":""}? This cannot be undone.`)))try{await v.versions.bulkDelete(r,e),E.toast(`${t} version${t>1?"s":""} deleted.`,{type:"success"}),await G()}catch(u){E.toast("Delete failed: "+u.message,{type:"error"})}},saveNamed:async()=>{await ma(r)&&h()&&(n.tab.peek()==="history"?await G():ve=!1)},closeCompare:()=>{n.compare.value=null,be=null},restoreCompared:()=>ze(be)};fe.querySelectorAll("[data-tip]").forEach(e=>E.tooltip(e,{content:e.getAttribute("data-tip"),position:"top"}));const Mt=M.applyBindings(wt,He,{methods:Et});s(()=>Mt.dispose());const Ke=aa(w("#page-hop"),{current:{kind:"page",id:r||"",label:f?.title||r||""},pages:O,blocks:H});s(()=>Ke.dispose()),E.tabs(w("#editor-meta-tabs"));let we=null;s(()=>clearTimeout(we)),T.addEventListener("input",()=>{x.refresh(),N.scrollTop=T.scrollTop,!z&&(clearTimeout(we),we=setTimeout($e,120),J(),Ne())}),T.addEventListener("scroll",()=>{N.scrollTop=T.scrollTop}),F.addEventListener("scroll",()=>{N.scrollTop=F.scrollTop});let Ue=!0;s(M.effect(()=>{for(const e of Object.keys(a))e!=="css"&&e!=="cssOpened"&&a[e].value;if(n.bodyRev.value,Ue){Ue=!1,J();return}J(),Ne()}));const St=oe.map(e=>e.name),Tt=oe.filter(e=>e.shortTag).map(e=>e.name);s(M.effect(()=>{n.bodyRev.value,re=ia($(),St,Tt),Ke.setRelated(re.map(e=>({kind:"block",id:e,label:e}))),ie()}));let Ee=null;if(s(()=>clearTimeout(Ee)),s(M.effect(()=>{const e=a.css.value;n.cssMode.value&&(ha(N,e,F),clearTimeout(Ee),Ee=setTimeout(Oe,400))})),d){const e=w(".pe-version-list"),t=E.contextMenu(e,{cursor:"glyph",match:"tr[data-file]",inherit:!1,density:"compact",items:u=>{const y=ge(u.dataset.file);return y?[{header:tt(y)},{label:"Compare with the editor",icon:"columns",action:()=>he(y)},{label:"Restore this version\u2026",icon:"rotate-ccw",action:()=>ze(y.filename)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>kt(y)}]:[]}});s(()=>t?.destroy?.()),e.addEventListener("click",u=>{const y=u.target.closest("tr[data-file]");if(!y||u.target.matches("input"))return;const A=u.target.closest(".pe-kebab");if(A){Ye(A);return}const I=ge(y.dataset.file);I&&he(I)}),e.addEventListener("keydown",u=>{const y=u.target.closest("tr[data-file]");if(y){if(u.key==="F10"&&u.shiftKey)u.preventDefault(),Ye(y);else if(u.key==="Enter"&&u.target===y){const A=ge(y.dataset.file);A&&he(A)}}})}ya(w("#editor-body"),w("#editor-divider"),s),Nt({image:e=>(n.cssMode.value=!1,ka(e)),scribe:e=>Ft(e),card:e=>xt.open(e,{onMediaBrowse:()=>v.media.list().catch(()=>[])}),timeline:e=>Pt.open(e,{onPreview:async t=>{try{return await v.pages.preview(t)}catch{return null}},onListCollections:()=>v.collections.list().catch(()=>[]),onGetCollection:t=>v.collections.get(t).catch(()=>null)})}),me.onEffects(e=>Ot.open(e)),me.onHelp(()=>Ge()),me.onTutorial(e=>zt(e));const _e=e=>{(e.ctrlKey||e.metaKey)&&!e.shiftKey&&!e.altKey&&e.key.toLowerCase()==="s"&&h()&&(e.preventDefault(),qe())};document.addEventListener("keydown",_e),s(()=>document.removeEventListener("keydown",_e));const Z=b.querySelector(".dm-plugin-banner-name");s(M.effect(()=>{const e=Gt(d,a.title.value,r);if(!h()||(document.title=`${e} - Domma CMS`,!Z))return;const t=Z.lastChild;t&&t.nodeType===3&&Z.querySelector(".dm-plugin-banner-sep")?t.textContent=e:Z.textContent=`Pages \u203A ${e}`})),V(o,{id:"page-editor-back",icon:"arrow-left",title:"Back to Pages",onClick:()=>R.navigate("/pages")}),V(o,{id:"page-editor-help",icon:"help-circle",title:"Editor reference",onClick:()=>Ge()}),d&&V(o,{id:"page-editor-view",icon:"external-link",title:"View on the site",onClick:()=>window.open(a.urlPath.peek()||r,"_blank","noopener")});const Me=V(o,{id:"page-editor-clone",icon:"copy",title:"Clone page",onClick:()=>{if(!d||n.saving.peek()||!h())return;const e=B(),t=Te({...e,urlPath:r});if(t.length){E.toast(t[0].text,{type:"warning"});return}const u=jt({page:f,frontmatter:{...U(ne()),adminTags:a.adminTags.peek()},body:e.body,source:r,pages:O,onCloned:y=>{h()&&(C.cloneTarget=`/pages/edit${y}`,R.navigate(C.cloneTarget))}});s(()=>u.close())}});Me&&(Me.disabled=!d,d||(Me.title="Save the page before cloning it")),V(o,{id:"page-editor-settings",icon:"settings",title:"Editor settings - font, size, spacing",onClick:()=>{Pe=pa(xe)}}),s(We(He,{position:"bottom"})),Domma.icons.scan();let Je=null;try{Je=S.get(X)}catch{}const P=ea(Je),Ct=oa(C.item);if(P){if(ke(),j(P.state)!==n.base.peek()&&Ct&&!Qe(P,n.base.peek()))ye(P.state),E.toast("Your unsaved changes to this page are back.",{type:"info"});else if(j(P.state)!==n.base.peek()){const e=P.at?` (${te(P.at)})`:"",t=Qe(P,n.base.peek())?" The saved page has changed since - restoring puts your version back over it (the saved one stays in History).":"";await E.confirm(`You left unsaved changes to this page${e}. Restore them?${t}`)&&h()&&ye(P.state)}}}};function ha(o,l,c){o.replaceChildren(...String(l).split(`
94
+ `).map((s,i)=>{const m=document.createElement("span");m.className="editor-line-number-row";const p=document.createElement("span");p.className="fold-toggle fold-toggle--empty";const r=document.createElement("span");return r.className="editor-line-num",r.textContent=String(i+1),m.append(p,r),m})),o.scrollTop=c.scrollTop}function ya(o,l,c){if(!o||!l)return;const s=15,i=85,m=b=>Math.min(i,Math.max(s,b));let p=m(parseFloat(q().splitRatio)||50);const r=b=>{o.style.setProperty("--dm-editor-split",p+"%"),l.setAttribute("aria-valuenow",String(Math.round(p))),b&&se("splitRatio",p)};r(!1);const d=b=>{const h=o.getBoundingClientRect();h.width&&(p=m((b.clientX-h.left)/h.width*100)),r(!1)},C=()=>{document.removeEventListener("pointermove",d),document.removeEventListener("pointerup",C),o.classList.remove("is-resizing"),l.classList.remove("is-dragging"),r(!0)};c(C),l.addEventListener("pointerdown",b=>{window.matchMedia("(max-width: 768px)").matches||(b.preventDefault(),o.classList.add("is-resizing"),l.classList.add("is-dragging"),document.addEventListener("pointermove",d),document.addEventListener("pointerup",C))}),l.addEventListener("dblclick",()=>{p=50,r(!0)}),l.addEventListener("keydown",b=>{const h=b.shiftKey?10:2;let g=p;if(b.key==="ArrowLeft")g=p-h;else if(b.key==="ArrowRight")g=p+h;else if(b.key==="Home")g=s;else if(b.key==="End")g=i;else if(b.key==="Enter"||b.key===" ")g=50;else return;b.preventDefault(),p=m(g),r(!0)})}async function ka(o){const l=o.selectionStart,c=o.selectionEnd,s=await $t({title:"Upload or insert an image"});if(!s||!o.isConnected)return;let i=s.split("/").pop().split("?")[0];try{i=decodeURIComponent(i)}catch{}const m=i.replace(/[\\[\]]/g,"\\$&"),p=s.replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/ /g,"%20");o.setSelectionRange(l,c),Dt(o,`![${m}](${p})`),o.dispatchEvent(new Event("input",{bubbles:!0}))}function wa(o,l,c){const s=Object.entries(l||{});return o?(o.replaceChildren(...s.map(([i,m])=>new Option(m?.label||i,i))),s.some(([i])=>i===c)?c:s[0]?.[0]||c):c}async function Ea(o,l){if(!o)return;let c=[];try{const d=await v.get("/collections/roles/entries?limit=100");c=Array.isArray(d)?d:d?.entries||[]}catch{}const s=c.map(d=>({name:d.data?.name,label:d.data?.label||d.data?.name,level:d.data?.level??99})).filter(d=>d.name&&d.level!==0).sort((d,C)=>C.level-d.level),i=(d,C)=>Object.assign(document.createElement("option"),{value:d,textContent:C}),m=()=>{typeof l=="string"&&l&&![...o.options].some(d=>d.value===l)&&o.append(i(l,`${l} (custom)`))};if(!s.length)return m();const p=document.createElement("optgroup");p.label="Role and above",s.forEach(d=>p.append(i(d.name,`${d.label} and above`)));const r=document.createElement("optgroup");r.label="Role only",s.forEach(d=>r.append(i(`=${d.name}`,`${d.label} only`))),o.replaceChildren(i("public","Public - everyone"),p,r,i("private","Private - super-admin only")),m()}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.8.17",
3
+ "version": "1.8.18",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -7,13 +7,14 @@
7
7
  * PUT /api/pages/* - update page
8
8
  * DELETE /api/pages/* - delete page
9
9
  * POST /api/pages/preview - render markdown body to HTML (no page shell)
10
- * POST /api/pages/preview/full - render UNSAVED frontmatter+body as a complete page
10
+ * POST /api/pages/preview/full - render UNSAVED frontmatter+body as a complete page;
11
+ * `blocks` {name: {content, css}} = unsaved block edits to use
11
12
  * GET /api/pages/preview-links - list share links (optionally ?urlPath=)
12
13
  * POST /api/pages/preview-links - mint a share link for one page
13
14
  * DELETE /api/pages/preview-links/:id - revoke a share link
14
15
  */
15
16
  import {buildPreviewPage, createPage, deletePage, getPage, listPages, renamePage, updatePage} from '../../services/content.js';
16
- import {parseMarkdown} from '../../services/markdown.js';
17
+ import {parseMarkdown, readBlockOverrides, withBlockOverrides} from '../../services/markdown.js';
17
18
  import {renderPage} from '../../services/renderer.js';
18
19
  import {
19
20
  createPreviewLink,
@@ -97,11 +98,18 @@ export async function pagesRoutes(fastify) {
97
98
  return reply.status(400).send({error: 'frontmatter must be an object'});
98
99
  }
99
100
 
100
- const page = await buildPreviewPage(urlPath, frontmatter, body, {user: request.user || null});
101
- const html = await renderPage(page, {
102
- baseUrl: `${request.protocol}://${request.host}`,
103
- user: request.user || null,
104
- noTrack: true
101
+ // Unsaved block edits (the block editor's "show on this page", a page editor with
102
+ // block changes held): this render only. The output is sanitised as any page is.
103
+ const overrides = readBlockOverrides(request.body?.blocks);
104
+ if (overrides.error) return reply.status(400).send({error: overrides.error});
105
+
106
+ const html = await withBlockOverrides(overrides.blocks, async () => {
107
+ const page = await buildPreviewPage(urlPath, frontmatter, body, {user: request.user || null});
108
+ return renderPage(page, {
109
+ baseUrl: `${request.protocol}://${request.host}`,
110
+ user: request.user || null,
111
+ noTrack: true
112
+ });
105
113
  });
106
114
  return {html};
107
115
  });
@@ -8,6 +8,7 @@ import matter from 'gray-matter';
8
8
  import {marked} from 'marked';
9
9
  import sanitizeHtml from 'sanitize-html';
10
10
  import {readdir, readFile} from 'fs/promises';
11
+ import {AsyncLocalStorage} from 'node:async_hooks';
11
12
  import path from 'path';
12
13
  import {fileURLToPath} from 'url';
13
14
  import {applyTransforms, getSanitizeExtensions, getShortcodeProcessors} from './hooks.js';
@@ -141,6 +142,49 @@ function buildShortcodeFilter(attrs) {
141
142
  return filter;
142
143
  }
143
144
 
145
+ /**
146
+ * Unsaved block edits for ONE render: the editor previews ask for a page as it
147
+ * would look with the block they are editing (POST /api/pages/preview/full,
148
+ * `blocks`). Held in an AsyncLocalStorage, so every place that loads a block -
149
+ * the short tag, [block], a display's block="…", the footer blocks - sees the
150
+ * same edit without each call site being handed it, and no other request ever
151
+ * does. Never used for a cached or public render.
152
+ */
153
+ const blockOverrides = new AsyncLocalStorage();
154
+
155
+ /** The longest unsaved template or stylesheet a preview may send (characters), and how many blocks. The request as a whole is still held to Fastify's body limit. */
156
+ export const BLOCK_OVERRIDE_LIMITS = {count: 20, chars: 200_000};
157
+
158
+ /**
159
+ * Check a preview's `blocks` map: {name: {content, css}} with plain block names
160
+ * and strings, within the limits. Returns the clean map, or a message.
161
+ *
162
+ * @param {unknown} raw
163
+ * @returns {{blocks: Map<string, {content: string, css: string}>}|{error: string}}
164
+ */
165
+ export function readBlockOverrides(raw) {
166
+ if (raw == null) return {blocks: new Map()};
167
+ if (typeof raw !== 'object' || Array.isArray(raw)) return {error: 'blocks must be an object'};
168
+ const entries = Object.entries(raw);
169
+ if (entries.length > BLOCK_OVERRIDE_LIMITS.count) return {error: `at most ${BLOCK_OVERRIDE_LIMITS.count} blocks`};
170
+ const blocks = new Map();
171
+ for (const [name, v] of entries) {
172
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return {error: `"${name.slice(0, 40)}" is not a block name`};
173
+ if (!v || typeof v !== 'object' || typeof v.content !== 'string' || (v.css != null && typeof v.css !== 'string')) {
174
+ return {error: `blocks.${name} needs content (and css) as text`};
175
+ }
176
+ const css = v.css ?? '';
177
+ if (v.content.length > BLOCK_OVERRIDE_LIMITS.chars || css.length > BLOCK_OVERRIDE_LIMITS.chars) return {error: `blocks.${name} is too large`};
178
+ blocks.set(name, {content: v.content, css});
179
+ }
180
+ return {blocks};
181
+ }
182
+
183
+ /** Run `fn` with these unsaved blocks in place of the saved ones (an empty map = as saved). */
184
+ export function withBlockOverrides(blocks, fn) {
185
+ return blocks?.size ? blockOverrides.run(blocks, fn) : fn();
186
+ }
187
+
144
188
  /**
145
189
  * Load a block template by name from content/blocks/{name}.html.
146
190
  * Throws if the file does not exist.
@@ -164,7 +208,8 @@ function buildShortcodeFilter(attrs) {
164
208
  */
165
209
  async function loadBlockTemplate(blockName) {
166
210
  const safe = path.basename(blockName).replace(/[^a-z0-9-]/g, '');
167
- const raw = await readFile(path.join(BLOCKS_DIR, `${safe}.html`), 'utf8');
211
+ const unsaved = blockOverrides.getStore()?.get(safe);
212
+ const raw = unsaved ? unsaved.content : await readFile(path.join(BLOCKS_DIR, `${safe}.html`), 'utf8');
168
213
  return stripTemplateComments(raw);
169
214
  }
170
215
 
@@ -191,6 +236,8 @@ export function stripTemplateComments(tpl) {
191
236
  */
192
237
  async function loadBlockCss(blockName) {
193
238
  const safe = path.basename(blockName).replace(/[^a-z0-9-]/g, '');
239
+ const unsaved = blockOverrides.getStore()?.get(safe);
240
+ if (unsaved) return unsaved.css;
194
241
  try {
195
242
  return await readFile(path.join(BLOCKS_DIR, `${safe}.css`), 'utf8');
196
243
  } catch {