domma-cms 0.72.0 → 0.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +7 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/admin-theme.js +1 -0
- package/admin/js/templates/theme.html +0 -3
- package/admin/js/views/theme.js +1 -1
- package/package.json +1 -1
- package/public/js/site.js +1 -1
- package/public/js/theme-switcher.js +1 -1
- package/server/routes/api/theme.js +22 -3
- package/server/services/plugins.js +4 -0
- package/server/services/renderer.js +24 -10
- package/server/services/themeSettings.js +210 -10
- package/server/templates/page.html +13 -4
package/CLAUDE.md
CHANGED
|
@@ -245,6 +245,13 @@ id="dm-theme-overrides">` is injected **before** `content/custom.css` so hand-wr
|
|
|
245
245
|
both render paths in `renderer.js` go through `buildThemeView()` - wiring one and not the other is the
|
|
246
246
|
standing trap in that file. Token values that could close their own declaration are refused, not escaped.
|
|
247
247
|
|
|
248
|
+
**Custom themes** (a plugin's - the Theme Roller) come from `hooks.registerThemes(provider)` and join the
|
|
249
|
+
built-in ones everywhere except the public switcher. Domma's engine strips every `dm-theme-*` class it did
|
|
250
|
+
not add, so a custom theme is `dm-theme-<base> dm-custom-<id>` plus one generated rule of the changed
|
|
251
|
+
tokens (`buildCustomThemeStyleTag`, both render paths); `resolveThemeClasses()` is the one place that
|
|
252
|
+
decides, and config/theme.json's `customBases` keeps a page on the base if the plugin goes away. The
|
|
253
|
+
admin applies its own through `GET /api/theme/admin` + `admin/js/lib/admin-theme.js`.
|
|
254
|
+
|
|
248
255
|
## Multi-menu system, overlays & admin sidebar
|
|
249
256
|
|
|
250
257
|
Menus live in `config/menus/<slug>.json`, mapped to slots by `config/menu-locations.json` (built-in
|
package/admin/js/api.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const m="/api";function d(){return S.get("auth_token")}function p(){return S.get("auth_refresh_token")}function y(e){S.set("auth_token",e)}function u(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function f(){const e=p();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 u(),R.navigate("/login"),new Error("Token refresh failed");const{token:n}=await o.json();return y(n),n}async function t(e,o={}){let n=d();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&&p())try{n=await f(),s=await fetch(`${m}${e}`,{...o,headers:r(n)})}catch{return}if(!s.ok){const i=await s.json().catch(()=>({error:"Request failed"})),a=new Error(i.error||i.message||`HTTP ${s.status}`);throw Array.isArray(i.validation)&&(a.validation=i.validation),a.status=s.status,a}return s.status===204?null:s.json()}async function g(e,o){const n=d(),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)}),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})}),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||[])},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"}),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"}),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"})},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=>g("/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"})},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"}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"}),async exportBundle(e,{includeData:o=!1}={}){const n=d(),r=o?"?includeData=1":"",s=await fetch(`${m}/plugins/${encodeURIComponent(e)}/export${r}`,{headers:n?{Authorization:`Bearer ${n}`}:{}});if(!s.ok){const T=await s.json().catch(()=>({}));throw new Error(T.error||`Export failed (${s.status})`)}const i=s.headers.get("Content-Disposition")||"",a=/filename="([^"]+)"/.exec(i)?.[1],c=await s.blob(),h=URL.createObjectURL(c),l=document.createElement("a");l.href=h,l.download=a||`${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=d(),a=await fetch(`${m}/plugins/install-upload`,{method:"POST",headers:i?{Authorization:`Bearer ${i}`}:{},body:s}),c=await a.json().catch(()=>({}));if(a.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(!a.ok)throw new Error(c.error||`Install failed (${a.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=d(),n=await fetch(`${m}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const a=await n.json().catch(()=>({}));throw new Error(a.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",...d()?{Authorization:`Bearer ${d()}`}:{}},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=d(),n=await fetch(`${m}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const a=await n.json().catch(()=>({}));throw new Error(a.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",...d()?{Authorization:`Bearer ${d()}`}:{}},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!!d()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:n}){e&&y(e),o&&S.set("auth_refresh_token",o),n&&S.set("auth_user",n)}export function logout(){const e=p();e&&fetch(`${m}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),u(),R.navigate("/login")}export{t as apiRequest,f as refreshAccessToken,d as getToken,u as clearAuth};
|
|
1
|
+
const m="/api";function a(){return S.get("auth_token")}function p(){return S.get("auth_refresh_token")}function y(e){S.set("auth_token",e)}function u(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function f(){const e=p();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 u(),R.navigate("/login"),new Error("Token refresh failed");const{token:n}=await o.json();return y(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&&p())try{n=await f(),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}return s.status===204?null:s.json()}async function g(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)}),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})}),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||[])},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:()=>t("/theme/admin",{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"}),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"})},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=>g("/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"})},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"}),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 T=await s.json().catch(()=>({}));throw new Error(T.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&&y(e),o&&S.set("auth_refresh_token",o),n&&S.set("auth_user",n)}export function logout(){const e=p();e&&fetch(`${m}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),u(),R.navigate("/login")}export{t as apiRequest,f as refreshAccessToken,a as getToken,u as clearAuth};
|
package/admin/js/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{views as B}from"./views/index.js";import{withPluginChrome as I}from"./lib/plugin-chrome.js";import{api as p,getUser as U,isAuthenticated as d,logout as O}from"./api.js";import{installHttpInterceptor as V}from"./http-interceptor.js";import{installResizableSlideovers as F}from"./lib/slideover-resizable.js";import{installActivityThrobber as H}from"./lib/activity-throbber.js";import{isPathAllowed as q,redirectFor as K}from"./lib/admin-scope.js";$(()=>{V(),H(),F(),(async()=>{try{const t=d()?await p.theme.get():null;Domma.theme.init({theme:t?.adminTheme||"charcoal-dark",persist:!0})}catch{Domma.theme.init({theme:"charcoal-dark",persist:!0})}})();let l=null,r="/",u=!1;function v(t){const e=f[t]||K(l,t);return e||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"?f[r]||r:null)}const f={};R.use(async(t,e,o)=>{if(t.path==="/login"||t.path==="/reset-password")return o();if(!d()){R.navigate("/login");return}const i=v(t.path);if(i){R.navigate(i);return}o()});async function C(){try{const t=await p.get("/auth/permissions");l=t.adminScope||null,r=t.adminHome||"/";const e=t.permissions||[];return u=q(l,"/settings")&&e.some(o=>o==="settings"||o.startsWith("settings.")),e}catch{return l=null,r="/",u=!1,[]}}document.addEventListener("dm:admin-home-changed",()=>{C()});async function y(t){const{renderAdminSidebar:e}=await import("./lib/sidebar-renderer.js");await e({mount:"#admin-sidebar",permissions:t,adminScope:l})}M.subscribe("router:afterChange",({to:t})=>{(t.path==="/login"||t.path==="/reset-password")&&(m&&(clearInterval(m),m=null),l=null,r="/",u=!1)});const T=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Plugin - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projectEditor",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projectEditor",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:e})=>{if(!(t.path==="/login"||t.path==="/reset-password")){if($("#admin-sidebar").show(),$("#admin-topbar").show(),e?.path==="/login"||e?.path==="/reset-password"){$("#topbar-user-name").remove(),await A();const o=await C(),i=v(t.path);i&&R.navigate(i),y(o);try{const a=await p.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const w=h(s.data?.title||""),P=h((s.data?.body||"").slice(0,120));E.toast(`${w} - ${P}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}k()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const x=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function L(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const e=t?String(t.path).split("?")[0]:"";if(!t||!L(e))return;const i=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!i||!/\/new$/.test(e))return;let a=0;const n=60,s=1500,w=()=>{a+=n;for(const P of x){const g=document.querySelector(P);if(!g)continue;if(Array.from(g.options||[]).find(_=>_.value===i)){g.value=i,g.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(w,n)};setTimeout(w,n)});const j="cms_card_states",D=S.get(j)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&D[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const e=$(this).closest(".card");e.toggleClass("card-collapsed");const o=$(this).find("h2, h3").first().text().trim();o&&(D[o]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(j,D))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash==="#/login"||window.location.hash.startsWith("#/reset-password"))return;const e=document.querySelector("#view-container .view-header button.btn-primary");e&&(t.preventDefault(),e.click())});const c={...B},W={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},analytics:{displayName:"Analytics",icon:"chart-bar"},contacts:{displayName:"Contacts",icon:"users"}};for(const[t,e]of Object.entries(W))c[t]&&(c[t]=I(c[t],{plugin:t,licence:"Built in",...e}));const b=[...T];async function A(){if(d())try{const t=await p.plugins.adminConfig();t.routes?.length&&b.push(...t.routes);for(const[e,o]of Object.entries(t.views||{}))try{const i=await import(`/plugins/${o.entry}`);c[e]=I(i[o.exportName],(t.meta||{})[e])}catch{}for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(f[`/${e}`]=o);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const i=document.createElement("link");i.id=e,i.rel="stylesheet",i.href=o,document.head.appendChild(i)}}catch{}}function k(){const t=U();if(!t||$("#topbar-user-name").length)return;const o={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role;$("#topbar-user").html(`
|
|
1
|
+
import{views as B}from"./views/index.js";import{withPluginChrome as I}from"./lib/plugin-chrome.js";import{api as c,getUser as U,isAuthenticated as d,logout as O}from"./api.js";import{installHttpInterceptor as V}from"./http-interceptor.js";import{installResizableSlideovers as F}from"./lib/slideover-resizable.js";import{installActivityThrobber as H}from"./lib/activity-throbber.js";import{isPathAllowed as q,redirectFor as K}from"./lib/admin-scope.js";$(()=>{V(),H(),F(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:e}=await import("./lib/admin-theme.js");d()?await e(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let l=null,r="/",u=!1;function v(t){const e=f[t]||K(l,t);return e||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"?f[r]||r:null)}const f={};R.use(async(t,e,o)=>{if(t.path==="/login"||t.path==="/reset-password")return o();if(!d()){R.navigate("/login");return}const i=v(t.path);if(i){R.navigate(i);return}o()});async function C(){try{const t=await c.get("/auth/permissions");l=t.adminScope||null,r=t.adminHome||"/";const e=t.permissions||[];return u=q(l,"/settings")&&e.some(o=>o==="settings"||o.startsWith("settings.")),e}catch{return l=null,r="/",u=!1,[]}}document.addEventListener("dm:admin-home-changed",()=>{C()});async function y(t){const{renderAdminSidebar:e}=await import("./lib/sidebar-renderer.js");await e({mount:"#admin-sidebar",permissions:t,adminScope:l})}M.subscribe("router:afterChange",({to:t})=>{(t.path==="/login"||t.path==="/reset-password")&&(p&&(clearInterval(p),p=null),l=null,r="/",u=!1)});const T=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Plugin - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projectEditor",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projectEditor",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:e})=>{if(!(t.path==="/login"||t.path==="/reset-password")){if($("#admin-sidebar").show(),$("#admin-topbar").show(),e?.path==="/login"||e?.path==="/reset-password"){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(c)),await A();const o=await C(),i=v(t.path);i&&R.navigate(i),y(o);try{const a=await c.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const w=h(s.data?.title||""),P=h((s.data?.body||"").slice(0,120));E.toast(`${w} - ${P}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}k()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const x=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function L(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const e=t?String(t.path).split("?")[0]:"";if(!t||!L(e))return;const i=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!i||!/\/new$/.test(e))return;let a=0;const n=60,s=1500,w=()=>{a+=n;for(const P of x){const g=document.querySelector(P);if(!g)continue;if(Array.from(g.options||[]).find(_=>_.value===i)){g.value=i,g.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(w,n)};setTimeout(w,n)});const j="cms_card_states",D=S.get(j)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&D[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const e=$(this).closest(".card");e.toggleClass("card-collapsed");const o=$(this).find("h2, h3").first().text().trim();o&&(D[o]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(j,D))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash==="#/login"||window.location.hash.startsWith("#/reset-password"))return;const e=document.querySelector("#view-container .view-header button.btn-primary");e&&(t.preventDefault(),e.click())});const m={...B},W={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},analytics:{displayName:"Analytics",icon:"chart-bar"},contacts:{displayName:"Contacts",icon:"users"}};for(const[t,e]of Object.entries(W))m[t]&&(m[t]=I(m[t],{plugin:t,licence:"Built in",...e}));const b=[...T];async function A(){if(d())try{const t=await c.plugins.adminConfig();t.routes?.length&&b.push(...t.routes);for(const[e,o]of Object.entries(t.views||{}))try{const i=await import(`/plugins/${o.entry}`);m[e]=I(i[o.exportName],(t.meta||{})[e])}catch{}for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(f[`/${e}`]=o);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const i=document.createElement("link");i.id=e,i.rel="stylesheet",i.href=o,document.head.appendChild(i)}}catch{}}function k(){const t=U();if(!t||$("#topbar-user-name").length)return;const o={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role;$("#topbar-user").html(`
|
|
2
2
|
<span id="topbar-user-name" class="topbar-user-name">${h(t.name)}</span>
|
|
3
3
|
<span class="topbar-role-badge topbar-role-badge--${h(t.role)}">${o}</span>
|
|
4
4
|
`),$("#topbar-actions").html(`
|
|
@@ -18,4 +18,4 @@ import{views as B}from"./views/index.js";import{withPluginChrome as I}from"./lib
|
|
|
18
18
|
<span data-icon="log-out"></span>
|
|
19
19
|
<span>Sign out</span>
|
|
20
20
|
</a>
|
|
21
|
-
`);const i=$("#topbar-bell").get(0);i&&(i.hidden=!0);const a=$("#topbar-settings").get(0);a&&(a.hidden=!u),$("#topbar-logout-btn").on("click",n=>{n.preventDefault(),O()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),N(),
|
|
21
|
+
`);const i=$("#topbar-bell").get(0);i&&(i.hidden=!0);const a=$("#topbar-settings").get(0);a&&(a.hidden=!u),$("#topbar-logout-btn").on("click",n=>{n.preventDefault(),O()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),N(),p||(p=setInterval(N,6e4))}function h(t){return String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}let p=null;async function N(){if(d())try{const{count:t}=await c.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const e=$("#topbar-bell-badge");t>0?e.text(t>99?"99+":String(t)).removeAttr("hidden"):e.attr("hidden","").text("")}catch{}}(async()=>{if(!d()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash="#/login";else if(await A(),U()){const a=await C(),n=(window.location.hash||"#/").slice(1)||"/",s=v(n);s&&(window.location.hash=`#${s}`),await y(a),k()}R.init({container:"#view-container",routes:b,views:m,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}});const t=R._extractParams.bind(R);R._extractParams=function(i,a){const n=a.indexOf("?");if(n!==-1&&(a=a.slice(0,n)),i.endsWith("/*")){const s=i.slice(0,-2);return a.startsWith(s+"/")?{}:null}return t(i,a)};const e=(window.location.hash||"#/").slice(1)||"/";b.filter(i=>i.path.endsWith("/*")).some(i=>e.startsWith(i.path.slice(0,-2)+"/"))&&R._handleRouteChange()})()});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const a="dm-custom-themes";export function applyAdminTheme(e,{init:s=!1}={}){const o=e?.dommaTheme||"charcoal-dark";s?Domma.theme.init({theme:o,persist:!0}):Domma.theme.set(o);let t=document.getElementById(a);e?.css?(t||(t=document.createElement("style"),t.id=a,document.head.appendChild(t)),t.textContent=e.css):t&&t.remove();const m=document.body;for(const c of[...m.classList])c.startsWith("dm-custom-")&&m.classList.remove(c);e?.customClass&&m.classList.add(e.customClass)}export async function refreshAdminTheme(e,s={}){try{applyAdminTheme(await e.theme.admin(),s)}catch{applyAdminTheme({dommaTheme:"charcoal-dark"},s)}}
|
|
@@ -44,9 +44,6 @@
|
|
|
44
44
|
<input id="theme-auto-enabled" type="checkbox" class="form-check">
|
|
45
45
|
Switch automatically between a day and a night theme
|
|
46
46
|
</label>
|
|
47
|
-
<p id="theme-auto-roller-hint" class="form-hint text-muted"
|
|
48
|
-
style="display:none;margin-top:.35rem;font-size:.8rem;">
|
|
49
|
-
Custom themes (Theme Roller) do not support auto-switching.</p>
|
|
50
47
|
</div>
|
|
51
48
|
</div>
|
|
52
49
|
<div id="theme-auto-fields" class="row mt-3" style="display:none;">
|
package/admin/js/views/theme.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as x}from"../api.js";import{populateThemeSelect as T}from"../lib/themes.js";const ce=[{value:"#site-navbar",label:"Navigation bar"},{value:"#site-footer",label:"Footer"},{value:".site-content",label:"Page content area"},{value:".page-body",label:"Page body (inside the container)"},{value:".site-sidebar",label:"Sidebar"},{value:".card",label:"Every card"},{value:".cms-form-embed",label:"Embedded forms"},{value:".cms-collection",label:"Collection displays"}];export const themeView={templateUrl:"/admin/js/templates/theme.html",async onMount(o){E.tabs(o.find("#theme-tabs").get(0));const K=E.loader(o.get(0),{type:"dots"});let d,V;try{[d,V]=await Promise.all([x.theme.get(),x.theme.catalog().catch(()=>({themes:[],tokens:[]}))])}catch(e){K.destroy(),E.toast(`Could not load theme settings: ${e.message||e}`,{type:"error"});return}K.destroy();const z=V.tokens||[],B=new Map;async function _(e){if(!e)return null;if(B.has(e))return B.get(e);const t=await x.theme.themeTokens(e).catch(()=>null),n=t&&t.tokens?t.tokens:null;return B.set(e,n),n}function ne(){const e=o.find("#theme-auto-enabled").prop("checked");return d.baseTheme||(e?S.val():f.val())||d.theme}const X=["--dm-card-bg","--dm-surface"],Y=[...X,"--dm-text","--dm-primary","--dm-background"];function H(e){const t=document.createElement("span");return t.style.cssText="display:inline-block;width:.7rem;height:.7rem;border-radius:2px;border:1px solid var(--dm-border);vertical-align:middle;margin-right:.25rem;",/^#[0-9a-f]{3,8}$/i.test(e)?t.style.background=e:t.style.background="transparent",t}async function D(e){const t=e.querySelector(".ov-theme-diff");if(!t)return;for(;t.firstChild;)t.removeChild(t.firstChild);const n=e.querySelector(".ov-theme")?.value;if(!n)return;const s=ne();if(n===s){const r=document.createElement("p");r.className="form-hint text-muted",r.style.cssText="margin:.4rem 0 0;font-size:.8rem;",r.textContent="That is already the site theme, so this changes nothing on its own. Set a token below instead.",t.appendChild(r);return}const[a,l]=await Promise.all([_(s),_(n)]);if(!a||!l)return;const c=Object.keys(l).filter(r=>String(a[r]??"").toLowerCase()!==String(l[r]).toLowerCase()),i=X.filter(r=>c.includes(r)),m=document.createElement("div");m.style.cssText="margin-top:.5rem;padding:.5rem .6rem;border-radius:var(--dm-radius-md);background:var(--dm-surface-raised);font-size:.8rem;";const h=document.createElement("p");if(h.style.cssText="margin:0 0 .35rem;",h.textContent=`Changes ${c.length} of ${Object.keys(l).length} tokens here.`,m.appendChild(h),!i.length){const r=document.createElement("p");r.style.cssText="margin:0 0 .35rem;color:var(--dm-warning);",r.textContent="Both themes use the same surface colour, so this region keeps the fill it has now. Set --dm-card-bg (a card) or --dm-surface (most other regions) below to change it.",m.appendChild(r)}const p=document.createElement("div");p.style.cssText="display:flex;flex-wrap:wrap;gap:.15rem .75rem;";const v=[...Y.filter(r=>c.includes(r)),...c.filter(r=>!Y.includes(r))];for(const r of v.slice(0,6)){const u=document.createElement("span");u.style.cssText="white-space:nowrap;color:var(--dm-text-muted);",u.appendChild(H(a[r]||""));const C=document.createElement("span");C.textContent=`${r} `,u.appendChild(C),u.appendChild(H(l[r])),p.appendChild(u)}if(v.length>6){const r=document.createElement("span");r.style.cssText="color:var(--dm-text-muted);",r.textContent=`+${v.length-6} more`,p.appendChild(r)}m.appendChild(p),t.appendChild(m)}let b=Array.isArray(d.overrides)?d.overrides.map(J):[];const f=o.find("#theme-frontend"),F=o.find("#theme-admin");T(f.get(0)),T(F.get(0),{includeAdmin:!0}),f.val(d.baseTheme||d.theme||"charcoal-dark"),F.val(d.adminTheme||"charcoal-dark"),d.baseTheme&&(f.prop("disabled",!0),o.find("#theme-auto-enabled").prop("disabled",!0),o.find("#theme-auto-roller-hint").show());const S=o.find("#theme-day"),L=o.find("#theme-night");T(S.get(0)),T(L.get(0));const y=d.autoTheme||{};S.val(y.dayTheme||"charcoal-light"),L.val(y.nightTheme||"charcoal-dark"),o.find("#theme-day-start").val(y.dayStart||"07:00"),o.find("#theme-night-start").val(y.nightStart||"19:00");const N=d.switcher||{},q=o.find("#theme-switcher-enabled"),oe=o.find("#theme-switcher-fields");q.prop("checked",N.enabled===!0),o.find("#theme-switcher-position").val(N.position||"right-center"),o.find("#theme-switcher-offset-x").val(N.offsetX??32),o.find("#theme-switcher-offset-y").val(N.offsetY??32);const U=()=>{oe.get(0).style.display=q.prop("checked")?"":"none"};U(),q.on("change",U);const W=o.find("#theme-auto-enabled"),G=o.find("#theme-auto-fields");d.baseTheme||(W.prop("checked",!!y.enabled),y.enabled&&(G.show(),f.prop("disabled",!0)),W.on("change",function(){G.toggle(this.checked),f.prop("disabled",this.checked)})),o.find("#theme-font-family").val(d.font?.family||"Roboto"),o.find("#theme-font-size").val(String(d.font?.size??16));function J(e){return{id:e.id||`ov-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,label:e.label||"",enabled:e.enabled!==!1,scope:e.scope||"",theme:e.theme||"",tokens:{...e.tokens||{}},css:e.css||""}}const g=document.getElementById("theme-overrides"),se=document.getElementById("theme-overrides-empty");function P(){for(;g.firstChild;)g.removeChild(g.firstChild);se.style.display=b.length?"none":"",b.forEach((e,t)=>g.appendChild(ae(e,t))),Domma.icons.scan("#theme-overrides")}function ae(e,t){const n=document.createElement("div");n.className="card mb-3",n.dataset.i=String(t);const s=document.createElement("div");s.className="card-header";const a=document.createElement("h3");a.className="m-0",a.style.fontSize="1rem",a.textContent=e.label||e.scope||`Override ${t+1}`,s.appendChild(a);const l=document.createElement("div");l.style.cssText="display:flex;gap:.5rem;align-items:center;";const c=document.createElement("label");c.style.cssText="display:flex;align-items:center;gap:.35rem;font-size:.85rem;cursor:pointer;";const i=document.createElement("input");i.type="checkbox",i.className="form-check ov-enabled",i.checked=e.enabled,c.appendChild(i),c.appendChild(document.createTextNode("Enabled")),l.appendChild(c);const m=document.createElement("button");m.type="button",m.className="btn btn-ghost btn-sm ov-remove",m.textContent="Remove",l.appendChild(m),s.appendChild(l),n.appendChild(s);const h=document.createElement("div");h.className="card-body";const p=document.createElement("div");p.className="row mb-3",p.appendChild(M("col-4","Name",$("ov-label",e.label,"Navbar island"))),p.appendChild(M("col-8","Target selector",le(e.scope),"The region to re-theme. Everything inside it follows.")),h.appendChild(p);const v=document.createElement("div");v.className="row mb-3";const r=document.createElement("select");r.className="form-select ov-theme",T(r,{includeDefault:!0,defaultLabel:"Keep the site theme"}),r.value=e.theme||"",v.appendChild(M("col-6","Adopt a theme here",r,"Copies every token of that theme onto this region.")),h.appendChild(v);const u=document.createElement("div");u.className="ov-theme-diff",h.appendChild(u);const C=document.createElement("div");C.className="ov-tokens mb-2",Object.entries(e.tokens||{}).forEach(([re,de])=>{C.appendChild(Q(re,de))}),h.appendChild(Z("Token overrides",C));const A=document.createElement("button");A.type="button",A.className="btn btn-ghost btn-sm ov-add-token",A.textContent="+ Add token",h.appendChild(A);const w=document.createElement("details");w.style.marginTop="1rem";const R=document.createElement("summary");R.style.cssText="cursor:pointer;font-size:.85rem;",R.textContent="Custom CSS for this override",w.appendChild(R);const k=document.createElement("textarea");k.className="form-input ov-css",k.rows=4,k.placeholder="#site-navbar .brand { letter-spacing: .04em; }",k.value=e.css||"",k.style.marginTop=".5rem",w.appendChild(k);const I=document.createElement("p");return I.className="form-hint text-muted",I.style.cssText="font-size:.8rem;margin-top:.35rem;",I.textContent="Appended verbatim after the generated rule - you own these selectors. Prefer tokens: they survive a Domma upgrade, hand-written rules fight the cascade.",w.appendChild(I),h.appendChild(w),n.appendChild(h),setTimeout(()=>D(n),0),n}function Q(e,t){const n=document.createElement("div");n.className="ov-token-row",n.style.cssText="display:flex;gap:.5rem;align-items:center;margin-bottom:.5rem;";const s=document.createElement("select");if(s.className="form-select ov-token-name",s.style.flex="1 1 auto",z.length){for(const i of z){const m=document.createElement("option");m.value=i,m.textContent=i,s.appendChild(m)}s.value=e||z[0]}else{const i=document.createElement("option");i.value=e||"",i.textContent=e||"(no tokens found)",s.appendChild(i)}const a=document.createElement("input");a.type="text",a.className="form-input ov-token-value",a.style.flex="0 0 150px",a.placeholder="#0096c7",a.value=t||"";const l=document.createElement("input");l.type="color",l.className="ov-token-swatch",l.style.cssText="flex:0 0 38px;height:34px;padding:0;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);background:none;cursor:pointer;",/^#[0-9a-f]{6}$/i.test(t||"")&&(l.value=t),l.addEventListener("input",()=>{a.value=l.value,a.dispatchEvent(new Event("input",{bubbles:!0}))});const c=document.createElement("button");return c.type="button",c.className="btn btn-ghost btn-sm ov-token-remove",c.textContent="\xD7",n.append(s,a,l,c),n}function M(e,t,n,s){const a=document.createElement("div");return a.className=e,a.appendChild(Z(t,n,s)),a}function Z(e,t,n){const s=document.createDocumentFragment(),a=document.createElement("label");if(a.className="form-label",a.textContent=e,s.appendChild(a),s.appendChild(t),n){const l=document.createElement("p");l.className="form-hint text-muted",l.style.cssText="margin-top:.35rem;font-size:.8rem;",l.textContent=n,s.appendChild(l)}return s}function $(e,t,n){const s=document.createElement("input");return s.type="text",s.className=`form-input ${e}`,s.value=t||"",s.placeholder=n||"",s}function le(e){const t=document.createElement("div"),n=$("ov-scope",e,"#site-navbar");if(n.setAttribute("list","theme-scope-options"),t.appendChild(n),!document.getElementById("theme-scope-options")){const s=document.createElement("datalist");s.id="theme-scope-options";for(const a of ce){const l=document.createElement("option");l.value=a.value,l.label=a.label,s.appendChild(l)}t.appendChild(s)}return t}function O(){const e=g.querySelectorAll(".card"),t=[];return e.forEach((n,s)=>{const a={};n.querySelectorAll(".ov-token-row").forEach(l=>{const c=l.querySelector(".ov-token-name")?.value?.trim(),i=l.querySelector(".ov-token-value")?.value?.trim();c&&i&&(a[c]=i)}),t.push({id:b[s]?.id,label:n.querySelector(".ov-label")?.value?.trim()||"",enabled:!!n.querySelector(".ov-enabled")?.checked,scope:n.querySelector(".ov-scope")?.value?.trim()||"",theme:n.querySelector(".ov-theme")?.value||"",tokens:a,css:n.querySelector(".ov-css")?.value||""})}),b=t,t}o.on("click",".ov-remove",function(){const e=Number(this.closest(".card").dataset.i);O(),b.splice(e,1),P()}),o.on("change",".ov-theme",function(){D(this.closest(".card"))}),o.on("change","#theme-frontend, #theme-auto-enabled, #theme-day",()=>{g.querySelectorAll(".card").forEach(D)}),o.on("click",".ov-add-token",function(){this.closest(".card").querySelector(".ov-tokens").appendChild(Q("",""))}),o.on("click",".ov-token-remove",function(){this.closest(".ov-token-row").remove()}),o.find("#theme-add-override").on("click",()=>{O(),b.push(J({label:"",scope:"",enabled:!0})),P()}),P();async function j(){const e=document.getElementById("theme-css-preview"),t={...ee(),overrides:O()};try{const n=await x.theme.compile(t);e.textContent=n.css?.trim()||"(no overrides - nothing is injected)",e.style.color=""}catch(n){e.textContent=n.message||String(n),e.style.color="var(--dm-danger)"}}o.find("#theme-refresh-css").on("click",j);function ee(){const e=!d.baseTheme&&o.find("#theme-auto-enabled").prop("checked"),t={enabled:e,dayTheme:S.val()||"charcoal-light",nightTheme:L.val()||"charcoal-dark",dayStart:o.find("#theme-day-start").val()||"07:00",nightStart:o.find("#theme-night-start").val()||"19:00"};return{theme:d.baseTheme?d.theme:e?t.dayTheme:f.val(),...d.baseTheme?{baseTheme:d.baseTheme}:{},adminTheme:F.val(),autoTheme:t,font:{family:o.find("#theme-font-family").val()||"Roboto",size:parseInt(o.find("#theme-font-size").val(),10)||16},switcher:{enabled:q.prop("checked"),position:o.find("#theme-switcher-position").val()||"right-center",offsetX:parseInt(o.find("#theme-switcher-offset-x").val(),10)||0,offsetY:parseInt(o.find("#theme-switcher-offset-y").val(),10)||0,themes:Array.isArray(d.switcher?.themes)?d.switcher.themes:[]}}}function te(e){const t=document.getElementById("theme-errors");for(;t.firstChild;)t.removeChild(t.firstChild);if(!e||!e.length){t.style.display="none";return}t.style.display="";for(const n of e){const s=document.createElement("div");s.textContent=n,t.appendChild(s)}t.scrollIntoView({behavior:"smooth",block:"nearest"})}o.find("#theme-save-btn").on("click",async function(){te(null);const e={...ee(),overrides:O()};this.disabled=!0;try{d=await x.theme.save(e),Domma.theme.set(e.adminTheme),E.toast("Theme saved.",{type:"success"}),await j()}catch(t){const n=t.validation||[t.message||"Could not save the theme."];te(n),E.toast("Theme not saved.",{type:"error"})}finally{this.disabled=!1}}),await j()}};
|
|
1
|
+
import{api as w}from"../api.js";import{populateThemeSelect as A}from"../lib/themes.js";const he=[{value:"#site-navbar",label:"Navigation bar"},{value:"#site-footer",label:"Footer"},{value:".site-content",label:"Page content area"},{value:".page-body",label:"Page body (inside the container)"},{value:".site-sidebar",label:"Sidebar"},{value:".card",label:"Every card"},{value:".cms-form-embed",label:"Embedded forms"},{value:".cms-collection",label:"Collection displays"}];export const themeView={templateUrl:"/admin/js/templates/theme.html",async onMount(s){E.tabs(s.find("#theme-tabs").get(0));const X=E.loader(s.get(0),{type:"dots"});let m,q;try{[m,q]=await Promise.all([w.theme.get(),w.theme.catalog().catch(()=>({themes:[],tokens:[]}))])}catch(e){X.destroy(),E.toast(`Could not load theme settings: ${e.message||e}`,{type:"error"});return}X.destroy();const D=q.tokens||[],Y=Array.isArray(q.custom)?q.custom:[];function x(e){if(!Y.length)return;const t=document.createElement("optgroup");t.label="Custom themes";for(const n of Y){const o=document.createElement("option");o.value=n.id,o.textContent=`${n.label} (on ${n.baseTheme})`,t.appendChild(o)}e.appendChild(t)}function T(e,t){const n=e.get?e.get(0):e;if(t&&![...n.options].some(o=>o.value===t)){const o=document.createElement("option");o.value=t,o.textContent=`${t} (not available)`,n.appendChild(o)}n.value=t}const M=new Map;async function H(e){if(!e)return null;if(M.has(e))return M.get(e);const t=await w.theme.themeTokens(e).catch(()=>null),n=t&&t.tokens?t.tokens:null;return M.set(e,n),n}function ae(){return(s.find("#theme-auto-enabled").prop("checked")?S.val():f.val())||m.theme}const U=["--dm-card-bg","--dm-surface"],W=[...U,"--dm-text","--dm-primary","--dm-background"];function G(e){const t=document.createElement("span");return t.style.cssText="display:inline-block;width:.7rem;height:.7rem;border-radius:2px;border:1px solid var(--dm-border);vertical-align:middle;margin-right:.25rem;",/^#[0-9a-f]{3,8}$/i.test(e)?t.style.background=e:t.style.background="transparent",t}async function j(e){const t=e.querySelector(".ov-theme-diff");if(!t)return;for(;t.firstChild;)t.removeChild(t.firstChild);const n=e.querySelector(".ov-theme")?.value;if(!n)return;const o=ae();if(n===o){const l=document.createElement("p");l.className="form-hint text-muted",l.style.cssText="margin:.4rem 0 0;font-size:.8rem;",l.textContent="That is already the site theme, so this changes nothing on its own. Set a token below instead.",t.appendChild(l);return}const[a,r]=await Promise.all([H(o),H(n)]);if(!a||!r)return;const c=Object.keys(r).filter(l=>String(a[l]??"").toLowerCase()!==String(r[l]).toLowerCase()),d=U.filter(l=>c.includes(l)),i=document.createElement("div");i.style.cssText="margin-top:.5rem;padding:.5rem .6rem;border-radius:var(--dm-radius-md);background:var(--dm-surface-raised);font-size:.8rem;";const h=document.createElement("p");if(h.style.cssText="margin:0 0 .35rem;",h.textContent=`Changes ${c.length} of ${Object.keys(r).length} tokens here.`,i.appendChild(h),!d.length){const l=document.createElement("p");l.style.cssText="margin:0 0 .35rem;color:var(--dm-warning);",l.textContent="Both themes use the same surface colour, so this region keeps the fill it has now. Set --dm-card-bg (a card) or --dm-surface (most other regions) below to change it.",i.appendChild(l)}const p=document.createElement("div");p.style.cssText="display:flex;flex-wrap:wrap;gap:.15rem .75rem;";const v=[...W.filter(l=>c.includes(l)),...c.filter(l=>!W.includes(l))];for(const l of v.slice(0,6)){const u=document.createElement("span");u.style.cssText="white-space:nowrap;color:var(--dm-text-muted);",u.appendChild(G(a[l]||""));const C=document.createElement("span");C.textContent=`${l} `,u.appendChild(C),u.appendChild(G(r[l])),p.appendChild(u)}if(v.length>6){const l=document.createElement("span");l.style.cssText="color:var(--dm-text-muted);",l.textContent=`+${v.length-6} more`,p.appendChild(l)}i.appendChild(p),t.appendChild(i)}let b=Array.isArray(m.overrides)?m.overrides.map($):[];const f=s.find("#theme-frontend"),O=s.find("#theme-admin");A(f.get(0)),A(O.get(0),{includeAdmin:!0}),x(f.get(0)),x(O.get(0)),T(f,m.theme||"charcoal-dark"),T(O,m.adminTheme||"charcoal-dark");const S=s.find("#theme-day"),I=s.find("#theme-night");A(S.get(0)),A(I.get(0)),x(S.get(0)),x(I.get(0));const g=m.autoTheme||{};T(S,g.dayTheme||"charcoal-light"),T(I,g.nightTheme||"charcoal-dark"),s.find("#theme-day-start").val(g.dayStart||"07:00"),s.find("#theme-night-start").val(g.nightStart||"19:00");const z=m.switcher||{},B=s.find("#theme-switcher-enabled"),le=s.find("#theme-switcher-fields");B.prop("checked",z.enabled===!0),s.find("#theme-switcher-position").val(z.position||"right-center"),s.find("#theme-switcher-offset-x").val(z.offsetX??32),s.find("#theme-switcher-offset-y").val(z.offsetY??32);const J=()=>{le.get(0).style.display=B.prop("checked")?"":"none"};J(),B.on("change",J);const Q=s.find("#theme-auto-enabled"),Z=s.find("#theme-auto-fields");Q.prop("checked",!!g.enabled),g.enabled&&(Z.show(),f.prop("disabled",!0)),Q.on("change",function(){Z.toggle(this.checked),f.prop("disabled",this.checked)}),s.find("#theme-font-family").val(m.font?.family||"Roboto"),s.find("#theme-font-size").val(String(m.font?.size??16));function $(e){return{id:e.id||`ov-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,label:e.label||"",enabled:e.enabled!==!1,scope:e.scope||"",theme:e.theme||"",tokens:{...e.tokens||{}},css:e.css||""}}const y=document.getElementById("theme-overrides"),re=document.getElementById("theme-overrides-empty");function R(){for(;y.firstChild;)y.removeChild(y.firstChild);re.style.display=b.length?"none":"",b.forEach((e,t)=>y.appendChild(ce(e,t))),Domma.icons.scan("#theme-overrides")}function ce(e,t){const n=document.createElement("div");n.className="card mb-3",n.dataset.i=String(t);const o=document.createElement("div");o.className="card-header";const a=document.createElement("h3");a.className="m-0",a.style.fontSize="1rem",a.textContent=e.label||e.scope||`Override ${t+1}`,o.appendChild(a);const r=document.createElement("div");r.style.cssText="display:flex;gap:.5rem;align-items:center;";const c=document.createElement("label");c.style.cssText="display:flex;align-items:center;gap:.35rem;font-size:.85rem;cursor:pointer;";const d=document.createElement("input");d.type="checkbox",d.className="form-check ov-enabled",d.checked=e.enabled,c.appendChild(d),c.appendChild(document.createTextNode("Enabled")),r.appendChild(c);const i=document.createElement("button");i.type="button",i.className="btn btn-ghost btn-sm ov-remove",i.textContent="Remove",r.appendChild(i),o.appendChild(r),n.appendChild(o);const h=document.createElement("div");h.className="card-body";const p=document.createElement("div");p.className="row mb-3",p.appendChild(V("col-4","Name",ne("ov-label",e.label,"Navbar island"))),p.appendChild(V("col-8","Target selector",de(e.scope),"The region to re-theme. Everything inside it follows.")),h.appendChild(p);const v=document.createElement("div");v.className="row mb-3";const l=document.createElement("select");l.className="form-select ov-theme",A(l,{includeDefault:!0,defaultLabel:"Keep the site theme"}),x(l),T(l,e.theme||""),v.appendChild(V("col-6","Adopt a theme here",l,"Copies every token of that theme onto this region.")),h.appendChild(v);const u=document.createElement("div");u.className="ov-theme-diff",h.appendChild(u);const C=document.createElement("div");C.className="ov-tokens mb-2",Object.entries(e.tokens||{}).forEach(([ie,me])=>{C.appendChild(ee(ie,me))}),h.appendChild(te("Token overrides",C));const L=document.createElement("button");L.type="button",L.className="btn btn-ghost btn-sm ov-add-token",L.textContent="+ Add token",h.appendChild(L);const N=document.createElement("details");N.style.marginTop="1rem";const _=document.createElement("summary");_.style.cssText="cursor:pointer;font-size:.85rem;",_.textContent="Custom CSS for this override",N.appendChild(_);const k=document.createElement("textarea");k.className="form-input ov-css",k.rows=4,k.placeholder="#site-navbar .brand { letter-spacing: .04em; }",k.value=e.css||"",k.style.marginTop=".5rem",N.appendChild(k);const P=document.createElement("p");return P.className="form-hint text-muted",P.style.cssText="font-size:.8rem;margin-top:.35rem;",P.textContent="Appended verbatim after the generated rule - you own these selectors. Prefer tokens: they survive a Domma upgrade, hand-written rules fight the cascade.",N.appendChild(P),h.appendChild(N),n.appendChild(h),setTimeout(()=>j(n),0),n}function ee(e,t){const n=document.createElement("div");n.className="ov-token-row",n.style.cssText="display:flex;gap:.5rem;align-items:center;margin-bottom:.5rem;";const o=document.createElement("select");if(o.className="form-select ov-token-name",o.style.flex="1 1 auto",D.length){for(const d of D){const i=document.createElement("option");i.value=d,i.textContent=d,o.appendChild(i)}o.value=e||D[0]}else{const d=document.createElement("option");d.value=e||"",d.textContent=e||"(no tokens found)",o.appendChild(d)}const a=document.createElement("input");a.type="text",a.className="form-input ov-token-value",a.style.flex="0 0 150px",a.placeholder="#0096c7",a.value=t||"";const r=document.createElement("input");r.type="color",r.className="ov-token-swatch",r.style.cssText="flex:0 0 38px;height:34px;padding:0;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);background:none;cursor:pointer;",/^#[0-9a-f]{6}$/i.test(t||"")&&(r.value=t),r.addEventListener("input",()=>{a.value=r.value,a.dispatchEvent(new Event("input",{bubbles:!0}))});const c=document.createElement("button");return c.type="button",c.className="btn btn-ghost btn-sm ov-token-remove",c.textContent="\xD7",n.append(o,a,r,c),n}function V(e,t,n,o){const a=document.createElement("div");return a.className=e,a.appendChild(te(t,n,o)),a}function te(e,t,n){const o=document.createDocumentFragment(),a=document.createElement("label");if(a.className="form-label",a.textContent=e,o.appendChild(a),o.appendChild(t),n){const r=document.createElement("p");r.className="form-hint text-muted",r.style.cssText="margin-top:.35rem;font-size:.8rem;",r.textContent=n,o.appendChild(r)}return o}function ne(e,t,n){const o=document.createElement("input");return o.type="text",o.className=`form-input ${e}`,o.value=t||"",o.placeholder=n||"",o}function de(e){const t=document.createElement("div"),n=ne("ov-scope",e,"#site-navbar");if(n.setAttribute("list","theme-scope-options"),t.appendChild(n),!document.getElementById("theme-scope-options")){const o=document.createElement("datalist");o.id="theme-scope-options";for(const a of he){const r=document.createElement("option");r.value=a.value,r.label=a.label,o.appendChild(r)}t.appendChild(o)}return t}function F(){const e=y.querySelectorAll(".card"),t=[];return e.forEach((n,o)=>{const a={};n.querySelectorAll(".ov-token-row").forEach(r=>{const c=r.querySelector(".ov-token-name")?.value?.trim(),d=r.querySelector(".ov-token-value")?.value?.trim();c&&d&&(a[c]=d)}),t.push({id:b[o]?.id,label:n.querySelector(".ov-label")?.value?.trim()||"",enabled:!!n.querySelector(".ov-enabled")?.checked,scope:n.querySelector(".ov-scope")?.value?.trim()||"",theme:n.querySelector(".ov-theme")?.value||"",tokens:a,css:n.querySelector(".ov-css")?.value||""})}),b=t,t}s.on("click",".ov-remove",function(){const e=Number(this.closest(".card").dataset.i);F(),b.splice(e,1),R()}),s.on("change",".ov-theme",function(){j(this.closest(".card"))}),s.on("change","#theme-frontend, #theme-auto-enabled, #theme-day",()=>{y.querySelectorAll(".card").forEach(j)}),s.on("click",".ov-add-token",function(){this.closest(".card").querySelector(".ov-tokens").appendChild(ee("",""))}),s.on("click",".ov-token-remove",function(){this.closest(".ov-token-row").remove()}),s.find("#theme-add-override").on("click",()=>{F(),b.push($({label:"",scope:"",enabled:!0})),R()}),R();async function K(){const e=document.getElementById("theme-css-preview"),t={...oe(),overrides:F()};try{const n=await w.theme.compile(t);e.textContent=n.css?.trim()||"(no overrides - nothing is injected)",e.style.color=""}catch(n){e.textContent=n.message||String(n),e.style.color="var(--dm-danger)"}}s.find("#theme-refresh-css").on("click",K);function oe(){const e=s.find("#theme-auto-enabled").prop("checked"),t={enabled:e,dayTheme:S.val()||"charcoal-light",nightTheme:I.val()||"charcoal-dark",dayStart:s.find("#theme-day-start").val()||"07:00",nightStart:s.find("#theme-night-start").val()||"19:00"};return{theme:e?t.dayTheme:f.val(),adminTheme:O.val(),autoTheme:t,font:{family:s.find("#theme-font-family").val()||"Roboto",size:parseInt(s.find("#theme-font-size").val(),10)||16},switcher:{enabled:B.prop("checked"),position:s.find("#theme-switcher-position").val()||"right-center",offsetX:parseInt(s.find("#theme-switcher-offset-x").val(),10)||0,offsetY:parseInt(s.find("#theme-switcher-offset-y").val(),10)||0,themes:Array.isArray(m.switcher?.themes)?m.switcher.themes:[]}}}function se(e){const t=document.getElementById("theme-errors");for(;t.firstChild;)t.removeChild(t.firstChild);if(!e||!e.length){t.style.display="none";return}t.style.display="";for(const n of e){const o=document.createElement("div");o.textContent=n,t.appendChild(o)}t.scrollIntoView({behavior:"smooth",block:"nearest"})}s.find("#theme-save-btn").on("click",async function(){se(null);const e={...oe(),overrides:F()};this.disabled=!0;try{m=await w.theme.save(e);const{refreshAdminTheme:n}=await import("../lib/admin-theme.js");await n(w),E.toast("Theme saved.",{type:"success"}),await K()}catch(t){const n=t.validation||[t.message||"Could not save the theme."];se(n),E.toast("Theme not saved.",{type:"error"})}finally{this.disabled=!1}}),await K()}};
|
package/package.json
CHANGED
package/public/js/site.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{makeBadgeEl as N,applyColour as M,applyPill as B,applyFloat as R,applyHighlight as z,applyItemHighlight as P,itemHighlightAttrs as V,isCurrentItem as O,menuAlign as J}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{initFloatingMenus as W}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as j,markInvalidFields as G,showValidationPopup as X}from"/public/js/forms.js?v=20260821-wizard-grid";import{buildWizardSteps as Y}from"/public/js/form-steps.mjs";$(()=>{const b=window.__CMS_NAV__||{},g=window.__CMS_SITE__||{};if(g.autoTheme?.enabled){let e=function(a){const t=(a||"07:00").split(":");return+t[0]*60+(+t[1]||0)},n=function(){const a=new Date,t=a.getHours()*60+a.getMinutes();return t>=e(o.dayStart)&&t<e(o.nightStart)?o.dayTheme:o.nightTheme};const o=g.autoTheme;Domma.theme.set(n()),setInterval(()=>Domma.theme.set(n()),6e4)}const k=$("#site-navbar");if(k.length&&b.brand){let n=function(i,l=0){return Array.isArray(i)?i.filter(s=>!(s&&s.type==="spacer")).filter(s=>!(s&&s.type==="separator"&&l===0)).map(s=>s&&s.type==="separator"?{divider:!0}:Array.isArray(s?.items)?{...s,items:n(s.items,l+1)}:s):i},a=function(i){return Array.isArray(i)?i.filter(l=>!(l&&(l.type==="separator"||l.type==="spacer"))):[]};const o={...b.brand},e=o.size&&o.size!=="md"?` navbar-brand-${o.size}`:"";if(o.logo||o.icon||o.tagline){let i="";o.logo?i+=`<img src="${o.logo}" class="navbar-brand-logo" alt="${o.text||""}">`:o.icon&&(i+=`<span data-icon="${o.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),o.text&&(i+=`<span class="navbar-brand-text${e}">${o.text}</span>`),o.tagline&&(i+=`<small class="navbar-brand-tagline">${o.tagline}</small>`),o.html=i}else e&&o.text&&(o.html=`<span class="navbar-brand-text${e}">${o.text}</span>`);const t=b.position==="floating",d=b.orientation==="vertical";Domma.elements.navbar("#site-navbar",{brand:o,items:n(b.items||[]),variant:b.variant||"dark",position:t?"static":b.position||"sticky",collapsible:!0,appearOnHover:!!b.appearOnHover}),Domma.icons.scan("#site-navbar");const h=k.get(0);h.classList.add(`dm-nav-align-${J(b)||"centre"}`),d&&k.get(0).classList.add("dm-nav-vertical",b.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left"),z(h,b),t&&(R(k.get(0),b),W(k.get(0).parentNode||document));const y=h.querySelector(".navbar-toggle"),f=h.querySelector(".navbar-collapse");if(y&&f){let i=null;const l=()=>{f.classList.contains("show")&&y.click()},s=()=>{i&&(i.remove(),i=null),document.body.classList.remove("dm-drawer-open")},r=()=>{i||(i=document.createElement("div"),i.className="dm-navbar-backdrop",i.addEventListener("click",l),document.body.appendChild(i),document.body.classList.add("dm-drawer-open"))};y.addEventListener("click",()=>{requestAnimationFrame(()=>{f.classList.contains("show")?r():s()})}),f.addEventListener("click",c=>{c.target.closest("a")&&requestAnimationFrame(s)}),document.addEventListener("keydown",c=>{c.key==="Escape"&&f.classList.contains("show")&&l()})}const u=h.querySelector(".navbar-nav");if(u){const i=Array.from(u.children);let l=0;for(const s of b.items||[])if(!(s&&s.type==="separator")){if(s&&s.type==="spacer"){const r=document.createElement("li"),c=["xs","sm","md","lg","xl","flex"].includes(s.size)?s.size:"md";r.className=`dm-menu-spacer dm-menu-spacer--${c}`,r.setAttribute("aria-hidden","true"),u.insertBefore(r,i[l]||null);continue}l++}}const w=a(b.items||[]);h.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(i=>{const l=w[parseInt(i.dataset.index,10)];if(!l)return;const s=i.dataset.subindex!=null,r=s&&Array.isArray(l.items)?l.items[parseInt(i.dataset.subindex,10)]:l;if(!r)return;r.colour&&M(i,r.colour),r.pill&&!s&&B(i,r.pill),P(i,r);const c=N(r.badge);c&&i.appendChild(c),O(r.url,location.pathname)&&(i.classList.add("active"),i.setAttribute("aria-current","page"))})}const C=$("#site-footer");if(C.length){let t=function(r){return String(r??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},d=function(r){return t(r).replace(/'/g,"'")},h=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';if(r&&r.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(r.size)?r.size:"md"}" aria-hidden="true"></li>`;const c=d(r.url||"#"),m=t(r.text||""),v=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(h).join("")}</ul>`:"",I=V(r),L=[I.hover&&` data-menu-item-hover="${d(I.hover)}"`,I.active&&` data-menu-item-active="${d(I.active)}"`].filter(Boolean).join(""),x=I.accent?` style="--dm-menu-highlight:${d(I.accent)}"`:"";return`<li><a href="${c}"${L}${x}>${m}</a>${v}</li>`},y=function(r,c){return(r||[]).forEach(m=>{m&&(m.type==="separator"||m.type==="spacer")||(c.push(m),Array.isArray(m.items)&&m.items.length&&y(m.items,c))}),c};const o=g.social||{},e={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let n='<div class="footer-inner container">';const a=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(a.copyright&&(n+=`<p>${t(a.copyright)}</p>`),a.primary.length&&(n+=`<nav class="footer-nav footer-nav--primary"><ul>${a.primary.map(h).join("")}</ul></nav>`),a.legal.length&&(n+=`<nav class="footer-nav footer-nav--legal"><ul>${a.legal.map(h).join("")}</ul></nav>`),g.footer){const r=Object.keys(e).filter(c=>o[c]);r.length&&(n+='<div class="footer-social">',r.forEach(c=>{const{label:m,svg:v}=e[c];n+=`<a href="${o[c]}" target="_blank" rel="noopener noreferrer" aria-label="${m}" class="footer-social-link">${v}</a>`}),n+="</div>")}n+="</div>",C.html(n);const f=[...y(a.primary,[]),...y(a.legal,[])];C.get(0).querySelectorAll(".footer-nav a").forEach((r,c)=>{const m=f[c];if(!m)return;m.colour&&M(r,m.colour);const v=N(m.badge);v&&r.appendChild(v),O(m.url,location.pathname)&&(r.classList.add("is-active"),r.setAttribute("aria-current","page"))}),z(C.get(0).querySelector(".footer-nav--primary"),{highlight:a.primaryHighlight}),z(C.get(0).querySelector(".footer-nav--legal"),{highlight:a.legalHighlight});const w=g.footer?.showMotionToggle!==!1,i=S.get("reduced_motion"),l=i!==null?!!i:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),s=C.get(0).querySelector(".footer-inner");if(w&&s){const r=document.createElement("input");r.type="checkbox",r.className="form-switch-input",r.id="dm-motion-switch",r.checked=l,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const c=document.createElement("span");c.className="form-switch-label",c.textContent="Reduce motion";const m=document.createElement("label");m.className="form-switch footer-motion-switch",m.title="Reduce motion",m.appendChild(r),m.appendChild(c),s.appendChild(m)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const p=$(".page-body");if(p.length){p.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),p.find(".tabs").each(function(){Domma.elements.tabs(this)}),p.find(".carousel").each(function(){const o={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const e=parseInt(this.dataset.animationDuration,10);Number.isFinite(e)&&(o.animationDuration=e)}this.dataset.animationEasing&&(o.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(e=>e.cloneNode(!0)),this._dmCarouselOpts=o),this._dmCarousel=Domma.elements.carousel(this,o)}),p.find(".dm-countdown").each(function(){const o={autoStart:!0};this.dataset.to&&(o.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(o.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(o.format=this.dataset.format),Domma.elements.timer(this,o)}),p.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),p.find(".dm-progression").each(function(){const o=Array.from(this.querySelectorAll(".dm-progression-item")).map(a=>{const t={title:a.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:a.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return a.dataset.date&&(t.date=a.dataset.date),a.dataset.status&&(t.status=a.dataset.status),a.dataset.entryId&&(t._entryId=a.dataset.entryId),t}),e=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",n=Domma.elements.progression(this,{layout:e,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...o.length?{items:o}:{}});o.length&&(this._dmCtx={instance:n,items:o})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(o=>{const e=o.dataset.revealMode||"stagger",n=o.dataset.revealAnimation||"slide-up",a=parseInt(o.dataset.revealDuration,10)||400,t=parseInt(o.dataset.revealStagger,10)||60,d=parseInt(o.dataset.revealDelay,10)||0,h=o.dataset.revealDirection==="rtl",y=Array.from(o.children),f={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};y.forEach((u,w)=>{u.style.opacity="0",u.style.transform=f[n]||"",u.style.transition=`opacity ${a}ms ease, transform ${a}ms ease`;const i=h?y.length-1-w:w;u.style.transitionDelay=e==="stagger"?`${d+i*t}ms`:`${d}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const u=new IntersectionObserver(w=>{w.forEach(i=>{i.isIntersecting&&(i.target.offsetWidth,i.target.style.opacity="1",i.target.style.transform="none",u.unobserve(i.target))})},{threshold:.1});y.forEach(w=>u.observe(w))}))}),p.find(".card[data-collapsible]").each(function(){const o=this.querySelector(".card-header");o&&o.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),p.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const o=this.dataset.soTarget,e=document.getElementById(o);if(!e)return;const n=E.slideover({title:e.dataset.soTitle||"",size:e.dataset.soSize||"md",position:e.dataset.soPosition||"right"});e.style.display="",n.setContent(e),n.open()})})}if(typeof $.setup=="function"){const o=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(e=>{try{const n=atob(e.dataset.config),a=JSON.parse(n);Object.assign(o,a)}catch{}}),Object.keys(o).length>0){const e={};for(const[n,a]of Object.entries(o)){const t=a?.events?.click,{confirm:d,toast:h,alert:y,prompt:f,...u}=t||{};d||h||y||f?($(n).on("click",async function(i){if(i.preventDefault(),d&&!await E.confirm(d))return;let l=null;if(!(f&&(l=await E.prompt(f,{inputPlaceholder:u.promptPlaceholder||"",inputValue:u.promptDefault||""}),l===null))){if(u.target){const s=$(u.target);u.toggleClass&&s.toggleClass(u.toggleClass),u.addClass&&s.addClass(u.addClass),u.removeClass&&s.removeClass(u.removeClass),l!==null&&(u.setText&&s.text(l),u.setVal&&s.val(l),u.setAttr&&s.attr(u.setAttr,l))}u.href&&(window.location.href=u.href),h&&E.toast(h,{type:u.toastType||"success"}),y&&E.alert(y)}}),Object.keys(u).length&&(e[n]={...a,events:{...a.events,click:u}})):e[n]=a}$.setup(e)}}p.length&&D(p.get(0))});function D(b){b.querySelectorAll(".dm-cta-trigger").forEach(g=>{g.__ctaWired||(g.__ctaWired=!0,g.addEventListener("click",async()=>{const k=g.dataset.action,C=g.dataset.entry,_=g.dataset.confirm;let p=S.get("auth_token");if(!p){E.toast("Please log in to perform this action.",{type:"warning"});return}if(_&&!await E.confirm(_))return;const o=Array.from(g.childNodes).map(n=>n.cloneNode(!0));g.disabled=!0,g.textContent="Running\u2026";const e=n=>fetch(`/api/actions/${k}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`},body:JSON.stringify({entryId:C})});try{let n=await e(p);if(n.status===401){const t=S.get("auth_refresh_token");if(t){const d=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:t})});if(d.ok){const{token:h}=await d.json();S.set("auth_token",h),p=h,n=await e(p)}}}const a=await n.json().catch(()=>({}));if(!n.ok)throw new Error(a.error||a.message||`Error ${n.status}`);E.toast(a.message||"Action completed.",{type:"success"})}catch(n){E.toast(n.message||"Action failed.",{type:"error"})}finally{g.disabled=!1,g.textContent="",o.forEach(n=>g.appendChild(n)),Domma.icons.scan(g)}}))})}(function(){const g=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!g.length)return;const k=S.get("auth_token");function C(p,o){const e=document.createElement("div");e.className="dm-collection-display dm-collection-empty";const n=document.createElement("p");if(o){n.appendChild(document.createTextNode("Please "));const a=document.createElement("a");a.href="/login",a.textContent="sign in",n.appendChild(a),n.appendChild(document.createTextNode(" to view your entries."))}else n.textContent=p;return e.appendChild(n),e}function _(p,o){const e=document.createElement("template");e.innerHTML=o||"",p.replaceWith(e.content)}g.forEach(async p=>{const o=p.dataset.collectionAttrs;if(!o)return;if(!k){p.replaceChildren(C("",!0));return}const e=n=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`},body:JSON.stringify({attrs:o})});try{let n=k,a=await e(n);if(a.status===401){const d=S.get("auth_refresh_token");if(d){const h=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:d})});if(h.ok){const{token:y}=await h.json();S.set("auth_token",y),n=y,a=await e(n)}}}if(!a.ok){const{error:d}=await a.json().catch(()=>({}));p.replaceChildren(C(d||"Unable to load entries.",!1));return}const{html:t}=await a.json();_(p,t)}catch{p.replaceChildren(C("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),D(document.body)},100)})(),(function(){const g=document.querySelectorAll("[data-collection-table]");!g.length||typeof T>"u"||!T.create||g.forEach(k=>{let C;try{C=JSON.parse(atob(k.dataset.payload))}catch{return}const{columns:_,rows:p,search:o,sortable:e,exportable:n,pageSize:a,empty:t,ctaConfig:d,slug:h,exportCfg:y}=C;if(!_?.length)return;if(d){const w=_.findIndex(i=>i.key==="_cta");w!==-1&&(_[w]={key:"_cta",title:"",render:(i,l)=>{const s=document.createElement("button");if(s.className=`btn btn-${d.style||"primary"} dm-cta-trigger`,s.dataset.action=d.action||"",s.dataset.entry=l._entryId||"",d.confirm&&(s.dataset.confirm=d.confirm),d.icon){const r=document.createElement("span");r.dataset.icon=d.icon,s.appendChild(r),s.appendChild(document.createTextNode(" "))}return s.appendChild(document.createTextNode(d.label||"Run")),s}})}const f="col-table-"+Math.random().toString(36).slice(2,7),u=document.createElement("div");u.id=f,k.replaceChildren(u),T.create("#"+f,{data:p,columns:_,search:o,sortable:e,exportable:n,pageSize:a,emptyMessage:t}),d&&D(u),k._dmTable={columns:_,rows:p,slug:h,exportCfg:y,render(w){T.create("#"+f,{data:w,columns:_,search:o,sortable:e,exportable:n,pageSize:a,emptyMessage:t}),d&&D(u)}}})})(),(function(){const g=document.querySelectorAll("[data-form-inline]");if(!g.length||typeof F>"u")return;function k(e){const n=e&&e.logic;return!!(n&&(n.visibility||n.requirement))}function C(e,n){const a={};return(e||[]).forEach(t=>{if(t.type==="page-break"||t.type==="spacer"||!t.name)return;const d=t.type==="chooser"||!!(t.variant&&["select","radio","checkbox-group"].includes(t.type)),h=t.type==="checkbox"?"boolean":t.type==="date"||t.type==="icon"||t.type==="image"?"string":d?"chooser":t.type,y={...t.formConfig||{}};y.span==="full"&&n&&(y.span=n);const f={type:h,required:k(t)?!1:t.required,label:t.label,options:t.options,formConfig:{...t.placeholder&&{placeholder:t.placeholder},...t.helper&&{hint:t.helper},...y}};d&&(t.variant&&(f.variant=t.variant),(t.multiple||t.type==="checkbox-group")&&(f.multiple=!0),t.density&&(f.density=t.density),t.columns&&(f.columns=t.columns),t.accent&&(f.accent=t.accent),t.accentStyle&&(f.accentStyle=t.accentStyle),t.glow&&(f.glow=!0),t.glowColour&&(f.glowColour=t.glowColour),t.shadow&&(f.shadow=t.shadow),t.shadowColour&&(f.shadowColour=t.shadowColour)),a[t.name]=f}),a}function _(e){const n={};return(e||[]).forEach(a=>{if(!(!a.name||a.type==="page-break"||a.type==="spacer")&&(a.type==="select"||a.type==="multiselect")&&a.required){const t=(a.options||[])[0];t&&(n[a.name]=typeof t=="object"?t.value:t)}}),n}function p(e,n){(n||[]).forEach(a=>{if(a.type!=="date"||!a.name)return;const t=e.querySelector(`[name="${a.name}"]`);t&&t.type!=="date"&&(t.type="date")})}function o(e,n,a){let t=e.querySelector(".cms-form-message");t||(t=document.createElement("p"),t.className="cms-form-message",e.appendChild(t)),t.textContent=n,t.style.cssText=a?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}g.forEach(e=>{let n;try{n=JSON.parse(atob(e.dataset.formInline))}catch{return}const a=n.fields||[],t=n.settings||{},d=Number(t.columns)===3?3:2,h=t.layout||"grid",y=a.some(l=>l.type==="page-break"),f=async l=>{if(e.dataset.triggerBlocked==="true"){const s=e._formLogicRuntime,r=s&&s.submitBlock?s.submitBlock().reasons:[];o(e,r.join(" ")||"This form cannot be submitted yet.",!0);const c=new Error("Blocked by a form trigger");throw c.formSubmitFailed=!0,c}try{const s=e.querySelector(".fb-form-honeypot"),r=s?.querySelector('input[type="text"]')||null,c=s?.querySelector('[name="_t"]')||null,m=Object.assign({},l);delete m._t,s?.dataset.hpName&&delete m[s.dataset.hpName],r!==null&&(m._hp=r.value),c!==null&&(m._t=c.value);const v=await H.post(`/api/forms/submit/${n.slug}`,m);if(v&&v.error){o(e,v.error,!0);const I=new Error(v.error);throw I.formSubmitFailed=!0,I}if(v?.redirect){window.location.href=v.redirect;return}for(;e.firstChild;)e.removeChild(e.firstChild);o(e,v?.message||t.successMessage||"Thank you for your submission.",!1)}catch(s){throw s.formSubmitFailed||o(e,s.message||"Submission failed. Please try again.",!0),s}},u=Date.now();function w(l){const s=l.querySelector("form");if(!s||s.querySelector(".fb-form-honeypot"))return;const r=document.createElement("div");r.className="fb-form-honeypot",r.setAttribute("aria-hidden","true");const c=document.createElement("input");c.name=s.querySelector('[name="website"]')?"_website":"website",r.dataset.hpName=c.name,c.type="text",c.tabIndex=-1,c.autocomplete="url",c.placeholder="https://",r.appendChild(c);const m=document.createElement("input");m.name="_t",m.type="hidden",m.value=u,r.appendChild(m),s.appendChild(r)}function i(l){if(!window.FormLogicEngine||!a.some(r=>r.logic||Array.isArray(r.triggers)&&r.triggers.length))return null;const s=new window.FormLogicEngine.FormLogicRuntime(n,e,l?{baseValues:l}:void 0);if(s.init(),e._formLogicRuntime=s,e._formLogicObserved)return s;if(e._formLogicObserved=!0,e.parentNode&&typeof MutationObserver<"u"){const r=new MutationObserver(function(c){for(const m of c)for(const v of m.removedNodes)if(v===e||v.nodeType===1&&v.contains&&v.contains(e)){e._formLogicRuntime&&e._formLogicRuntime.destroy(),r.disconnect();return}});r.observe(e.parentNode,{childList:!0,subtree:!1}),e._formLogicObserver=r}return s}if(y&&F.wizard){const l=Y(a,n.title),s=l.map(x=>({title:x.title,description:x.description,fields:C(x.fields,d)})),r=l.map(x=>x.fields);let c=0,m=null,v=null;const I=x=>{const A=window.FormLogicEngine;if(!A||!A.validateFields)return;const q=A.validateFields(e,r[c]||[]);q.valid||(x.preventDefault(),x.stopImmediatePropagation(),G(e,q.missing),X(q))},L=()=>{const x=r[c]||a;p(e,x),j(e,x),t.honeypot!==!1&&w(e),m&&m.destroy(),m=i(v?.getStepData?v.getStepData():void 0),e.querySelectorAll(".wizard-next, .wizard-submit").forEach(A=>A.addEventListener("click",I,!0))};v=F.wizard(e,{schema:{steps:s},layout:h,columns:d,finishText:t.submitText||"Finish",onSubmit:f,onStepChange:x=>{c=x,setTimeout(L,0)}}),e.addEventListener("dm-form-trigger",x=>{if(x.detail?.type!=="goto-step"||!v?.goToStep)return;const A=Number(x.detail.action.step);!Number.isFinite(A)||A<0||A>=s.length||A!==c&&v.goToStep(A)}),Promise.resolve(v).then(L)}else if(F.render){const l=F.render(e,C(a,d),_(a),{submitText:t.submitText||"Submit",layout:h,columns:d,onSubmit:f});Promise.resolve(l).then(function(){if(h==="grid"&&t.submitSpan==="full"){const s=e.querySelector(".form-buttons");s&&s.classList.add("col-span-full")}p(e,a),j(e,a),t.honeypot!==!1&&w(e),i()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
|
|
1
|
+
import{makeBadgeEl as N,applyColour as M,applyPill as B,applyFloat as R,applyHighlight as z,applyItemHighlight as P,itemHighlightAttrs as V,isCurrentItem as O,menuAlign as J}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{initFloatingMenus as W}from"/public/js/menu-float.js?v=20260829-float-panel";import{enhancePickerFields as j,markInvalidFields as G,showValidationPopup as X}from"/public/js/forms.js?v=20260821-wizard-grid";import{buildWizardSteps as Y}from"/public/js/form-steps.mjs";$(()=>{const b=window.__CMS_NAV__||{},g=window.__CMS_SITE__||{};if(g.autoTheme?.enabled){let t=function(e){const c=(e||"07:00").split(":");return+c[0]*60+(+c[1]||0)},o=function(){const e=new Date,c=e.getHours()*60+e.getMinutes();return c>=t(a.dayStart)&&c<t(a.nightStart)?a.dayTheme:a.nightTheme},n=function(e){const c=a.apply&&a.apply[e]||{theme:e,custom:""};Domma.theme.set(c.theme);for(const p of[...document.body.classList])p.startsWith("dm-custom-")&&document.body.classList.remove(p);c.custom&&document.body.classList.add(c.custom)};const a=g.autoTheme;n(o()),setInterval(()=>n(o()),6e4)}const k=$("#site-navbar");if(k.length&&b.brand){let o=function(i,d=0){return Array.isArray(i)?i.filter(s=>!(s&&s.type==="spacer")).filter(s=>!(s&&s.type==="separator"&&d===0)).map(s=>s&&s.type==="separator"?{divider:!0}:Array.isArray(s?.items)?{...s,items:o(s.items,d+1)}:s):i},n=function(i){return Array.isArray(i)?i.filter(d=>!(d&&(d.type==="separator"||d.type==="spacer"))):[]};const a={...b.brand},t=a.size&&a.size!=="md"?` navbar-brand-${a.size}`:"";if(a.logo||a.icon||a.tagline){let i="";a.logo?i+=`<img src="${a.logo}" class="navbar-brand-logo" alt="${a.text||""}">`:a.icon&&(i+=`<span data-icon="${a.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),a.text&&(i+=`<span class="navbar-brand-text${t}">${a.text}</span>`),a.tagline&&(i+=`<small class="navbar-brand-tagline">${a.tagline}</small>`),a.html=i}else t&&a.text&&(a.html=`<span class="navbar-brand-text${t}">${a.text}</span>`);const e=b.position==="floating",c=b.orientation==="vertical";Domma.elements.navbar("#site-navbar",{brand:a,items:o(b.items||[]),variant:b.variant||"dark",position:e?"static":b.position||"sticky",collapsible:!0,appearOnHover:!!b.appearOnHover}),Domma.icons.scan("#site-navbar");const p=k.get(0);p.classList.add(`dm-nav-align-${J(b)||"centre"}`),c&&k.get(0).classList.add("dm-nav-vertical",b.side==="right"?"dm-nav-vertical-right":"dm-nav-vertical-left"),z(p,b),e&&(R(k.get(0),b),W(k.get(0).parentNode||document));const y=p.querySelector(".navbar-toggle"),h=p.querySelector(".navbar-collapse");if(y&&h){let i=null;const d=()=>{h.classList.contains("show")&&y.click()},s=()=>{i&&(i.remove(),i=null),document.body.classList.remove("dm-drawer-open")},r=()=>{i||(i=document.createElement("div"),i.className="dm-navbar-backdrop",i.addEventListener("click",d),document.body.appendChild(i),document.body.classList.add("dm-drawer-open"))};y.addEventListener("click",()=>{requestAnimationFrame(()=>{h.classList.contains("show")?r():s()})}),h.addEventListener("click",l=>{l.target.closest("a")&&requestAnimationFrame(s)}),document.addEventListener("keydown",l=>{l.key==="Escape"&&h.classList.contains("show")&&d()})}const u=p.querySelector(".navbar-nav");if(u){const i=Array.from(u.children);let d=0;for(const s of b.items||[])if(!(s&&s.type==="separator")){if(s&&s.type==="spacer"){const r=document.createElement("li"),l=["xs","sm","md","lg","xl","flex"].includes(s.size)?s.size:"md";r.className=`dm-menu-spacer dm-menu-spacer--${l}`,r.setAttribute("aria-hidden","true"),u.insertBefore(r,i[d]||null);continue}d++}}const w=n(b.items||[]);p.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(i=>{const d=w[parseInt(i.dataset.index,10)];if(!d)return;const s=i.dataset.subindex!=null,r=s&&Array.isArray(d.items)?d.items[parseInt(i.dataset.subindex,10)]:d;if(!r)return;r.colour&&M(i,r.colour),r.pill&&!s&&B(i,r.pill),P(i,r);const l=N(r.badge);l&&i.appendChild(l),O(r.url,location.pathname)&&(i.classList.add("active"),i.setAttribute("aria-current","page"))})}const C=$("#site-footer");if(C.length){let e=function(r){return String(r??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},c=function(r){return e(r).replace(/'/g,"'")},p=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';if(r&&r.type==="spacer")return`<li class="dm-menu-spacer dm-menu-spacer--${["xs","sm","md","lg","xl","flex"].includes(r.size)?r.size:"md"}" aria-hidden="true"></li>`;const l=c(r.url||"#"),m=e(r.text||""),v=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(p).join("")}</ul>`:"",L=V(r),I=[L.hover&&` data-menu-item-hover="${c(L.hover)}"`,L.active&&` data-menu-item-active="${c(L.active)}"`].filter(Boolean).join(""),x=L.accent?` style="--dm-menu-highlight:${c(L.accent)}"`:"";return`<li><a href="${l}"${I}${x}>${m}</a>${v}</li>`},y=function(r,l){return(r||[]).forEach(m=>{m&&(m.type==="separator"||m.type==="spacer")||(l.push(m),Array.isArray(m.items)&&m.items.length&&y(m.items,l))}),l};const a=g.social||{},t={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let o='<div class="footer-inner container">';const n=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(n.copyright&&(o+=`<p>${e(n.copyright)}</p>`),n.primary.length&&(o+=`<nav class="footer-nav footer-nav--primary"><ul>${n.primary.map(p).join("")}</ul></nav>`),n.legal.length&&(o+=`<nav class="footer-nav footer-nav--legal"><ul>${n.legal.map(p).join("")}</ul></nav>`),g.footer){const r=Object.keys(t).filter(l=>a[l]);r.length&&(o+='<div class="footer-social">',r.forEach(l=>{const{label:m,svg:v}=t[l];o+=`<a href="${a[l]}" target="_blank" rel="noopener noreferrer" aria-label="${m}" class="footer-social-link">${v}</a>`}),o+="</div>")}o+="</div>",C.html(o);const h=[...y(n.primary,[]),...y(n.legal,[])];C.get(0).querySelectorAll(".footer-nav a").forEach((r,l)=>{const m=h[l];if(!m)return;m.colour&&M(r,m.colour);const v=N(m.badge);v&&r.appendChild(v),O(m.url,location.pathname)&&(r.classList.add("is-active"),r.setAttribute("aria-current","page"))}),z(C.get(0).querySelector(".footer-nav--primary"),{highlight:n.primaryHighlight}),z(C.get(0).querySelector(".footer-nav--legal"),{highlight:n.legalHighlight});const w=g.footer?.showMotionToggle!==!1,i=S.get("reduced_motion"),d=i!==null?!!i:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),s=C.get(0).querySelector(".footer-inner");if(w&&s){const r=document.createElement("input");r.type="checkbox",r.className="form-switch-input",r.id="dm-motion-switch",r.checked=d,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const l=document.createElement("span");l.className="form-switch-label",l.textContent="Reduce motion";const m=document.createElement("label");m.className="form-switch footer-motion-switch",m.title="Reduce motion",m.appendChild(r),m.appendChild(l),s.appendChild(m)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const f=$(".page-body");if(f.length){f.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),f.find(".tabs").each(function(){Domma.elements.tabs(this)}),f.find(".carousel").each(function(){const a={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const t=parseInt(this.dataset.animationDuration,10);Number.isFinite(t)&&(a.animationDuration=t)}this.dataset.animationEasing&&(a.animationEasing=this.dataset.animationEasing),this.dataset.ctx&&(this._dmCtxSlides=Array.from(this.querySelectorAll(".carousel-slide")).map(t=>t.cloneNode(!0)),this._dmCarouselOpts=a),this._dmCarousel=Domma.elements.carousel(this,a)}),f.find(".dm-countdown").each(function(){const a={autoStart:!0};this.dataset.to&&(a.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(a.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(a.format=this.dataset.format),Domma.elements.timer(this,a)}),f.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),f.find(".dm-progression").each(function(){const a=Array.from(this.querySelectorAll(".dm-progression-item")).map(n=>{const e={title:n.querySelector(".dm-progression-item-title")?.textContent?.trim()||"",description:n.querySelector(".dm-progression-item-body")?.innerHTML?.trim()||""};return n.dataset.date&&(e.date=n.dataset.date),n.dataset.status&&(e.status=n.dataset.status),n.dataset.entryId&&(e._entryId=n.dataset.entryId),e}),t=this.dataset.layout==="centred"?"centered":this.dataset.layout||"vertical",o=Domma.elements.progression(this,{layout:t,theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0,allowHtml:!0,...a.length?{items:a}:{}});a.length&&(this._dmCtx={instance:o,items:a})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(a=>{const t=a.dataset.revealMode||"stagger",o=a.dataset.revealAnimation||"slide-up",n=parseInt(a.dataset.revealDuration,10)||400,e=parseInt(a.dataset.revealStagger,10)||60,c=parseInt(a.dataset.revealDelay,10)||0,p=a.dataset.revealDirection==="rtl",y=Array.from(a.children),h={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};y.forEach((u,w)=>{u.style.opacity="0",u.style.transform=h[o]||"",u.style.transition=`opacity ${n}ms ease, transform ${n}ms ease`;const i=p?y.length-1-w:w;u.style.transitionDelay=t==="stagger"?`${c+i*e}ms`:`${c}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const u=new IntersectionObserver(w=>{w.forEach(i=>{i.isIntersecting&&(i.target.offsetWidth,i.target.style.opacity="1",i.target.style.transform="none",u.unobserve(i.target))})},{threshold:.1});y.forEach(w=>u.observe(w))}))}),f.find(".card[data-collapsible]").each(function(){const a=this.querySelector(".card-header");a&&a.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),f.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const a=this.dataset.soTarget,t=document.getElementById(a);if(!t)return;const o=E.slideover({title:t.dataset.soTitle||"",size:t.dataset.soSize||"md",position:t.dataset.soPosition||"right"});t.style.display="",o.setContent(t),o.open()})})}if(typeof $.setup=="function"){const a=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(t=>{try{const o=atob(t.dataset.config),n=JSON.parse(o);Object.assign(a,n)}catch{}}),Object.keys(a).length>0){const t={};for(const[o,n]of Object.entries(a)){const e=n?.events?.click,{confirm:c,toast:p,alert:y,prompt:h,...u}=e||{};c||p||y||h?($(o).on("click",async function(i){if(i.preventDefault(),c&&!await E.confirm(c))return;let d=null;if(!(h&&(d=await E.prompt(h,{inputPlaceholder:u.promptPlaceholder||"",inputValue:u.promptDefault||""}),d===null))){if(u.target){const s=$(u.target);u.toggleClass&&s.toggleClass(u.toggleClass),u.addClass&&s.addClass(u.addClass),u.removeClass&&s.removeClass(u.removeClass),d!==null&&(u.setText&&s.text(d),u.setVal&&s.val(d),u.setAttr&&s.attr(u.setAttr,d))}u.href&&(window.location.href=u.href),p&&E.toast(p,{type:u.toastType||"success"}),y&&E.alert(y)}}),Object.keys(u).length&&(t[o]={...n,events:{...n.events,click:u}})):t[o]=n}$.setup(t)}}f.length&&D(f.get(0))});function D(b){b.querySelectorAll(".dm-cta-trigger").forEach(g=>{g.__ctaWired||(g.__ctaWired=!0,g.addEventListener("click",async()=>{const k=g.dataset.action,C=g.dataset.entry,_=g.dataset.confirm;let f=S.get("auth_token");if(!f){E.toast("Please log in to perform this action.",{type:"warning"});return}if(_&&!await E.confirm(_))return;const a=Array.from(g.childNodes).map(o=>o.cloneNode(!0));g.disabled=!0,g.textContent="Running\u2026";const t=o=>fetch(`/api/actions/${k}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify({entryId:C})});try{let o=await t(f);if(o.status===401){const e=S.get("auth_refresh_token");if(e){const c=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(c.ok){const{token:p}=await c.json();S.set("auth_token",p),f=p,o=await t(f)}}}const n=await o.json().catch(()=>({}));if(!o.ok)throw new Error(n.error||n.message||`Error ${o.status}`);E.toast(n.message||"Action completed.",{type:"success"})}catch(o){E.toast(o.message||"Action failed.",{type:"error"})}finally{g.disabled=!1,g.textContent="",a.forEach(o=>g.appendChild(o)),Domma.icons.scan(g)}}))})}(function(){const g=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!g.length)return;const k=S.get("auth_token");function C(f,a){const t=document.createElement("div");t.className="dm-collection-display dm-collection-empty";const o=document.createElement("p");if(a){o.appendChild(document.createTextNode("Please "));const n=document.createElement("a");n.href="/login",n.textContent="sign in",o.appendChild(n),o.appendChild(document.createTextNode(" to view your entries."))}else o.textContent=f;return t.appendChild(o),t}function _(f,a){const t=document.createElement("template");t.innerHTML=a||"",f.replaceWith(t.content)}g.forEach(async f=>{const a=f.dataset.collectionAttrs;if(!a)return;if(!k){f.replaceChildren(C("",!0));return}const t=o=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify({attrs:a})});try{let o=k,n=await t(o);if(n.status===401){const c=S.get("auth_refresh_token");if(c){const p=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:c})});if(p.ok){const{token:y}=await p.json();S.set("auth_token",y),o=y,n=await t(o)}}}if(!n.ok){const{error:c}=await n.json().catch(()=>({}));f.replaceChildren(C(c||"Unable to load entries.",!1));return}const{html:e}=await n.json();_(f,e)}catch{f.replaceChildren(C("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),D(document.body)},100)})(),(function(){const g=document.querySelectorAll("[data-collection-table]");!g.length||typeof T>"u"||!T.create||g.forEach(k=>{let C;try{C=JSON.parse(atob(k.dataset.payload))}catch{return}const{columns:_,rows:f,search:a,sortable:t,exportable:o,pageSize:n,empty:e,ctaConfig:c,slug:p,exportCfg:y}=C;if(!_?.length)return;if(c){const w=_.findIndex(i=>i.key==="_cta");w!==-1&&(_[w]={key:"_cta",title:"",render:(i,d)=>{const s=document.createElement("button");if(s.className=`btn btn-${c.style||"primary"} dm-cta-trigger`,s.dataset.action=c.action||"",s.dataset.entry=d._entryId||"",c.confirm&&(s.dataset.confirm=c.confirm),c.icon){const r=document.createElement("span");r.dataset.icon=c.icon,s.appendChild(r),s.appendChild(document.createTextNode(" "))}return s.appendChild(document.createTextNode(c.label||"Run")),s}})}const h="col-table-"+Math.random().toString(36).slice(2,7),u=document.createElement("div");u.id=h,k.replaceChildren(u),T.create("#"+h,{data:f,columns:_,search:a,sortable:t,exportable:o,pageSize:n,emptyMessage:e}),c&&D(u),k._dmTable={columns:_,rows:f,slug:p,exportCfg:y,render(w){T.create("#"+h,{data:w,columns:_,search:a,sortable:t,exportable:o,pageSize:n,emptyMessage:e}),c&&D(u)}}})})(),(function(){const g=document.querySelectorAll("[data-form-inline]");if(!g.length||typeof F>"u")return;function k(t){const o=t&&t.logic;return!!(o&&(o.visibility||o.requirement))}function C(t,o){const n={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const c=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),p=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":c?"chooser":e.type,y={...e.formConfig||{}};y.span==="full"&&o&&(y.span=o);const h={type:p,required:k(e)?!1:e.required,label:e.label,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...y}};c&&(e.variant&&(h.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(h.multiple=!0),e.density&&(h.density=e.density),e.columns&&(h.columns=e.columns),e.accent&&(h.accent=e.accent),e.accentStyle&&(h.accentStyle=e.accentStyle),e.glow&&(h.glow=!0),e.glowColour&&(h.glowColour=e.glowColour),e.shadow&&(h.shadow=e.shadow),e.shadowColour&&(h.shadowColour=e.shadowColour)),n[e.name]=h}),n}function _(t){const o={};return(t||[]).forEach(n=>{if(!(!n.name||n.type==="page-break"||n.type==="spacer")&&(n.type==="select"||n.type==="multiselect")&&n.required){const e=(n.options||[])[0];e&&(o[n.name]=typeof e=="object"?e.value:e)}}),o}function f(t,o){(o||[]).forEach(n=>{if(n.type!=="date"||!n.name)return;const e=t.querySelector(`[name="${n.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function a(t,o,n){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=o,e.style.cssText=n?"color:var(--dm-danger);margin-top:.75rem;":"color:var(--dm-success);margin-top:.75rem;"}g.forEach(t=>{let o;try{o=JSON.parse(atob(t.dataset.formInline))}catch{return}const n=o.fields||[],e=o.settings||{},c=Number(e.columns)===3?3:2,p=e.layout||"grid",y=n.some(d=>d.type==="page-break"),h=async d=>{if(t.dataset.triggerBlocked==="true"){const s=t._formLogicRuntime,r=s&&s.submitBlock?s.submitBlock().reasons:[];a(t,r.join(" ")||"This form cannot be submitted yet.",!0);const l=new Error("Blocked by a form trigger");throw l.formSubmitFailed=!0,l}try{const s=t.querySelector(".fb-form-honeypot"),r=s?.querySelector('input[type="text"]')||null,l=s?.querySelector('[name="_t"]')||null,m=Object.assign({},d);delete m._t,s?.dataset.hpName&&delete m[s.dataset.hpName],r!==null&&(m._hp=r.value),l!==null&&(m._t=l.value);const v=await H.post(`/api/forms/submit/${o.slug}`,m);if(v&&v.error){a(t,v.error,!0);const L=new Error(v.error);throw L.formSubmitFailed=!0,L}if(v?.redirect){window.location.href=v.redirect;return}for(;t.firstChild;)t.removeChild(t.firstChild);a(t,v?.message||e.successMessage||"Thank you for your submission.",!1)}catch(s){throw s.formSubmitFailed||a(t,s.message||"Submission failed. Please try again.",!0),s}},u=Date.now();function w(d){const s=d.querySelector("form");if(!s||s.querySelector(".fb-form-honeypot"))return;const r=document.createElement("div");r.className="fb-form-honeypot",r.setAttribute("aria-hidden","true");const l=document.createElement("input");l.name=s.querySelector('[name="website"]')?"_website":"website",r.dataset.hpName=l.name,l.type="text",l.tabIndex=-1,l.autocomplete="url",l.placeholder="https://",r.appendChild(l);const m=document.createElement("input");m.name="_t",m.type="hidden",m.value=u,r.appendChild(m),s.appendChild(r)}function i(d){if(!window.FormLogicEngine||!n.some(r=>r.logic||Array.isArray(r.triggers)&&r.triggers.length))return null;const s=new window.FormLogicEngine.FormLogicRuntime(o,t,d?{baseValues:d}:void 0);if(s.init(),t._formLogicRuntime=s,t._formLogicObserved)return s;if(t._formLogicObserved=!0,t.parentNode&&typeof MutationObserver<"u"){const r=new MutationObserver(function(l){for(const m of l)for(const v of m.removedNodes)if(v===t||v.nodeType===1&&v.contains&&v.contains(t)){t._formLogicRuntime&&t._formLogicRuntime.destroy(),r.disconnect();return}});r.observe(t.parentNode,{childList:!0,subtree:!1}),t._formLogicObserver=r}return s}if(y&&F.wizard){const d=Y(n,o.title),s=d.map(x=>({title:x.title,description:x.description,fields:C(x.fields,c)})),r=d.map(x=>x.fields);let l=0,m=null,v=null;const L=x=>{const A=window.FormLogicEngine;if(!A||!A.validateFields)return;const q=A.validateFields(t,r[l]||[]);q.valid||(x.preventDefault(),x.stopImmediatePropagation(),G(t,q.missing),X(q))},I=()=>{const x=r[l]||n;f(t,x),j(t,x),e.honeypot!==!1&&w(t),m&&m.destroy(),m=i(v?.getStepData?v.getStepData():void 0),t.querySelectorAll(".wizard-next, .wizard-submit").forEach(A=>A.addEventListener("click",L,!0))};v=F.wizard(t,{schema:{steps:s},layout:p,columns:c,finishText:e.submitText||"Finish",onSubmit:h,onStepChange:x=>{l=x,setTimeout(I,0)}}),t.addEventListener("dm-form-trigger",x=>{if(x.detail?.type!=="goto-step"||!v?.goToStep)return;const A=Number(x.detail.action.step);!Number.isFinite(A)||A<0||A>=s.length||A!==l&&v.goToStep(A)}),Promise.resolve(v).then(I)}else if(F.render){const d=F.render(t,C(n,c),_(n),{submitText:e.submitText||"Submit",layout:p,columns:c,onSubmit:h});Promise.resolve(d).then(function(){if(p==="grid"&&e.submitSpan==="full"){const s=t.querySelector(".form-buttons");s&&s.classList.add("col-span-full")}f(t,n),j(t,n),e.honeypot!==!1&&w(t),i()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";var a=window.__CMS_THEME_SWITCHER__;if(!a||!Array.isArray(a.themes)||!a.themes.length)return;var
|
|
1
|
+
(function(){"use strict";var a=window.__CMS_THEME_SWITCHER__;if(!a||!Array.isArray(a.themes)||!a.themes.length)return;var i=a.position||"right-center",m=i==="left-center"||i==="bottom-left"||i==="top-left",u=i==="bottom-left"||i==="bottom-right",l=i==="top-left"||i==="top-right",c=a.offsetX!=null?a.offsetX:32,f=a.offsetY!=null?a.offsetY:32,n=document.createElement("div");n.className="dm-theme-switcher"+(m?" dm-ts-left":"")+(l?" dm-ts-top":""),n.id="dm-theme-switcher",n.setAttribute("role","navigation"),n.setAttribute("aria-label","Theme switcher");var o=document.createElement("button");o.className="dm-ts-trigger",o.setAttribute("data-tooltip","Switch theme"),o.setAttribute("aria-haspopup","true"),o.setAttribute("aria-expanded","false");var p=360/a.themes.length;o.style.background="conic-gradient("+a.themes.map(function(e,t){return(e.from||"#888")+" "+(t*p).toFixed(2)+"deg "+((t+1)*p).toFixed(2)+"deg"}).join(", ")+")";var s=document.createElement("div");s.className="dm-ts-options",s.setAttribute("role","menu");var h=a.themes.map(function(e){var t=document.createElement("button");return t.className="dm-ts-dot",t.dataset.theme=e.id,t.setAttribute("data-tooltip",e.label),t.setAttribute("aria-label",e.label),t.setAttribute("role","menuitem"),t.style.background="linear-gradient(135deg, "+(e.from||"#888")+", "+(e.to||e.from||"#888")+")",s.appendChild(t),t});n.appendChild(o),n.appendChild(s),Object.assign(n.style,{right:m?"auto":c+"px",left:m?c+"px":"auto",top:l?f+"px":u?"auto":"50vh",bottom:u?f+"px":"auto",transform:l||u?"none":"translateY(-50%)"});function b(){document.body.appendChild(n)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",b):b();function g(e,t){typeof Domma<"u"&&Domma.theme?e(Domma.theme):t>0&&setTimeout(function(){g(e,t-1)},100)}g(function(e){function t(){var r=e.get();h.forEach(function(d){d.classList.toggle("active",d.dataset.theme===r)})}h.forEach(function(r){r.addEventListener("click",function(){e.set(r.dataset.theme),document.body.className=document.body.className.split(" ").filter(function(d){return d.indexOf("dm-custom-")!==0}).join(" "),t()})}),e.onChange(t),t(),o.addEventListener("click",function(){var r=o.getAttribute("aria-expanded")==="true";o.setAttribute("aria-expanded",String(!r))})},30)})();
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* GET /api/theme - the theme config (themes, fonts, overrides)
|
|
5
5
|
* PUT /api/theme - replace it
|
|
6
6
|
* GET /api/theme/catalog - theme ids and --dm-* token names the shipped
|
|
7
|
-
* stylesheet actually defines
|
|
7
|
+
* stylesheet actually defines, and the custom
|
|
8
|
+
* themes plugins provide
|
|
9
|
+
* GET /api/theme/admin - how to apply the admin's own theme (any signed-in
|
|
10
|
+
* user - everyone's admin needs one)
|
|
8
11
|
* POST /api/theme/compile - compile a candidate config to CSS without saving,
|
|
9
12
|
* so the admin previews exactly what the page gets
|
|
10
13
|
*
|
|
@@ -16,11 +19,15 @@ import {
|
|
|
16
19
|
requirePermission as defaultRequirePermission
|
|
17
20
|
} from '../../middleware/auth.js';
|
|
18
21
|
import {
|
|
22
|
+
compileCustomThemeCss,
|
|
19
23
|
compileOverrideCss,
|
|
20
24
|
getThemeTokenMap,
|
|
25
|
+
listCustomThemes,
|
|
26
|
+
resolveThemeClasses,
|
|
21
27
|
listThemeIds,
|
|
22
28
|
listThemeTokens,
|
|
23
29
|
loadThemeConfig,
|
|
30
|
+
rememberedIds,
|
|
24
31
|
saveThemeConfig,
|
|
25
32
|
validateThemeConfig
|
|
26
33
|
} from '../../services/themeSettings.js';
|
|
@@ -38,9 +45,21 @@ export async function themeRoutes(fastify, opts = {}) {
|
|
|
38
45
|
// rather than from a hardcoded list that would drift from the CSS.
|
|
39
46
|
fastify.get('/theme/catalog', canRead, async () => ({
|
|
40
47
|
themes: listThemeIds(),
|
|
41
|
-
tokens: listThemeTokens()
|
|
48
|
+
tokens: listThemeTokens(),
|
|
49
|
+
// Custom themes are listed apart: they are a base plus changes, and the
|
|
50
|
+
// public switcher cannot offer them.
|
|
51
|
+
custom: listCustomThemes().map(({id, label, baseTheme, owner}) => ({id, label, baseTheme, owner}))
|
|
42
52
|
}));
|
|
43
53
|
|
|
54
|
+
// The admin applies its theme before anything else loads. A custom admin
|
|
55
|
+
// theme is its base, its class, and the CSS behind that class.
|
|
56
|
+
fastify.get('/theme/admin', {preHandler: [authenticate]}, async () => {
|
|
57
|
+
const cfg = await loadThemeConfig();
|
|
58
|
+
const id = cfg.adminTheme || 'charcoal-dark';
|
|
59
|
+
const {dommaTheme, customClass} = resolveThemeClasses(id, cfg);
|
|
60
|
+
return {theme: id, dommaTheme, customClass, css: customClass ? compileCustomThemeCss([id]) : ''};
|
|
61
|
+
});
|
|
62
|
+
|
|
44
63
|
/*
|
|
45
64
|
* One theme's token values. Fetched on demand rather than folded into
|
|
46
65
|
* /catalog: 32 themes x ~245 tokens is a payload nobody needs on a page
|
|
@@ -62,7 +81,7 @@ export async function themeRoutes(fastify, opts = {}) {
|
|
|
62
81
|
|
|
63
82
|
fastify.post('/theme/compile', canRead, async (request, reply) => {
|
|
64
83
|
const candidate = request.body || {};
|
|
65
|
-
const errors = validateThemeConfig(candidate);
|
|
84
|
+
const errors = validateThemeConfig(candidate, {remembered: rememberedIds(await loadThemeConfig())});
|
|
66
85
|
if (errors.length) return reply.code(400).send({error: errors.join('; '), validation: errors});
|
|
67
86
|
return {css: compileOverrideCss(candidate)};
|
|
68
87
|
});
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
registerTransform
|
|
26
26
|
} from './hooks.js';
|
|
27
27
|
import {registerPluginResource, unregisterPluginResourcesByPlugin} from './permissionRegistry.js';
|
|
28
|
+
import {registerThemeProvider} from './themeSettings.js';
|
|
28
29
|
import {classifyEntitlement, mayLoad} from './pluginEntitlement.js';
|
|
29
30
|
import {createCollection, getCollection} from './collections.js';
|
|
30
31
|
import * as defaultRolesService from './roles.js';
|
|
@@ -464,6 +465,9 @@ export async function registerPlugins(fastify) {
|
|
|
464
465
|
hooks: {registerShortcode, registerSanitizeRules, registerTransform, registerMenuLocation,
|
|
465
466
|
// Stamped with the plugin's name, so the Menus editor can say where a Tool came from.
|
|
466
467
|
registerSidebarItem: (opts) => registerSidebarItem({...opts, item: {source: manifest.displayName || manifest.name, ...opts?.item}}),
|
|
468
|
+
// Custom themes the plugin offers (the Theme Roller) - bound to the
|
|
469
|
+
// plugin, so one plugin cannot replace another's. See themeSettings.js.
|
|
470
|
+
registerThemes: (provider) => registerThemeProvider(manifest.name, provider),
|
|
467
471
|
on: hooks.on.bind(hooks), ...pluginRoleHooks(manifest.name)},
|
|
468
472
|
settings,
|
|
469
473
|
config: {}
|
|
@@ -12,7 +12,7 @@ import {resolveLocation, resolveMenuDecorations, resolveOverlaysForPage} from '.
|
|
|
12
12
|
import {buildMenuNav} from './menuRender.js';
|
|
13
13
|
import {getProjectForPage} from './projects.js';
|
|
14
14
|
import {resolveContextMenusForPage} from './contextMenus.js';
|
|
15
|
-
import {buildOverrideStyleTag, getThemeTokenMap, listThemeIds, loadThemeConfig} from './themeSettings.js';
|
|
15
|
+
import {buildCustomThemeStyleTag, buildOverrideStyleTag, getThemeTokenMap, listThemeIds, loadThemeConfig, resolveThemeClasses} from './themeSettings.js';
|
|
16
16
|
import {getSearchSettings} from './search.js';
|
|
17
17
|
|
|
18
18
|
const VALID_LAYOUT_WIDTHS = new Set(['narrow', 'normal', 'wide', 'full']);
|
|
@@ -74,7 +74,7 @@ function buildSearchAssets() {
|
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const THEME_SWITCHER_ASSET_V = '
|
|
77
|
+
const THEME_SWITCHER_ASSET_V = '20260925-ts';
|
|
78
78
|
const ANALYTICS_ASSET_V = '20260922-analytics';
|
|
79
79
|
|
|
80
80
|
/**
|
|
@@ -397,7 +397,7 @@ export async function renderPage(page, opts = {}) {
|
|
|
397
397
|
// Theme overrides land BEFORE content/custom.css: the overrides are
|
|
398
398
|
// generated, custom.css is hand-written, and the hand-written one stays
|
|
399
399
|
// the last word.
|
|
400
|
-
headInjectLate: [injection.headLate, searchAssets.headTag, switcherAssets.headTag, themeView.overrideStyleTag, customCssTag, navbarStyleTag]
|
|
400
|
+
headInjectLate: [injection.headLate, searchAssets.headTag, switcherAssets.headTag, themeView.customThemeStyleTag, themeView.overrideStyleTag, customCssTag, navbarStyleTag]
|
|
401
401
|
.filter(Boolean).join('\n'),
|
|
402
402
|
bodyEndInject: [
|
|
403
403
|
// Draft banner - first, so it is the topmost fixed element and a
|
|
@@ -789,16 +789,28 @@ async function buildThemeView(page = {}) {
|
|
|
789
789
|
|
|
790
790
|
// A page pinned to a theme opts out of the automatic pair - otherwise the
|
|
791
791
|
// pin would silently expire at the day/night boundary.
|
|
792
|
-
|
|
792
|
+
// A copy: `apply` is added below, and the loaded config must not carry it.
|
|
793
|
+
const autoTheme = (!page.theme && cfg.autoTheme?.enabled) ? {...cfg.autoTheme} : null;
|
|
793
794
|
const activeTheme = page.theme
|
|
794
795
|
|| (autoTheme ? autoTheme.dayTheme : cfg.theme)
|
|
795
796
|
|| 'charcoal-dark';
|
|
796
797
|
|
|
797
|
-
// A custom (Theme Roller)
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
|
|
801
|
-
const customThemeClass =
|
|
798
|
+
// A custom theme (a plugin's - the Theme Roller) is not one Domma's engine
|
|
799
|
+
// knows: Domma is given its built-in base, and the custom class rides
|
|
800
|
+
// beside it. It is `dm-custom-<id>`, not `dm-theme-<id>`, because Domma
|
|
801
|
+
// strips every dm-theme-* class it did not put there.
|
|
802
|
+
const {dommaTheme, customClass: customThemeClass} = resolveThemeClasses(activeTheme, cfg);
|
|
803
|
+
|
|
804
|
+
// The browser switches between the day and night themes itself, so it is
|
|
805
|
+
// told how to apply each (base + custom class) rather than just its id.
|
|
806
|
+
if (autoTheme) {
|
|
807
|
+
const day = resolveThemeClasses(autoTheme.dayTheme, cfg);
|
|
808
|
+
const night = resolveThemeClasses(autoTheme.nightTheme, cfg);
|
|
809
|
+
autoTheme.apply = {
|
|
810
|
+
[autoTheme.dayTheme]: {theme: day.dommaTheme, custom: day.customClass},
|
|
811
|
+
[autoTheme.nightTheme]: {theme: night.dommaTheme, custom: night.customClass}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
802
814
|
|
|
803
815
|
const {fontLink, fontOverride} = buildFontVars(cfg.font?.family, cfg.font?.size);
|
|
804
816
|
|
|
@@ -810,6 +822,8 @@ async function buildThemeView(page = {}) {
|
|
|
810
822
|
customThemeClass,
|
|
811
823
|
fontLink,
|
|
812
824
|
fontStyleTag: fontOverride ? `<style>${fontOverride}</style>` : '',
|
|
825
|
+
// Only the custom themes this page can show: its own, and day/night's.
|
|
826
|
+
customThemeStyleTag: buildCustomThemeStyleTag([activeTheme, autoTheme?.dayTheme, autoTheme?.nightTheme]),
|
|
813
827
|
overrideStyleTag: buildOverrideStyleTag(cfg)
|
|
814
828
|
};
|
|
815
829
|
}
|
|
@@ -1008,7 +1022,7 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
|
|
|
1008
1022
|
// Theme overrides land BEFORE content/custom.css: the overrides are
|
|
1009
1023
|
// generated, custom.css is hand-written, and the hand-written one stays
|
|
1010
1024
|
// the last word.
|
|
1011
|
-
headInjectLate: [injection.headLate, searchAssets.headTag, switcherAssets.headTag, themeView.overrideStyleTag, customCssTag, navbarStyleTag]
|
|
1025
|
+
headInjectLate: [injection.headLate, searchAssets.headTag, switcherAssets.headTag, themeView.customThemeStyleTag, themeView.overrideStyleTag, customCssTag, navbarStyleTag]
|
|
1012
1026
|
.filter(Boolean).join('\n'),
|
|
1013
1027
|
bodyEndInject: [
|
|
1014
1028
|
overlayMenus,
|
|
@@ -77,6 +77,8 @@ export function defaultThemeConfig() {
|
|
|
77
77
|
theme: DEFAULT_THEME,
|
|
78
78
|
adminTheme: DEFAULT_THEME,
|
|
79
79
|
baseTheme: null,
|
|
80
|
+
// Base of each custom theme the config uses - see rememberCustomBases().
|
|
81
|
+
customBases: {},
|
|
80
82
|
autoTheme: {
|
|
81
83
|
enabled: false,
|
|
82
84
|
dayTheme: 'charcoal-light',
|
|
@@ -174,6 +176,15 @@ export function listThemeTokens() {
|
|
|
174
176
|
*/
|
|
175
177
|
export function getThemeDeclarations(themeId) {
|
|
176
178
|
if (!themeId || !/^[a-z0-9-]+$/.test(themeId)) return null;
|
|
179
|
+
// A custom theme is its base with some tokens changed - adopted onto a
|
|
180
|
+
// region, it has to carry the whole set, not just the changes.
|
|
181
|
+
if (!listThemeIds().includes(themeId)) {
|
|
182
|
+
const custom = getCustomTheme(themeId);
|
|
183
|
+
if (!custom) return null;
|
|
184
|
+
const base = getThemeDeclarations(custom.baseTheme) || '';
|
|
185
|
+
const changed = Object.entries(custom.tokens).map(([k, v]) => `${k}: ${v};`).join('\n');
|
|
186
|
+
return [base, changed].filter(Boolean).join('\n');
|
|
187
|
+
}
|
|
177
188
|
const css = readThemeStylesheet();
|
|
178
189
|
// Anchored so `.dm-theme-x .child {}` descendant rules cannot be mistaken
|
|
179
190
|
// for the theme's own block, and tolerant of the whitespace a rebuild or a
|
|
@@ -214,6 +225,144 @@ export function getThemeTokenMap(themeId) {
|
|
|
214
225
|
return tokens;
|
|
215
226
|
}
|
|
216
227
|
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Custom themes (plugins - the Theme Roller)
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
//
|
|
232
|
+
// A custom theme is a built-in base plus the tokens it changes. It is not a
|
|
233
|
+
// class Domma's engine knows - `Domma.theme.set()` refuses an unknown id and
|
|
234
|
+
// strips every `dm-theme-*` class it did not put there - so it is applied as
|
|
235
|
+
// TWO classes: the base's `dm-theme-<base>`, which Domma owns, and
|
|
236
|
+
// `dm-custom-<id>`, which it leaves alone. The custom class's rule carries only
|
|
237
|
+
// the changed tokens and is emitted after Domma's theme stylesheet, so it wins
|
|
238
|
+
// on source order at equal specificity.
|
|
239
|
+
//
|
|
240
|
+
// Providers are functions, asked on every read: a plugin's themes change while
|
|
241
|
+
// the site runs (save, rename, delete), and a registry snapshot would go stale.
|
|
242
|
+
// A provider must be cheap and synchronous - the render path calls it.
|
|
243
|
+
|
|
244
|
+
const CUSTOM_ID_RE = /^[a-z0-9][a-z0-9-]{0,59}$/;
|
|
245
|
+
const _themeProviders = new Map();
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Register a source of custom themes. One per owner (a plugin's name); a second
|
|
249
|
+
* registration replaces the first, so a plugin that re-registers on reload does
|
|
250
|
+
* not double its themes.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} owner
|
|
253
|
+
* @param {() => Array<{id: string, label?: string, baseTheme: string, tokens: Object<string,string>}>} provider
|
|
254
|
+
*/
|
|
255
|
+
export function registerThemeProvider(owner, provider) {
|
|
256
|
+
if (typeof owner !== 'string' || !owner) throw new Error('registerThemeProvider: owner is required');
|
|
257
|
+
if (typeof provider !== 'function') throw new Error('registerThemeProvider: provider must be a function');
|
|
258
|
+
_themeProviders.set(owner, provider);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** @param {string} owner */
|
|
262
|
+
export function unregisterThemeProvider(owner) {
|
|
263
|
+
_themeProviders.delete(owner);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The class a custom theme adds beside its base's `dm-theme-<base>`. */
|
|
267
|
+
export function customThemeClass(id) {
|
|
268
|
+
return `dm-custom-${id}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Every custom theme the providers currently offer, cleaned: an id that is not
|
|
273
|
+
* a slug, that shadows a built-in theme or that another provider already took
|
|
274
|
+
* is dropped; an unknown base falls back to the default; only real `--dm-*`
|
|
275
|
+
* tokens with plain values survive.
|
|
276
|
+
*
|
|
277
|
+
* @returns {Array<{id: string, label: string, baseTheme: string, tokens: Object<string,string>, owner: string}>}
|
|
278
|
+
*/
|
|
279
|
+
export function listCustomThemes() {
|
|
280
|
+
const builtins = new Set(listThemeIds());
|
|
281
|
+
const out = [];
|
|
282
|
+
const seen = new Set();
|
|
283
|
+
for (const [owner, provider] of _themeProviders) {
|
|
284
|
+
let list;
|
|
285
|
+
try { list = provider(); } catch { continue; }
|
|
286
|
+
for (const t of Array.isArray(list) ? list : []) {
|
|
287
|
+
const id = typeof t?.id === 'string' ? t.id.trim().toLowerCase() : '';
|
|
288
|
+
if (!CUSTOM_ID_RE.test(id) || builtins.has(id) || seen.has(id)) continue;
|
|
289
|
+
const tokens = {};
|
|
290
|
+
for (const [k, v] of Object.entries(t.tokens || {})) {
|
|
291
|
+
if (TOKEN_RE.test(k) && typeof v === 'string' && v.trim() && isSafeTokenValue(v.trim())) tokens[k] = v.trim();
|
|
292
|
+
}
|
|
293
|
+
seen.add(id);
|
|
294
|
+
out.push({
|
|
295
|
+
id,
|
|
296
|
+
label: String(t.label || id).slice(0, 80),
|
|
297
|
+
baseTheme: builtins.has(t.baseTheme) ? t.baseTheme : DEFAULT_THEME,
|
|
298
|
+
tokens,
|
|
299
|
+
owner
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** @param {string} id @returns {object|null} */
|
|
307
|
+
export function getCustomTheme(id) {
|
|
308
|
+
return listCustomThemes().find((t) => t.id === id) || null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* How to apply a theme id: the built-in Domma should be told, and the extra
|
|
313
|
+
* class a custom theme adds. A custom theme whose provider has gone (plugin
|
|
314
|
+
* disabled, licence lapsed) falls back to the base remembered when it was
|
|
315
|
+
* chosen - the page keeps its base look rather than breaking.
|
|
316
|
+
*
|
|
317
|
+
* @param {string} id
|
|
318
|
+
* @param {object} [cfg] - a loaded theme config, for `customBases`
|
|
319
|
+
* @returns {{dommaTheme: string, customClass: string, custom: boolean}}
|
|
320
|
+
*/
|
|
321
|
+
export function resolveThemeClasses(id, cfg = {}) {
|
|
322
|
+
if (!id) return {dommaTheme: DEFAULT_THEME, customClass: '', custom: false};
|
|
323
|
+
const builtins = listThemeIds();
|
|
324
|
+
if (builtins.includes(id)) return {dommaTheme: id, customClass: '', custom: false};
|
|
325
|
+
const custom = getCustomTheme(id);
|
|
326
|
+
if (custom) return {dommaTheme: custom.baseTheme, customClass: customThemeClass(id), custom: true};
|
|
327
|
+
const remembered = cfg?.customBases?.[id];
|
|
328
|
+
if (remembered && builtins.includes(remembered)) return {dommaTheme: remembered, customClass: customThemeClass(id), custom: true};
|
|
329
|
+
// The pre-provider Theme Roller stored its base as `baseTheme` beside a custom `theme`.
|
|
330
|
+
if (id === cfg?.theme && cfg?.baseTheme && builtins.includes(cfg.baseTheme)) {
|
|
331
|
+
return {dommaTheme: cfg.baseTheme, customClass: customThemeClass(id), custom: true};
|
|
332
|
+
}
|
|
333
|
+
// Anything else goes to Domma as it always did - it knows themes the
|
|
334
|
+
// stylesheet does not declare (core-light), and falls back on its own.
|
|
335
|
+
return {dommaTheme: id, customClass: '', custom: false};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The stylesheet for custom themes: one `.dm-custom-<id>` rule of the tokens
|
|
340
|
+
* each changes. Emitted after Domma's theme stylesheet (headInjectLate).
|
|
341
|
+
*
|
|
342
|
+
* @param {string[]|null} [ids] - only these (the ones in use); all when null
|
|
343
|
+
* @returns {string} CSS, or '' when there are none
|
|
344
|
+
*/
|
|
345
|
+
export function compileCustomThemeCss(ids = null) {
|
|
346
|
+
const wanted = ids ? new Set(ids.filter(Boolean)) : null;
|
|
347
|
+
return listCustomThemes()
|
|
348
|
+
.filter((t) => Object.keys(t.tokens).length && (!wanted || wanted.has(t.id)))
|
|
349
|
+
.map((t) => `/* ${t.label.replace(/[*/<>]/g, '')} (${t.baseTheme}) */\n.${customThemeClass(t.id)} {\n`
|
|
350
|
+
+ Object.entries(t.tokens).map(([k, v]) => `${k}: ${v};`).join('\n') + '\n}')
|
|
351
|
+
.join('\n\n');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The page-head `<style>` for the custom themes a page can show, or ''. Only
|
|
356
|
+
* those: every theme a Roller keeps is ~250 tokens, and an unused draft's
|
|
357
|
+
* name has no business in public page source.
|
|
358
|
+
*
|
|
359
|
+
* @param {string[]} [ids] - the page's theme, day and night; all when omitted
|
|
360
|
+
*/
|
|
361
|
+
export function buildCustomThemeStyleTag(ids = null) {
|
|
362
|
+
const css = compileCustomThemeCss(ids);
|
|
363
|
+
return css ? `<style id="dm-custom-themes">\n${css.replace(/<\/style>/gi, '<\\/style>')}\n</style>` : '';
|
|
364
|
+
}
|
|
365
|
+
|
|
217
366
|
// ---------------------------------------------------------------------------
|
|
218
367
|
// Load / save / migrate
|
|
219
368
|
// ---------------------------------------------------------------------------
|
|
@@ -300,14 +449,15 @@ export function loadThemeConfigSync() {
|
|
|
300
449
|
* @returns {Promise<object>} The stored config.
|
|
301
450
|
*/
|
|
302
451
|
export async function saveThemeConfig(input) {
|
|
303
|
-
const
|
|
452
|
+
const stored = await loadThemeConfig();
|
|
453
|
+
const errors = validateThemeConfig(input, {remembered: rememberedIds(stored)});
|
|
304
454
|
if (errors.length) {
|
|
305
455
|
const err = new Error(errors.join('; '));
|
|
306
456
|
err.validation = errors;
|
|
307
457
|
throw err;
|
|
308
458
|
}
|
|
309
459
|
|
|
310
|
-
const current =
|
|
460
|
+
const current = stored;
|
|
311
461
|
const cfg = mergeDefaults({
|
|
312
462
|
...current,
|
|
313
463
|
...input,
|
|
@@ -319,6 +469,7 @@ export async function saveThemeConfig(input) {
|
|
|
319
469
|
overrides: Array.isArray(input.overrides) ? input.overrides.map(normaliseOverride) : current.overrides
|
|
320
470
|
});
|
|
321
471
|
cfg.version = 1;
|
|
472
|
+
rememberCustomBases(cfg);
|
|
322
473
|
|
|
323
474
|
await fs.mkdir(CONFIG_DIR, {recursive: true});
|
|
324
475
|
await fs.writeFile(THEME_FILE, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
@@ -327,6 +478,43 @@ export async function saveThemeConfig(input) {
|
|
|
327
478
|
return cfg;
|
|
328
479
|
}
|
|
329
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Record the base of every custom theme the config uses, so a page keeps that
|
|
483
|
+
* base if the theme's plugin goes away (see resolveThemeClasses). `baseTheme`
|
|
484
|
+
* keeps its old meaning - the base of the site theme when that is custom - for
|
|
485
|
+
* the site.json mirror's readers.
|
|
486
|
+
*
|
|
487
|
+
* @param {object} cfg - mutated
|
|
488
|
+
*/
|
|
489
|
+
function rememberCustomBases(cfg) {
|
|
490
|
+
const custom = new Map(listCustomThemes().map((t) => [t.id, t.baseTheme]));
|
|
491
|
+
const builtins = listThemeIds();
|
|
492
|
+
// A pre-provider Theme Roller config: `baseTheme` beside a custom `theme`,
|
|
493
|
+
// no customBases yet. Keep it, or the first save flips the site to default.
|
|
494
|
+
const legacy = {};
|
|
495
|
+
if (cfg.theme && cfg.baseTheme && !builtins.includes(cfg.theme) && builtins.includes(cfg.baseTheme)) {
|
|
496
|
+
legacy[cfg.theme] = cfg.baseTheme;
|
|
497
|
+
}
|
|
498
|
+
const used = [cfg.theme, cfg.adminTheme, cfg.autoTheme?.dayTheme, cfg.autoTheme?.nightTheme,
|
|
499
|
+
...(cfg.overrides || []).map((o) => o?.theme)];
|
|
500
|
+
const bases = {};
|
|
501
|
+
for (const id of used) {
|
|
502
|
+
if (!id) continue;
|
|
503
|
+
if (custom.has(id)) bases[id] = custom.get(id);
|
|
504
|
+
else if (cfg.customBases?.[id]) bases[id] = cfg.customBases[id]; // provider away: keep what we knew
|
|
505
|
+
else if (legacy[id]) bases[id] = legacy[id];
|
|
506
|
+
}
|
|
507
|
+
cfg.customBases = bases;
|
|
508
|
+
cfg.baseTheme = bases[cfg.theme] || null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Custom theme ids a stored config already uses and remembers the base of. */
|
|
512
|
+
export function rememberedIds(cfg) {
|
|
513
|
+
const ids = Object.keys(cfg?.customBases || {});
|
|
514
|
+
if (cfg?.theme && cfg?.baseTheme && !listThemeIds().includes(cfg.theme)) ids.push(cfg.theme);
|
|
515
|
+
return ids;
|
|
516
|
+
}
|
|
517
|
+
|
|
330
518
|
/**
|
|
331
519
|
* Rewrite the legacy site.json keys so third parties that still read them see
|
|
332
520
|
* the current values. One-way: nothing reads these back.
|
|
@@ -415,7 +603,9 @@ function normaliseOverride(o) {
|
|
|
415
603
|
*/
|
|
416
604
|
function isSafeTokenValue(value) {
|
|
417
605
|
if (value.length > 200) return false;
|
|
418
|
-
|
|
606
|
+
// A backslash can spell `url(` as `u\72l(`, and `/*` opens a comment that
|
|
607
|
+
// swallows the next declaration - neither has a legitimate use in a token.
|
|
608
|
+
if (/[{}<>;\\]|\/\*|\*\//.test(value)) return false;
|
|
419
609
|
return !/@import|javascript:|expression\s*\(|url\s*\(/i.test(value);
|
|
420
610
|
}
|
|
421
611
|
|
|
@@ -426,23 +616,33 @@ function isSafeTokenValue(value) {
|
|
|
426
616
|
* @param {object} input
|
|
427
617
|
* @returns {string[]}
|
|
428
618
|
*/
|
|
429
|
-
export function validateThemeConfig(input) {
|
|
619
|
+
export function validateThemeConfig(input, {remembered = []} = {}) {
|
|
430
620
|
const errors = [];
|
|
431
621
|
if (!input || typeof input !== 'object') return ['Theme config must be an object'];
|
|
432
622
|
|
|
433
|
-
const
|
|
434
|
-
|
|
623
|
+
const builtins = listThemeIds();
|
|
624
|
+
// Custom themes (a plugin's) are chosen like any other - except by the
|
|
625
|
+
// public switcher below, which switches through Domma's engine. One the
|
|
626
|
+
// config already uses stays valid after its plugin goes (`remembered` -
|
|
627
|
+
// its customBases): otherwise nothing else on the Theme screen could be
|
|
628
|
+
// saved until every use of it had been re-picked.
|
|
629
|
+
const known = builtins.length
|
|
630
|
+
? [...builtins, ...listCustomThemes().map((t) => t.id), ...remembered]
|
|
631
|
+
: [];
|
|
632
|
+
const checkTheme = (value, label, allowed = known) => {
|
|
435
633
|
if (value === undefined || value === null || value === '') return;
|
|
436
634
|
if (typeof value !== 'string' || !/^[a-z0-9-]+$/.test(value)) {
|
|
437
635
|
errors.push(`${label} is not a valid theme id`);
|
|
438
|
-
} else if (
|
|
439
|
-
errors.push(
|
|
636
|
+
} else if (allowed.length && !allowed.includes(value)) {
|
|
637
|
+
errors.push(allowed === builtins && known.includes(value)
|
|
638
|
+
? `${label}: "${value}" is a custom theme, which the public theme switcher cannot offer`
|
|
639
|
+
: `${label}: unknown theme "${value}"`);
|
|
440
640
|
}
|
|
441
641
|
};
|
|
442
642
|
|
|
443
643
|
checkTheme(input.theme, 'Front-end theme');
|
|
444
644
|
checkTheme(input.adminTheme, 'Admin theme');
|
|
445
|
-
if (input.baseTheme) checkTheme(input.baseTheme, 'Base theme');
|
|
645
|
+
if (input.baseTheme) checkTheme(input.baseTheme, 'Base theme', builtins);
|
|
446
646
|
if (input.autoTheme) {
|
|
447
647
|
checkTheme(input.autoTheme.dayTheme, 'Day theme');
|
|
448
648
|
checkTheme(input.autoTheme.nightTheme, 'Night theme');
|
|
@@ -470,7 +670,7 @@ export function validateThemeConfig(input) {
|
|
|
470
670
|
if (!Array.isArray(sw.themes)) {
|
|
471
671
|
errors.push('switcher.themes must be an array');
|
|
472
672
|
} else {
|
|
473
|
-
for (const id of sw.themes) checkTheme(id, 'switcher.themes');
|
|
673
|
+
for (const id of sw.themes) checkTheme(id, 'switcher.themes', builtins);
|
|
474
674
|
}
|
|
475
675
|
}
|
|
476
676
|
}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
<!-- Late head injection - custom CSS always loads last so it can override everything -->
|
|
40
40
|
{{headInjectLate}}
|
|
41
41
|
</head>
|
|
42
|
-
<body class="dm-cloaked dm-theme-{{
|
|
42
|
+
<body class="dm-cloaked dm-theme-{{dommaTheme}} {{customThemeClass}} {{layoutBodyClass}}" data-layout="{{layout}}">
|
|
43
43
|
|
|
44
44
|
{{#if showNavbar}}
|
|
45
45
|
<nav id="site-navbar" class="{{navbarClass}}"></nav>
|
|
@@ -109,7 +109,9 @@
|
|
|
109
109
|
window.Domma.init();
|
|
110
110
|
}
|
|
111
111
|
if (window.Domma && window.Domma.theme) {
|
|
112
|
-
|
|
112
|
+
// The built-in theme Domma knows; a custom theme's own class
|
|
113
|
+
// (dm-custom-<id>) is on <body> already and Domma leaves it alone.
|
|
114
|
+
window.Domma.theme.init({ theme: '{{dommaTheme}}', persist: false });
|
|
113
115
|
}
|
|
114
116
|
window.__CMS_NAV__ = {{navJson}};
|
|
115
117
|
window.__CMS_SITE__ = {{siteJson}};
|
|
@@ -121,13 +123,20 @@
|
|
|
121
123
|
var n = new Date(), m = n.getHours() * 60 + n.getMinutes(), ds = (c.dayStart || "07:00").split(":"),
|
|
122
124
|
ns = (c.nightStart || "19:00").split(":"), d = +ds[0] * 60 + (+ds[1] || 0),
|
|
123
125
|
e = +ns[0] * 60 + (+ns[1] || 0), t = (m >= d && m < e) ? c.dayTheme : c.nightTheme;
|
|
124
|
-
|
|
126
|
+
// A custom theme is a base plus its own class (see resolveThemeClasses).
|
|
127
|
+
var a = (c.apply && c.apply[t]) || { theme: t, custom: '' };
|
|
128
|
+
if (window.Domma && window.Domma.theme) window.Domma.theme.set(a.theme);
|
|
129
|
+
var b = document.body;
|
|
130
|
+
if (b) {
|
|
131
|
+
b.className = b.className.split(' ').filter(function (k) { return k.indexOf('dm-custom-') !== 0; }).join(' ');
|
|
132
|
+
if (a.custom) b.classList.add(a.custom);
|
|
133
|
+
}
|
|
125
134
|
}());
|
|
126
135
|
{{dconfigScript}}
|
|
127
136
|
</script>
|
|
128
137
|
|
|
129
138
|
<!-- Site initialisation -->
|
|
130
|
-
<script src="/public/js/site.js?v=
|
|
139
|
+
<script src="/public/js/site.js?v=20260925-custom-themes" type="module"></script>
|
|
131
140
|
<script src="/public/js/collection-context.js?v=20260919-ctx-tokens" type="module"></script>
|
|
132
141
|
{{ctxMenusModule}}
|
|
133
142
|
|