domma-cms 0.54.2 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/CLAUDE.md +151 -77
  2. package/README.md +4 -4
  3. package/admin/css/admin.css +1 -1
  4. package/admin/dist/domma/domma-tools.css +3 -3
  5. package/admin/dist/domma/domma-tools.min.js +3 -3
  6. package/admin/js/api.js +1 -1
  7. package/admin/js/app.js +3 -3
  8. package/admin/js/lib/page-picker.js +1 -0
  9. package/admin/js/lib/plugin-accent.js +1 -0
  10. package/admin/js/lib/plugin-chrome.js +1 -0
  11. package/admin/js/lib/shortcode-context-menu.js +2 -2
  12. package/admin/js/lib/sidebar-grouping.js +1 -1
  13. package/admin/js/lib/sidebar-grouping.test.js +1 -1
  14. package/admin/js/lib/sidebar-renderer.js +4 -4
  15. package/admin/js/lib/slideover-resizable.js +1 -0
  16. package/admin/js/templates/context-menu-editor.html +212 -0
  17. package/admin/js/templates/context-menus.html +16 -0
  18. package/admin/js/templates/menu-editor.html +19 -25
  19. package/admin/js/templates/plugin-code.html +1 -1
  20. package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
  21. package/admin/js/templates/settings.html +27 -97
  22. package/admin/js/templates/theme.html +173 -0
  23. package/admin/js/views/context-menu-editor.js +55 -0
  24. package/admin/js/views/context-menus.js +5 -0
  25. package/admin/js/views/form-editor.js +7 -7
  26. package/admin/js/views/index.js +1 -1
  27. package/admin/js/views/menu-editor.js +13 -13
  28. package/admin/js/views/plugin-marketplace.js +1 -1
  29. package/admin/js/views/plugins.js +25 -23
  30. package/admin/js/views/search.js +1 -0
  31. package/admin/js/views/settings.js +3 -3
  32. package/admin/js/views/theme.js +1 -0
  33. package/bin/cli.js +11 -2
  34. package/bin/lib/smtp-defaults.js +53 -0
  35. package/bin/update.js +13 -2
  36. package/config/menus/admin-sidebar.json +129 -23
  37. package/config/plugins.json +5 -5
  38. package/config/search.json +13 -0
  39. package/config/theme.json +18 -0
  40. package/package.json +12 -4
  41. package/plugins/_lib/admin/mail/compose-window.js +914 -0
  42. package/plugins/_lib/admin/mail/contacts.js +301 -0
  43. package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
  44. package/plugins/_lib/admin/mail/folder-tree.js +254 -0
  45. package/plugins/_lib/admin/mail/identity.js +480 -0
  46. package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
  47. package/plugins/_lib/admin/mail/keyboard.js +136 -0
  48. package/plugins/_lib/admin/mail/mail.css +1 -0
  49. package/plugins/_lib/admin/mail/mail.html +71 -0
  50. package/plugins/_lib/admin/mail/panes.js +253 -0
  51. package/plugins/_lib/admin/mail/reader-view.js +4453 -0
  52. package/plugins/_lib/admin/mail/resizable.js +26 -0
  53. package/plugins/_lib/admin/mail/rules.js +343 -0
  54. package/plugins/_lib/admin/mail/scheduling.js +203 -0
  55. package/plugins/_lib/admin/mail/section-kit.js +277 -0
  56. package/plugins/_lib/admin/mail/templates.js +238 -0
  57. package/plugins/_lib/admin/mail/threads.js +200 -0
  58. package/plugins/_lib/admin/mail/vacation.js +269 -0
  59. package/plugins/_lib/admin/ui/help.css +1 -0
  60. package/plugins/_lib/admin/ui/help.js +174 -0
  61. package/plugins/_lib/admin/ui/resizable.js +151 -0
  62. package/plugins/_lib/dataStore.js +117 -0
  63. package/plugins/_lib/mail/accounts.js +919 -0
  64. package/plugins/_lib/mail/bodyTokens.js +101 -0
  65. package/plugins/_lib/mail/compose.js +256 -0
  66. package/plugins/_lib/mail/defaults.js +57 -0
  67. package/plugins/_lib/mail/diagnostics.js +154 -0
  68. package/plugins/_lib/mail/envelope.js +161 -0
  69. package/plugins/_lib/mail/folders.js +192 -0
  70. package/plugins/_lib/mail/handoff.js +274 -0
  71. package/plugins/_lib/mail/imapPool.js +293 -0
  72. package/plugins/_lib/mail/mbox.js +74 -0
  73. package/plugins/_lib/mail/pollSchedule.js +79 -0
  74. package/plugins/_lib/mail/poller.js +135 -0
  75. package/plugins/_lib/mail/priority.js +138 -0
  76. package/plugins/_lib/mail/readRoutes.js +680 -0
  77. package/plugins/_lib/mail/render.js +291 -0
  78. package/plugins/_lib/mail/ruleRunner.js +152 -0
  79. package/plugins/_lib/mail/scheduler.js +254 -0
  80. package/plugins/_lib/mail/secretbox.js +229 -0
  81. package/plugins/_lib/mail/send.js +396 -0
  82. package/plugins/_lib/mail/store.js +1002 -0
  83. package/plugins/_lib/mail/sync.js +292 -0
  84. package/plugins/_lib/mail/syncPlan.js +126 -0
  85. package/plugins/_lib/mail/syncSelection.js +82 -0
  86. package/plugins/_lib/mail/unsubscribe.js +183 -0
  87. package/plugins/_lib/mail/vacationRunner.js +114 -0
  88. package/plugins/_lib/mail/write.js +473 -0
  89. package/plugins/_lib/schemaSync.js +83 -0
  90. package/plugins/_template/admin/css/index.css +0 -0
  91. package/plugins/_template/admin/templates/index.html +4 -4
  92. package/plugins/_template/admin/views/index.js +7 -0
  93. package/plugins/analytics/admin/css/index.css +1 -0
  94. package/plugins/analytics/admin/templates/analytics.html +22 -13
  95. package/plugins/analytics/plugin.json +3 -0
  96. package/plugins/blog/admin/css/index.css +1 -0
  97. package/plugins/blog/admin/templates/blog.html +30 -18
  98. package/plugins/blog/admin/templates/categories.html +2 -2
  99. package/plugins/blog/admin/templates/comments.html +2 -2
  100. package/plugins/blog/admin/templates/post-editor.html +34 -34
  101. package/plugins/blog/admin/templates/settings.html +6 -3
  102. package/plugins/blog/admin/views/blog.js +8 -5
  103. package/plugins/blog/admin/views/categories.js +5 -10
  104. package/plugins/blog/admin/views/comments.js +5 -5
  105. package/plugins/blog/admin/views/post-editor.js +39 -20
  106. package/plugins/blog/admin/views/settings.js +52 -50
  107. package/plugins/blog/collections/categories/schema.json +7 -6
  108. package/plugins/blog/collections/comments/schema.json +11 -10
  109. package/plugins/blog/collections/posts/schema.json +14 -13
  110. package/plugins/blog/plugin.js +36 -13
  111. package/plugins/blog/plugin.json +13 -5
  112. package/plugins/blog/plugin.public.js +312 -0
  113. package/plugins/contacts/admin/css/index.css +1 -0
  114. package/plugins/contacts/admin/templates/contacts.html +128 -0
  115. package/plugins/contacts/admin/views/contacts.js +237 -4
  116. package/plugins/contacts/collections/user-contacts/schema.json +108 -0
  117. package/plugins/contacts/plugin.js +214 -27
  118. package/plugins/contacts/plugin.json +4 -1
  119. package/plugins/invoice/admin/css/index.css +1 -0
  120. package/plugins/invoice/admin/templates/editor.html +140 -49
  121. package/plugins/invoice/admin/templates/index.html +153 -23
  122. package/plugins/invoice/admin/templates/issuers.html +2 -5
  123. package/plugins/invoice/admin/templates/receivers.html +2 -5
  124. package/plugins/invoice/admin/views/contacts-source.js +266 -0
  125. package/plugins/invoice/admin/views/editor.js +366 -199
  126. package/plugins/invoice/admin/views/export.js +199 -0
  127. package/plugins/invoice/admin/views/help-content.js +61 -0
  128. package/plugins/invoice/admin/views/index.js +582 -94
  129. package/plugins/invoice/admin/views/issuers.js +24 -17
  130. package/plugins/invoice/admin/views/media.js +172 -0
  131. package/plugins/invoice/admin/views/party-view.js +305 -67
  132. package/plugins/invoice/admin/views/payments.js +127 -0
  133. package/plugins/invoice/admin/views/print.js +130 -0
  134. package/plugins/invoice/admin/views/receivers.js +49 -16
  135. package/plugins/invoice/admin/views/send.js +212 -0
  136. package/plugins/invoice/admin/views/settings.js +594 -0
  137. package/plugins/invoice/admin/views/view-lifecycle.js +33 -0
  138. package/plugins/invoice/collections/invoice-issuers/schema.json +77 -11
  139. package/plugins/invoice/collections/invoice-receivers/schema.json +10 -9
  140. package/plugins/invoice/collections/invoices/schema.json +19 -13
  141. package/plugins/invoice/config.js +27 -6
  142. package/plugins/invoice/pdf.js +164 -0
  143. package/plugins/invoice/plugin.js +1217 -44
  144. package/plugins/invoice/plugin.json +10 -9
  145. package/plugins/invoice/templates/_base.css +1 -0
  146. package/plugins/invoice/templates/classic-nologo.html +100 -0
  147. package/plugins/invoice/templates/classic.html +91 -0
  148. package/plugins/invoice/templates/invoice-print.html +24 -0
  149. package/plugins/invoice/templates/minimal.html +99 -0
  150. package/plugins/invoice/templates/modern-nologo.html +114 -0
  151. package/plugins/invoice/templates/modern.html +113 -0
  152. package/plugins/invoice/templates/templates.json +11 -0
  153. package/plugins/mail-reader/admin/views/mail.js +19 -0
  154. package/plugins/mail-reader/config.js +7 -0
  155. package/plugins/mail-reader/plugin.js +48 -0
  156. package/plugins/mail-reader/plugin.json +33 -0
  157. package/plugins/notes/admin/views/notes.js +1 -1
  158. package/plugins/notes/plugin.json +2 -2
  159. package/plugins/surveys/lib/audience.js +37 -0
  160. package/plugins/surveys/lib/campaigns.js +43 -0
  161. package/plugins/surveys/lib/ledger.js +110 -0
  162. package/plugins/surveys/lib/sending.js +106 -0
  163. package/plugins/surveys/lib/stats.js +62 -0
  164. package/plugins/surveys/lib/submit.js +95 -0
  165. package/plugins/surveys/lib/tokens.js +28 -0
  166. package/plugins/surveys/plugin.public.js +149 -0
  167. package/plugins/theme-switcher/admin/templates/theme-switcher.html +1 -1
  168. package/public/css/forms.css +1 -1
  169. package/public/css/menu-highlight.css +1 -1
  170. package/public/css/search.css +1 -0
  171. package/public/css/site.css +1 -1
  172. package/public/js/collection-context.js +2 -2
  173. package/public/js/context-menus.js +1 -0
  174. package/public/js/form-logic-engine.js +1 -1
  175. package/public/js/forms.js +2 -2
  176. package/public/js/menu-decor.mjs +1 -1
  177. package/public/js/search.js +1 -0
  178. package/public/js/site.js +1 -1
  179. package/scripts/build.js +37 -3
  180. package/scripts/copy-domma.js +48 -0
  181. package/scripts/seed.js +1996 -0
  182. package/scripts/setup.js +8 -0
  183. package/server/routes/api/collections.js +34 -0
  184. package/server/routes/api/context-menus.js +104 -0
  185. package/server/routes/api/forms.js +42 -3
  186. package/server/routes/api/notifications.js +69 -19
  187. package/server/routes/api/plugins.js +50 -6
  188. package/server/routes/api/search.js +43 -0
  189. package/server/routes/api/theme.js +69 -0
  190. package/server/routes/public.js +42 -7
  191. package/server/server.js +74 -0
  192. package/server/services/adapters/FileAdapter.js +6 -1
  193. package/server/services/content.js +26 -0
  194. package/server/services/contextMenus.js +477 -0
  195. package/server/services/email.js +29 -3
  196. package/server/services/health.js +23 -2
  197. package/server/services/markdown.js +70 -9
  198. package/server/services/menuRender.js +28 -4
  199. package/server/services/menus.js +25 -1
  200. package/server/services/permissionRegistry.js +24 -0
  201. package/server/services/pluginFiles.js +52 -11
  202. package/server/services/plugins.js +229 -6
  203. package/server/services/renderer.js +148 -22
  204. package/server/services/roles.js +1 -1
  205. package/server/services/search-migration.js +82 -0
  206. package/server/services/search.js +413 -0
  207. package/server/services/sidebar-migration.js +1 -0
  208. package/server/services/themeSettings.js +541 -0
  209. package/server/services/users.js +8 -0
  210. package/server/templates/page.html +4 -2
  211. package/plugins/contacts/data/contacts.json +0 -20
  212. package/plugins/notes/data/notes.json +0 -1
  213. package/plugins/site-search/admin/views/site-search.js +0 -116
  214. package/plugins/site-search/config.js +0 -15
  215. package/plugins/site-search/plugin.js +0 -188
  216. package/plugins/site-search/plugin.json +0 -40
  217. package/plugins/site-search/public/inject-body.html +0 -17
  218. package/plugins/site-search/public/inject-head.html +0 -1
  219. package/plugins/site-search/public/search.css +0 -1
  220. package/plugins/site-search/public/search.js +0 -1
  221. package/plugins/todo/data/todos.json +0 -1
@@ -1,5 +1,5 @@
1
- import{api as _}from"../api.js";import{colourToCss as bt}from"/public/js/menu-decor.mjs?v=20260905-menu-highlight";import{openIconPicker as gt}from"../lib/card-builder.js";import{applyDrop as vt}from"../lib/menu-tree.js";let yt=1;const D=()=>"i_"+yt++,wt=760;function lt(t,d=null,s=[]){for(const i of t||[]){const n=D();if(i&&i.type==="separator"){s.push({id:n,parentId:d,type:"separator"});continue}if(i&&i.type==="spacer"){s.push({id:n,parentId:d,type:"spacer",size:i.size||"md"});continue}s.push({id:n,parentId:d,text:i.text||"",url:i.url||"",icon:i.icon||"",visibility:i.visibility||"",permission:i.permission||"",hidden:!!i.hidden,bundled:!!i.bundled,badge:i.badge?{...i.badge}:null,pill:i.pill?{...i.pill}:null,colour:i.colour||""}),Array.isArray(i.items)&&i.items.length&&lt(i.items,n,s)}return s}function xt(t){const d=new Map;for(const i of t)d.has(i.parentId)||d.set(i.parentId,[]),d.get(i.parentId).push(i);function s(i){return(d.get(i)||[]).map(n=>{if(n.type==="separator")return{type:"separator"};if(n.type==="spacer")return{type:"spacer",...n.size&&n.size!=="md"&&{size:n.size}};const o=s(n.id);return{text:(n.text||"").trim(),url:(n.url||"").trim(),...n.icon&&{icon:n.icon.trim()},...n.visibility&&{visibility:n.visibility},...n.permission&&{permission:n.permission},...n.hidden&&{hidden:!0},...n.bundled&&{bundled:!0},...n.badge&&(n.badge.text||n.badge.countFrom)?{badge:{...n.badge.text&&{text:n.badge.text},...n.badge.countFrom&&{countFrom:n.badge.countFrom},...n.badge.variant&&{variant:n.badge.variant}}}:{},...n.pill&&n.pill.variant?{pill:{style:n.pill.style||"filled",variant:n.pill.variant}}:{},...n.colour&&{colour:n.colour},...o.length&&{items:o}}})}return s(null)}function h(t){return String(t||"").replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;")}function kt(t,d){let s=0,i=d.find(n=>n.id===t);for(;i&&i.parentId;)s++,i=d.find(n=>n.id===i.parentId);return s}function dt(t,d){const{top:s,height:i}=t.getBoundingClientRect(),n=i?(d-s)/i:.5;return n<.25?"above":n>.75?"below":"into"}const ct=["me-drop-above","me-drop-below","me-drop-into"];function tt(t){t.get(0).querySelectorAll(".menu-tree-row").forEach(d=>d.classList.remove(...ct))}function _t(t,d,s){let i=null;t.get(0).querySelectorAll(".menu-tree-row").forEach(n=>{const o=n.querySelector(".menu-tree-grip");n.draggable=!1,o&&(o.addEventListener("mousedown",()=>{n.draggable=!0}),o.addEventListener("mouseup",()=>{n.draggable=!1})),n.addEventListener("dragstart",r=>{i=n.dataset.id,n.style.opacity="0.45",r.dataTransfer.effectAllowed="move";try{r.dataTransfer.setData("text/plain",i)}catch{}}),n.addEventListener("dragend",()=>{n.style.opacity="",n.draggable=!1,i=null,tt(t)}),n.addEventListener("dragover",r=>{if(!i||i===n.dataset.id)return;r.preventDefault(),r.dataTransfer.dropEffect="move";const y=dt(n,r.clientY);tt(t),n.classList.add(`me-drop-${y}`)}),n.addEventListener("dragleave",()=>n.classList.remove(...ct)),n.addEventListener("drop",r=>{r.preventDefault();const y=n.dataset.id,x=dt(n,r.clientY);if(tt(t),!i)return;const v=vt(d(),i,y,x);i=null,v&&s()})})}function At(t,d,s=()=>""){t.html("");function i(n){for(const o of d.filter(r=>r.parentId===n)){const r=kt(o.id,d);if(o.type==="separator"){t.append(`
2
- <div class="menu-tree-row menu-tree-row--separator" data-id="${o.id}" style="margin-left:${r*28}px">
1
+ import{api as k}from"../api.js";import{colourToCss as xt,HIGHLIGHT_HOVER as rt,HIGHLIGHT_ACTIVE as pt,HIGHLIGHT_HOVER_LABELS as mt,HIGHLIGHT_ACTIVE_LABELS as ft}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{openIconPicker as At}from"../lib/card-builder.js";import{applyDrop as _t}from"../lib/menu-tree.js";let kt=1;const G=()=>"i_"+kt++,It=760;function ut(t,d=null,s=[]){for(const i of t||[]){const n=G();if(i&&i.type==="separator"){s.push({id:n,parentId:d,type:"separator"});continue}if(i&&i.type==="spacer"){s.push({id:n,parentId:d,type:"spacer",size:i.size||"md"});continue}s.push({id:n,parentId:d,text:i.text||"",url:i.url||"",icon:i.icon||"",visibility:i.visibility||"",permission:i.permission||"",hidden:!!i.hidden,bundled:!!i.bundled,badge:i.badge?{...i.badge}:null,pill:i.pill?{...i.pill}:null,colour:i.colour||"",highlight:i.highlight?{...i.highlight}:null}),Array.isArray(i.items)&&i.items.length&&ut(i.items,n,s)}return s}function Ht(t){const d=new Map;for(const i of t)d.has(i.parentId)||d.set(i.parentId,[]),d.get(i.parentId).push(i);function s(i){return(d.get(i)||[]).map(n=>{if(n.type==="separator")return{type:"separator"};if(n.type==="spacer")return{type:"spacer",...n.size&&n.size!=="md"&&{size:n.size}};const o=s(n.id);return{text:(n.text||"").trim(),url:(n.url||"").trim(),...n.icon&&{icon:n.icon.trim()},...n.visibility&&{visibility:n.visibility},...n.permission&&{permission:n.permission},...n.hidden&&{hidden:!0},...n.bundled&&{bundled:!0},...n.badge&&(n.badge.text||n.badge.countFrom)?{badge:{...n.badge.text&&{text:n.badge.text},...n.badge.countFrom&&{countFrom:n.badge.countFrom},...n.badge.variant&&{variant:n.badge.variant}}}:{},...n.pill&&n.pill.variant?{pill:{style:n.pill.style||"filled",variant:n.pill.variant}}:{},...n.colour&&{colour:n.colour},...n.highlight&&(n.highlight.hover||n.highlight.active||n.highlight.accent)?{highlight:{...n.highlight.hover&&{hover:n.highlight.hover},...n.highlight.active&&{active:n.highlight.active},...n.highlight.accent&&{accent:n.highlight.accent}}}:{},...o.length&&{items:o}}})}return s(null)}function h(t){return String(t||"").replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;")}function St(t,d){let s=0,i=d.find(n=>n.id===t);for(;i&&i.parentId;)s++,i=d.find(n=>n.id===i.parentId);return s}function ht(t,d){const{top:s,height:i}=t.getBoundingClientRect(),n=i?(d-s)/i:.5;return n<.25?"above":n>.75?"below":"into"}const gt=["me-drop-above","me-drop-below","me-drop-into"];function st(t){t.get(0).querySelectorAll(".menu-tree-row").forEach(d=>d.classList.remove(...gt))}function Ot(t,d,s){let i=null;t.get(0).querySelectorAll(".menu-tree-row").forEach(n=>{const o=n.querySelector(".menu-tree-grip");n.draggable=!1,o&&(o.addEventListener("mousedown",()=>{n.draggable=!0}),o.addEventListener("mouseup",()=>{n.draggable=!1})),n.addEventListener("dragstart",p=>{i=n.dataset.id,n.style.opacity="0.45",p.dataTransfer.effectAllowed="move";try{p.dataTransfer.setData("text/plain",i)}catch{}}),n.addEventListener("dragend",()=>{n.style.opacity="",n.draggable=!1,i=null,st(t)}),n.addEventListener("dragover",p=>{if(!i||i===n.dataset.id)return;p.preventDefault(),p.dataTransfer.dropEffect="move";const w=ht(n,p.clientY);st(t),n.classList.add(`me-drop-${w}`)}),n.addEventListener("dragleave",()=>n.classList.remove(...gt)),n.addEventListener("drop",p=>{p.preventDefault();const w=n.dataset.id,A=ht(n,p.clientY);if(st(t),!i)return;const y=_t(d(),i,w,A);i=null,y&&s()})})}function jt(t,d,s=()=>""){t.html("");function i(n){for(const o of d.filter(p=>p.parentId===n)){const p=St(o.id,d);if(o.type==="separator"){t.append(`
2
+ <div class="menu-tree-row menu-tree-row--separator" data-id="${o.id}" style="margin-left:${p*28}px">
3
3
  <span class="menu-tree-grip" data-icon="move"></span>
4
4
  <span class="me-sep-line"><span class="me-sep-label">Separator</span></span>
5
5
  <button class="btn btn-sm btn-ghost me-ins-item" data-tooltip="Add item below"><span data-icon="plus"></span></button>
@@ -10,12 +10,12 @@ import{api as _}from"../api.js";import{colourToCss as bt}from"/public/js/menu-de
10
10
  <button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
11
11
  <button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
12
12
  </div>
13
- `);continue}if(o.type==="spacer"){const P=["xs","sm","md","lg","xl","flex"].map(z=>`<option value="${z}"${(o.size||"md")===z?" selected":""}>${z==="flex"?"flexible":z}</option>`).join("");t.append(`
14
- <div class="menu-tree-row menu-tree-row--spacer" data-id="${o.id}" style="margin-left:${r*28}px">
13
+ `);continue}if(o.type==="spacer"){const N=["xs","sm","md","lg","xl","flex"].map(L=>`<option value="${L}"${(o.size||"md")===L?" selected":""}>${L==="flex"?"flexible":L}</option>`).join("");t.append(`
14
+ <div class="menu-tree-row menu-tree-row--spacer" data-id="${o.id}" style="margin-left:${p*28}px">
15
15
  <span class="menu-tree-grip" data-icon="move"></span>
16
16
  <span class="me-spacer-line">
17
17
  <span class="me-spacer-label">Spacer</span>
18
- <select class="form-input me-spacer-size" data-tooltip="Spacer size - flexible fills the leftover room">${P}</select>
18
+ <select class="form-input me-spacer-size" data-tooltip="Spacer size - flexible fills the leftover room">${N}</select>
19
19
  </span>
20
20
  <button class="btn btn-sm btn-ghost me-ins-item" data-tooltip="Add item below"><span data-icon="plus"></span></button>
21
21
  <button class="btn btn-sm btn-ghost me-ins-sep" data-tooltip="Add separator below"><span data-icon="minus"></span></button>
@@ -25,10 +25,10 @@ import{api as _}from"../api.js";import{colourToCss as bt}from"/public/js/menu-de
25
25
  <button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
26
26
  <button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
27
27
  </div>
28
- `);continue}const y=o.badge&&(o.badge.text||o.badge.countFrom)?`<span class="dm-menu-badge">${h(String(o.badge.text||"#"))}</span>`:"",x=bt(o.colour),v=x?` style="color:${h(x)}"`:"";t.append(`
29
- <div class="menu-tree-row menu-tree-row--slim${o.hidden?" menu-tree-row--hidden":""}" data-id="${o.id}" style="margin-left:${r*28}px">
28
+ `);continue}const w=o.badge&&(o.badge.text||o.badge.countFrom)?`<span class="dm-menu-badge">${h(String(o.badge.text||"#"))}</span>`:"",A=xt(o.colour),y=A?` style="color:${h(A)}"`:"";t.append(`
29
+ <div class="menu-tree-row menu-tree-row--slim${o.hidden?" menu-tree-row--hidden":""}" data-id="${o.id}" style="margin-left:${p*28}px">
30
30
  <span class="menu-tree-grip" data-icon="move"></span>
31
- <span class="me-row-label"${v}>${h(o.text||"(untitled)")}${y}</span>
31
+ <span class="me-row-label"${y}>${h(o.text||"(untitled)")}${w}</span>
32
32
  <span class="me-row-url">${h(o.url||"")}</span>
33
33
  <button class="btn btn-sm btn-ghost me-ins-item" data-tooltip="Add item below"><span data-icon="plus"></span></button>
34
34
  <button class="btn btn-sm btn-ghost me-ins-sep" data-tooltip="Add separator below"><span data-icon="minus"></span></button>
@@ -40,8 +40,8 @@ import{api as _}from"../api.js";import{colourToCss as bt}from"/public/js/menu-de
40
40
  <button class="btn btn-sm btn-secondary me-edit" data-tooltip="Edit"><span data-icon="edit"></span></button>
41
41
  <button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
42
42
  </div>
43
- `),i(o.id)}}i(null),Domma.icons.scan(t.get(0)),t.get(0).querySelectorAll("[data-tooltip]").forEach(n=>{E.tooltip(n,{content:n.getAttribute("data-tooltip"),position:"top"})})}function j(t,d){t.find(".menu-tree-row").each(function(){const s=$(this).data("id"),i=d.find(v=>v.id===s);if(!i||i.type==="separator")return;if(i.type==="spacer"){const v=$(this).find(".me-spacer-size");v.length&&(i.size=v.val());return}const n=$(this).find(".me-text");n.length&&(i.text=n.val());const o=$(this).find(".me-url");o.length&&(i.url=o.val());const r=$(this).find(".me-icon");r.length&&(i.icon=r.val());const y=$(this).find(".me-vis");y.length&&(i.visibility=y.val());const x=$(this).find(".me-perm");x.length&&(i.permission=x.val())})}function Lt(t,d){const s={};for(const i of d){const n=t.querySelector(`[name="${i}"]`);n&&(s[i]=n.type==="checkbox"?n.checked:n.value)}return s}function b(t,d={},...s){const i=document.createElement(t);Object.assign(i,d);for(const n of s)n!=null&&i.append(n);return i}function O(t,d,s){const i=b("label",{className:"form-field"},b("span",{className:"form-label",textContent:t}),d);return s&&i.append(b("small",{className:"form-hint",textContent:s,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),i}function St(t,d){const s=[["","- Everyone -"],["public","Public - anyone, incl. logged-out"],...(d||[]).map(n=>[n.name,`${n.label||n.name} and above`]),["private","Private - super-admin only"]];t&&!s.some(([n])=>n===t)&&s.push([t,`${t} (custom)`]);const i=W(s,t||"");return i.name="visibility",i}function W(t,d){const s=b("select",{className:"form-select"});return t.forEach(([i,n])=>s.add(new Option(n,i))),s.value=d??"",s}function et(t,d){const s=typeof d=="string"&&d.startsWith("#"),i=W([["","- none -"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],s?"__custom__":d||""),n=b("input",{type:"color",className:"form-input me-colour-hex",value:s?d:"#000000"});n.style.display=s?"":"none",i.addEventListener("change",()=>{n.style.display=i.value==="__custom__"?"":"none"});const o=O(t,i);return o.append(n),o._read=()=>i.value==="__custom__"?n.value:i.value,o}function It(t,d){const s=b("input",{className:"form-input me-icon",name:"icon",value:d||""}),i=b("span",{className:"me-icon-preview"});d&&i.setAttribute("data-icon",d);const n=b("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),o=r=>{r?i.setAttribute("data-icon",r):i.removeAttribute("data-icon"),Domma.icons.scan(i.parentNode||i)};return n.addEventListener("click",()=>gt(r=>{s.value=r,o(r)})),s.addEventListener("input",()=>o(s.value.trim())),O(t,b("div",{className:"me-icon-field"},s,n,i))}function nt(...t){return b("div",{className:"me-field-grid"},...t)}async function jt(t,d,s,i,n){const o=u=>b("h4",{className:"me-form-section",textContent:u}),r=O("Label",b("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),y=O("URL",b("input",{className:"form-input",name:"url",value:t.url||""}),"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items."),x=It("Icon",t.icon||""),v=O("Visibility",St(t.visibility||"",i),"Who can see this item, by role seniority. Picking a role shows it to that role and anyone more senior. Blank = everyone."),P=W([["","- no permission gate -"],...(d||[]).map(u=>[u.key,u.label||u.key])],t.permission||"");P.name="permission";const z=O("Permission",P,"Hide unless the viewer holds this specific permission. Independent of Visibility - both must pass."),it=O("Badge text",b("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),U=W([["","- none -"],...(s||[]).map(u=>[u.slug,u.title||u.slug])],t.badge?.countFrom||"");U.name="badgeCount";const J=O("Badge count from",U,"Show a live count of this collection\u2019s entries as the badge."),G=et("Badge colour",t.badge?.variant||""),l=W([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");l.name="renderAs";const w=O("Render as",l,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),g=O("Pill style",W([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));g.querySelector("select").name="pillStyle";const M=et("Pill colour",t.pill?.variant||""),R=b("div",{className:"me-pill-wrap"},nt(g,M));R.style.display=t.pill?"":"none",l.addEventListener("change",()=>{R.style.display=l.value==="pill"?"":"none"});const T=et("Text colour",t.colour||""),q=b("button",{className:"btn btn-primary",textContent:"Apply"}),C=b("div",{className:"me-item-form"},o("Link"),r,y,x,o("Access"),nt(v,z),o("Badge"),nt(it,G),J,o("Appearance"),w,R,T,q),S=E.slideover({title:"Edit menu item",size:"lg",position:"right"});S.element.appendChild(C),S.open();const B=S.element.closest(".dm-slideover")||S.element.querySelector(".dm-slideover")||S.element;B.style.setProperty("width",wt+"px","important"),B.style.setProperty("max-width","95vw","important"),Domma.icons.scan(C);const A=u=>C.querySelector(`[name="${u}"]`)?.value||"";q.addEventListener("click",()=>{t.text=A("text"),t.url=A("url"),t.icon=A("icon"),t.visibility=A("visibility"),t.permission=A("permission");const u=A("badgeText"),N=A("badgeCount"),Y=G._read();if(t.badge=u||N?{...u&&{text:u},...N&&{countFrom:N},...Y&&{variant:Y}}:null,A("renderAs")==="pill"){const L=M._read();t.pill={style:A("pillStyle")||"filled",...L&&{variant:L}}}else t.pill=null;t.colour=T._read(),S.close(),n()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){const d=location.hash.match(/^#\/menus\/edit\/(.+)$/),s=d?decodeURIComponent(d[1]):null,i=!s,n=E.loader(t.get(0),{type:"dots"});let o,r,y,x,v,P,z;try{if(i)o={slug:"",name:"",description:"",items:[]};else try{o=await _.menus.get(s)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}r=await _.projects.list().catch(()=>[]),y=await H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),x=await _.collections.list().catch(()=>[]),v=await _.get("/collections/roles/entries?limit=100").catch(()=>null),P=await _.menuLocations.registry().catch(()=>[]),z=await _.pages.list().catch(()=>[])}finally{n.destroy()}const U=(Array.isArray(v)?v:v?.entries||[]).map(e=>({name:e.data?.name,label:e.data?.label,level:e.data?.level??99})).filter(e=>e.name).sort((e,a)=>a.level-e.level),J='<option value="">- no permission gate -</option>',G=e=>J+(y.resources||[]).map(a=>{const c=(e||"")===a.key?" selected":"";return`<option value="${a.key}"${c}>${a.label||a.key}</option>`}).join("");t.find("#me-title").text(i?"New menu":`Edit "${o.slug}"`);let l=lt(o.items);const w=t.find("#me-items-list");function g(){At(w,l,G),_t(w,()=>l,g)}g(),t.find("#me-add-item").on("click",()=>{j(w,l),l.push({id:D(),parentId:null,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}),g()}),t.find("#me-add-separator").on("click",()=>{j(w,l),l.push({id:D(),parentId:null,type:"separator"}),g()}),t.find("#me-add-spacer").on("click",()=>{j(w,l),l.push({id:D(),parentId:null,type:"spacer",size:"md"}),g()});function M(e){return function(){j(w,l);const a=$(this).closest(".menu-tree-row").data("id"),c=l.findIndex(k=>k.id===a);if(c===-1)return;const f=l[c],p={separator:{type:"separator"},spacer:{type:"spacer",size:"md"}}[e],m=p?{id:D(),parentId:f.parentId,...p}:{id:D(),parentId:f.parentId,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1};l.splice(c+1,0,m),g()}}t.off("click",".me-ins-item").on("click",".me-ins-item",M("item")),t.off("click",".me-ins-sep").on("click",".me-ins-sep",M("separator")),t.off("click",".me-remove").on("click",".me-remove",function(){const e=$(this).closest(".menu-tree-row").data("id");l=l.filter(a=>a.id!==e&&a.parentId!==e),g()}),t.off("click",".me-indent").on("click",".me-indent",function(){j(w,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(p=>p.id===e);if(!a)return;const c=l.filter(p=>p.parentId===a.parentId),f=c.findIndex(p=>p.id===e);f<=0||["separator","spacer"].includes(c[f-1].type)||(a.parentId=c[f-1].id,g())}),t.off("click",".me-outdent").on("click",".me-outdent",function(){j(w,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(f=>f.id===e);if(!a||a.parentId==null)return;const c=l.find(f=>f.id===a.parentId);a.parentId=c?c.parentId:null,g()}),t.off("click",".me-up").on("click",".me-up",function(){j(w,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.filter(m=>m.parentId===l.find(k=>k.id===e)?.parentId),c=a.findIndex(m=>m.id===e);if(c<=0)return;const f=l.indexOf(a[c]),p=l.indexOf(a[c-1]);[l[f],l[p]]=[l[p],l[f]],g()}),t.off("click",".me-down").on("click",".me-down",function(){j(w,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.filter(m=>m.parentId===l.find(k=>k.id===e)?.parentId),c=a.findIndex(m=>m.id===e);if(c===-1||c>=a.length-1)return;const f=l.indexOf(a[c]),p=l.indexOf(a[c+1]);[l[f],l[p]]=[l[p],l[f]],g()}),t.off("click",".me-hidden").on("click",".me-hidden",function(){j(w,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(c=>c.id===e);a&&(a.hidden=!a.hidden),g()}),t.off("click",".me-edit").on("click",".me-edit",function(){const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(c=>c.id===e);!a||["separator","spacer"].includes(a.type)||jt(a,y.resources||[],x,U,()=>g())}),E.tabs(t.find("#me-tabs").get(0)),t.find("#me-variant").val(o.variant||""),t.find("#me-position").val(o.position||"");const R=t.find("#me-appearOnHover").get(0);R&&(R.checked=o.appearOnHover===!0),t.find("#me-float-anchor").val(o.float?.anchor||"TL"),t.find("#me-float-offsetX").val(o.float?.offsetX??""),t.find("#me-float-offsetY").val(o.float?.offsetY??""),t.find("#me-float-width").val(o.float?.width??""),t.find("#me-float-radius").val(o.float?.radius??""),t.find("#me-float-shadow").val(o.float?.shadow||""),t.find("#me-float-opacity").val(o.float?.opacity??""),t.find("#me-float-title").val(o.float?.title||"");for(const[e,a]of[["#me-float-collapsible",o.float?.collapsible],["#me-float-collapsed",o.float?.collapsed],["#me-float-movable",o.float?.movable],["#me-float-resizable",o.float?.resizable]]){const c=t.find(e).get(0);c&&(c.checked=a===!0)}const T=o.float?.accent||"",q=T.startsWith("#"),C=t.find("#me-float-accent"),S=t.find("#me-float-accent-hex");C.val(q?"__custom__":T),q&&S.val(T);const B=()=>{S.get(0).style.display=C.val()==="__custom__"?"":"none"};B(),C.on("change",B);const A=()=>C.val()==="__custom__"?S.val():C.val(),u=()=>{const e=t.find("#me-position").val()==="floating";t.find(".dm-float-field").each(function(){this.style.display=e?"":"none"}),e&&B();const a=t.find("#me-orientation").val()==="vertical";t.find(".dm-vertical-field").each(function(){this.style.display=a?"":"none"});const c=!!(t.find("#me-float-collapsible").get(0)||{}).checked;t.find(".dm-float-collapsed-field").each(function(){this.style.display=e&&c?"":"none"})};t.find("#me-orientation").val(o.orientation||""),t.find("#me-side").val(o.side||"left"),u(),t.find("#me-position").on("change",u),t.find("#me-orientation").on("change",u),t.find("#me-float-collapsible").on("change",u),t.find("#me-highlight-hover").val(o.highlight?.hover||""),t.find("#me-highlight-active").val(o.highlight?.active||"");const N=o.highlight?.accent||"",Y=N.startsWith("#"),L=t.find("#me-highlight-accent"),Q=t.find("#me-highlight-accent-hex");L.val(Y?"__custom__":N),Y&&Q.val(N);const ot=()=>{Q.get(0).style.display=L.val()==="__custom__"?"":"none"};ot(),L.on("change",ot);const rt=()=>L.val()==="__custom__"?Q.val():L.val();t.find("#me-fontFamily").val(o.style?.fontFamily||""),t.find("#me-fontSize").val(o.style?.fontSize||""),t.find("#me-fontWeight").val(o.style?.fontWeight||""),t.find("#me-letterSpacing").val(o.style?.letterSpacing||""),t.find("#me-iconSize").val(o.style?.iconSize||"");const K=t.find("#me-bind-slot"),Z=t.find("#me-bind-projects"),X=t.find("#me-bind-pages");K.html('<option value="">- not bound -</option>'+(P||[]).map(e=>`<option value="${h(e.slot)}">${h(e.label||e.slot)} (${h(e.slot)})</option>`).join("")),Z.html(r.map(e=>`<option value="${h(e.slug)}">${h(e.name||e.slug)}</option>`).join("")),K.val(o.binding?.slot||"");const pt=new Set(o.binding?.projects||[]);Array.from(Z.get(0).options).forEach(e=>{e.selected=pt.has(e.value)}),X.val((o.binding?.pages||[]).join(`
44
- `)),t.find("#me-bind-page-pick").html('<option value="">- pick a page -</option>'+z.map(e=>{const a=e.urlPath||e.url||"";return a?`<option value="${h(a)}">${h(e.title||a)} - ${h(a)}</option>`:""}).join("")),t.find("#me-bind-page-add").on("click",()=>{const e=t.find("#me-bind-page-pick").val();if(!e)return;const a=X.val().split(`
45
- `).map(c=>c.trim()).filter(Boolean);a.includes(e)||a.push(e),X.val(a.join(`
46
- `)),V()});function at(){const e=K.val();return e?{slot:e,projects:Array.from(Z.get(0).selectedOptions).map(a=>a.value),pages:X.val().split(`
47
- `).map(a=>a.trim()).filter(Boolean)}:null}function V(){const e=at(),a=t.find("#me-bind-summary");if(!e){a.html('<span class="text-muted">Not bound - this menu renders only where it is mapped or placed by a shortcode.</span>');return}const c=[];e.projects.length&&c.push(`pages in ${e.projects.map(m=>`<code>${h(m)}</code>`).join(", ")}`),e.pages.length&&c.push(`pages matching ${e.pages.map(m=>`<code>${h(m)}</code>`).join(", ")}`);const f=e.pages.filter(m=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(m)),p=f.length?`<div class="text-danger" style="margin-top:.4rem">Not valid page patterns: ${f.map(m=>`<code>${h(m)}</code>`).join(", ")}</div>`:"";a.html(c.length?`Takes over the <code>${h(e.slot)}</code> slot on ${c.join(" and ")}.${p}`:`<span class="text-warning">Slot <code>${h(e.slot)}</code> selected but no project or page chosen - the binding will never match.</span>`)}K.on("change",V),Z.on("change",V),X.on("input",V),V();const{getProjectFromHash:mt}=await import("../lib/project-context.js"),ft=i?mt():null,st=t.find("#me-project");st.html('<option value="">- none -</option>'+r.map(e=>`<option value="${h(e.slug)}">${h(e.name||e.slug)}</option>`).join("")),t.find("#me-slug").val(o.slug||""),t.find("#me-name").val(o.name||""),t.find("#me-description").val(o.description||""),st.val(o.meta?.project||ft||""),Domma.icons.scan(t.get(0)),t.find("#me-save").on("click",async()=>{j(w,l);const e={variant:t.find("#me-variant").val(),position:t.find("#me-position").val(),appearOnHover:!!(t.find("#me-appearOnHover").get(0)||{}).checked,floatAnchor:t.find("#me-float-anchor").val(),floatOffsetX:t.find("#me-float-offsetX").val(),floatOffsetY:t.find("#me-float-offsetY").val(),floatWidth:t.find("#me-float-width").val(),floatRadius:t.find("#me-float-radius").val(),floatShadow:t.find("#me-float-shadow").val(),floatAccent:A(),floatOpacity:t.find("#me-float-opacity").val(),floatTitle:t.find("#me-float-title").val().trim(),floatCollapsible:!!(t.find("#me-float-collapsible").get(0)||{}).checked,floatCollapsed:!!(t.find("#me-float-collapsed").get(0)||{}).checked,floatMovable:!!(t.find("#me-float-movable").get(0)||{}).checked,floatResizable:!!(t.find("#me-float-resizable").get(0)||{}).checked,highlightHover:t.find("#me-highlight-hover").val(),highlightActive:t.find("#me-highlight-active").val(),highlightAccent:rt(),orientation:t.find("#me-orientation").val(),side:t.find("#me-side").val(),fontFamily:t.find("#me-fontFamily").val(),fontSize:t.find("#me-fontSize").val(),fontWeight:t.find("#me-fontWeight").val(),letterSpacing:t.find("#me-letterSpacing").val(),iconSize:t.find("#me-iconSize").val()},a={slug:t.find("#me-slug").val().trim(),name:t.find("#me-name").val().trim(),description:t.find("#me-description").val().trim(),project:t.find("#me-project").val()},c=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"],f=Object.fromEntries(Object.entries(e).filter(([I,F])=>c.includes(I)&&F)),p={};if(e.position==="floating"){e.floatAnchor&&(p.anchor=e.floatAnchor);for(const[I,F]of[["offsetX",e.floatOffsetX],["offsetY",e.floatOffsetY],["width",e.floatWidth],["radius",e.floatRadius],["opacity",e.floatOpacity]])F!==""&&F!=null&&Number.isFinite(Number(F))&&(p[I]=Number(F));e.floatShadow&&(p.shadow=e.floatShadow),e.floatAccent&&(p.accent=e.floatAccent),e.floatTitle&&(p.title=e.floatTitle),e.floatCollapsible&&(p.collapsible=!0),e.floatCollapsible&&e.floatCollapsed&&(p.collapsed=!0),e.floatMovable&&(p.movable=!0),e.floatResizable&&(p.resizable=!0)}const m={};e.highlightHover&&(m.hover=e.highlightHover),e.highlightActive&&(m.active=e.highlightActive),e.highlightAccent&&(m.accent=e.highlightAccent);const k={slug:a.slug,name:a.name,description:a.description,...e.variant&&{variant:e.variant},...e.position&&{position:e.position},...e.orientation&&{orientation:e.orientation},...e.orientation==="vertical"&&e.side&&{side:e.side},...Object.keys(p).length&&{float:p},...Object.keys(m).length&&{highlight:m},...e.appearOnHover&&{appearOnHover:!0},...Object.keys(f).length&&{style:f},binding:at(),items:xt(l),meta:{...o.meta||{},project:a.project||null}};try{if(i)await _.menus.create(k);else if(k.slug!==o.slug){await _.menus.create(k);const I=await _.menuLocations.get();let F=!1;for(const[ut,ht]of Object.entries(I))ht===o.slug&&(I[ut]=k.slug,F=!0);F&&await _.menuLocations.save(I),await _.menus.remove(o.slug)}else await _.menus.update(o.slug,k);E.toast("Saved.",{type:"success"}),location.hash="#/menus/edit/"+encodeURIComponent(k.slug)}catch(I){E.toast(`Save failed: ${I.message||I}`,{type:"error"})}})}};
43
+ `),i(o.id)}}i(null),Domma.icons.scan(t.get(0)),t.get(0).querySelectorAll("[data-tooltip]").forEach(n=>{E.tooltip(n,{content:n.getAttribute("data-tooltip"),position:"top"})})}function j(t,d){t.find(".menu-tree-row").each(function(){const s=$(this).data("id"),i=d.find(y=>y.id===s);if(!i||i.type==="separator")return;if(i.type==="spacer"){const y=$(this).find(".me-spacer-size");y.length&&(i.size=y.val());return}const n=$(this).find(".me-text");n.length&&(i.text=n.val());const o=$(this).find(".me-url");o.length&&(i.url=o.val());const p=$(this).find(".me-icon");p.length&&(i.icon=p.val());const w=$(this).find(".me-vis");w.length&&(i.visibility=w.val());const A=$(this).find(".me-perm");A.length&&(i.permission=A.val())})}function Nt(t,d){const s={};for(const i of d){const n=t.querySelector(`[name="${i}"]`);n&&(s[i]=n.type==="checkbox"?n.checked:n.value)}return s}function b(t,d={},...s){const i=document.createElement(t);Object.assign(i,d);for(const n of s)n!=null&&i.append(n);return i}function I(t,d,s){const i=b("label",{className:"form-field"},b("span",{className:"form-label",textContent:t}),d);return s&&i.append(b("small",{className:"form-hint",textContent:s,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),i}function Lt(t,d){const s=[["","- Everyone -"],["public","Public - anyone, incl. logged-out"],...(d||[]).map(n=>[n.name,`${n.label||n.name} and above`]),["private","Private - super-admin only"]];t&&!s.some(([n])=>n===t)&&s.push([t,`${t} (custom)`]);const i=P(s,t||"");return i.name="visibility",i}function P(t,d){const s=b("select",{className:"form-select"});return t.forEach(([i,n])=>s.add(new Option(n,i))),s.value=d??"",s}function it(t,d){const s=typeof d=="string"&&d.startsWith("#"),i=P([["","- none -"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],s?"__custom__":d||""),n=b("input",{type:"color",className:"form-input me-colour-hex",value:s?d:"#000000"});n.style.display=s?"":"none",i.addEventListener("change",()=>{n.style.display=i.value==="__custom__"?"":"none"});const o=I(t,i);return o.append(n),o._read=()=>i.value==="__custom__"?n.value:i.value,o}function zt(t,d){const s=b("input",{className:"form-input me-icon",name:"icon",value:d||""}),i=b("span",{className:"me-icon-preview"});d&&i.setAttribute("data-icon",d);const n=b("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),o=p=>{p?i.setAttribute("data-icon",p):i.removeAttribute("data-icon"),Domma.icons.scan(i.parentNode||i)};return n.addEventListener("click",()=>At(p=>{s.value=p,o(p)})),s.addEventListener("input",()=>o(s.value.trim())),I(t,b("div",{className:"me-icon-field"},s,n,i))}function ot(...t){return b("div",{className:"me-field-grid"},...t)}async function Ct(t,d,s,i,n){const o=m=>b("h4",{className:"me-form-section",textContent:m}),p=I("Label",b("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),w=I("URL",b("input",{className:"form-input",name:"url",value:t.url||""}),"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items."),A=zt("Icon",t.icon||""),y=I("Visibility",Lt(t.visibility||"",i),"Who can see this item, by role seniority. Picking a role shows it to that role and anyone more senior. Blank = everyone."),N=P([["","- no permission gate -"],...(d||[]).map(m=>[m.key,m.label||m.key])],t.permission||"");N.name="permission";const L=I("Permission",N,"Hide unless the viewer holds this specific permission. Independent of Visibility - both must pass."),lt=I("Badge text",b("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),J=P([["","- none -"],...(s||[]).map(m=>[m.slug,m.title||m.slug])],t.badge?.countFrom||"");J.name="badgeCount";const at=I("Badge count from",J,"Show a live count of this collection\u2019s entries as the badge."),Q=it("Badge colour",t.badge?.variant||""),l=P([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");l.name="renderAs";const x=I("Render as",l,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),v=I("Pill style",P([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));v.querySelector("select").name="pillStyle";const Y=it("Pill colour",t.pill?.variant||""),W=b("div",{className:"me-pill-wrap"},ot(v,Y));W.style.display=t.pill?"":"none",l.addEventListener("change",()=>{W.style.display=l.value==="pill"?"":"none"});const D=it("Text colour",t.colour||""),X=P([["","- use the menu default -"],...rt.map(m=>[m,mt[m]||m])],t.highlight?.hover||"");X.name="hlHover";const R=I("Hover effect",X,"Overrides the menu-wide hover effect for this item only."),M=P([["","- use the menu default -"],...pt.map(m=>[m,ft[m]||m])],t.highlight?.active||"");M.name="hlActive";const U=I("When selected",M,"How this item is marked when it points at the page being viewed."),tt=it("Highlight colour",t.highlight?.accent||""),B=b("button",{className:"btn btn-primary",textContent:"Apply"}),V=b("div",{className:"me-item-form"},o("Link"),p,w,A,o("Access"),ot(y,L),o("Badge"),ot(lt,Q),at,o("Appearance"),x,W,D,o("Highlighting"),ot(R,U),tt,B),S=E.slideover({title:"Edit menu item",size:"lg",position:"right"});S.element.appendChild(V),S.open();const K=S.element.closest(".dm-slideover")||S.element.querySelector(".dm-slideover")||S.element;K.style.setProperty("width",It+"px","important"),K.style.setProperty("max-width","95vw","important"),Domma.icons.scan(V);const g=m=>V.querySelector(`[name="${m}"]`)?.value||"";B.addEventListener("click",()=>{t.text=g("text"),t.url=g("url"),t.icon=g("icon"),t.visibility=g("visibility"),t.permission=g("permission");const m=g("badgeText"),q=g("badgeCount"),et=Q._read();if(t.badge=m||q?{...m&&{text:m},...q&&{countFrom:q},...et&&{variant:et}}:null,g("renderAs")==="pill"){const nt=Y._read();t.pill={style:g("pillStyle")||"filled",...nt&&{variant:nt}}}else t.pill=null;t.colour=D._read();const F=g("hlHover"),T=g("hlActive"),z=tt._read();t.highlight=F||T||z?{...F&&{hover:F},...T&&{active:T},...z&&{accent:z}}:null,S.close(),n()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){const d=location.hash.match(/^#\/menus\/edit\/(.+)$/),s=d?decodeURIComponent(d[1]):null,i=!s,n=E.loader(t.get(0),{type:"dots"});let o,p,w,A,y,N,L;try{if(i)o={slug:"",name:"",description:"",items:[]};else try{o=await k.menus.get(s)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}p=await k.projects.list().catch(()=>[]),w=await H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),A=await k.collections.list().catch(()=>[]),y=await k.get("/collections/roles/entries?limit=100").catch(()=>null),N=await k.menuLocations.registry().catch(()=>[]),L=await k.pages.list().catch(()=>[])}finally{n.destroy()}const J=(Array.isArray(y)?y:y?.entries||[]).map(e=>({name:e.data?.name,label:e.data?.label,level:e.data?.level??99})).filter(e=>e.name).sort((e,a)=>a.level-e.level),at='<option value="">- no permission gate -</option>',Q=e=>at+(w.resources||[]).map(a=>{const c=(e||"")===a.key?" selected":"";return`<option value="${a.key}"${c}>${a.label||a.key}</option>`}).join("");t.find("#me-title").text(i?"New menu":`Edit "${o.slug}"`);let l=ut(o.items);const x=t.find("#me-items-list");function v(){jt(x,l,Q),Ot(x,()=>l,v)}v(),t.find("#me-add-item").on("click",()=>{j(x,l),l.push({id:G(),parentId:null,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}),v()}),t.find("#me-add-separator").on("click",()=>{j(x,l),l.push({id:G(),parentId:null,type:"separator"}),v()}),t.find("#me-add-spacer").on("click",()=>{j(x,l),l.push({id:G(),parentId:null,type:"spacer",size:"md"}),v()});function Y(e){return function(){j(x,l);const a=$(this).closest(".menu-tree-row").data("id"),c=l.findIndex(_=>_.id===a);if(c===-1)return;const u=l[c],r={separator:{type:"separator"},spacer:{type:"spacer",size:"md"}}[e],f=r?{id:G(),parentId:u.parentId,...r}:{id:G(),parentId:u.parentId,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1};l.splice(c+1,0,f),v()}}t.off("click",".me-ins-item").on("click",".me-ins-item",Y("item")),t.off("click",".me-ins-sep").on("click",".me-ins-sep",Y("separator")),t.off("click",".me-remove").on("click",".me-remove",function(){const e=$(this).closest(".menu-tree-row").data("id");l=l.filter(a=>a.id!==e&&a.parentId!==e),v()}),t.off("click",".me-indent").on("click",".me-indent",function(){j(x,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(r=>r.id===e);if(!a)return;const c=l.filter(r=>r.parentId===a.parentId),u=c.findIndex(r=>r.id===e);u<=0||["separator","spacer"].includes(c[u-1].type)||(a.parentId=c[u-1].id,v())}),t.off("click",".me-outdent").on("click",".me-outdent",function(){j(x,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(u=>u.id===e);if(!a||a.parentId==null)return;const c=l.find(u=>u.id===a.parentId);a.parentId=c?c.parentId:null,v()}),t.off("click",".me-up").on("click",".me-up",function(){j(x,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.filter(f=>f.parentId===l.find(_=>_.id===e)?.parentId),c=a.findIndex(f=>f.id===e);if(c<=0)return;const u=l.indexOf(a[c]),r=l.indexOf(a[c-1]);[l[u],l[r]]=[l[r],l[u]],v()}),t.off("click",".me-down").on("click",".me-down",function(){j(x,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.filter(f=>f.parentId===l.find(_=>_.id===e)?.parentId),c=a.findIndex(f=>f.id===e);if(c===-1||c>=a.length-1)return;const u=l.indexOf(a[c]),r=l.indexOf(a[c+1]);[l[u],l[r]]=[l[r],l[u]],v()}),t.off("click",".me-hidden").on("click",".me-hidden",function(){j(x,l);const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(c=>c.id===e);a&&(a.hidden=!a.hidden),v()}),t.off("click",".me-edit").on("click",".me-edit",function(){const e=$(this).closest(".menu-tree-row").data("id"),a=l.find(c=>c.id===e);!a||["separator","spacer"].includes(a.type)||Ct(a,w.resources||[],A,J,()=>v())}),E.tabs(t.find("#me-tabs").get(0)),t.find("#me-variant").val(o.variant||""),t.find("#me-position").val(o.position||"");const W=t.find("#me-appearOnHover").get(0);W&&(W.checked=o.appearOnHover===!0),t.find("#me-float-anchor").val(o.float?.anchor||"TL"),t.find("#me-float-offsetX").val(o.float?.offsetX??""),t.find("#me-float-offsetY").val(o.float?.offsetY??""),t.find("#me-float-width").val(o.float?.width??""),t.find("#me-float-radius").val(o.float?.radius??""),t.find("#me-float-shadow").val(o.float?.shadow||""),t.find("#me-float-opacity").val(o.float?.opacity??""),t.find("#me-float-title").val(o.float?.title||"");for(const[e,a]of[["#me-float-collapsible",o.float?.collapsible],["#me-float-collapsed",o.float?.collapsed],["#me-float-movable",o.float?.movable],["#me-float-resizable",o.float?.resizable]]){const c=t.find(e).get(0);c&&(c.checked=a===!0)}const D=o.float?.accent||"",X=D.startsWith("#"),R=t.find("#me-float-accent"),M=t.find("#me-float-accent-hex");R.val(X?"__custom__":D),X&&M.val(D);const U=()=>{M.get(0).style.display=R.val()==="__custom__"?"":"none"};U(),R.on("change",U);const tt=()=>R.val()==="__custom__"?M.val():R.val(),B=()=>{const e=t.find("#me-position").val()==="floating";t.find(".dm-float-field").each(function(){this.style.display=e?"":"none"}),e&&U();const a=t.find("#me-orientation").val()==="vertical";t.find(".dm-vertical-field").each(function(){this.style.display=a?"":"none"});const c=!!(t.find("#me-float-collapsible").get(0)||{}).checked;t.find(".dm-float-collapsed-field").each(function(){this.style.display=e&&c?"":"none"})};t.find("#me-orientation").val(o.orientation||""),t.find("#me-align").val(o.align||""),t.find("#me-side").val(o.side||"left"),B(),t.find("#me-position").on("change",B),t.find("#me-orientation").on("change",B),t.find("#me-float-collapsible").on("change",B);const V=(e,a,c,u)=>{const r=t.find(e).get(0);r&&(r.innerHTML="",r.add(new Option(u,"")),a.forEach(f=>r.add(new Option(c[f]||f,f))))};V("#me-highlight-hover",rt,mt,"- theme default -"),V("#me-highlight-active",pt,ft,"- theme default -"),t.find("#me-highlight-hover").val(o.highlight?.hover||""),t.find("#me-highlight-active").val(o.highlight?.active||"");const S=o.highlight?.accent||"",K=S.startsWith("#"),g=t.find("#me-highlight-accent"),m=t.find("#me-highlight-accent-hex");g.val(K?"__custom__":S),K&&m.val(S);const q=()=>{m.get(0).style.display=g.val()==="__custom__"?"":"none"};q(),g.on("change",q);const et=()=>g.val()==="__custom__"?m.val():g.val();t.find("#me-fontFamily").val(o.style?.fontFamily||""),t.find("#me-fontSize").val(o.style?.fontSize||""),t.find("#me-fontWeight").val(o.style?.fontWeight||""),t.find("#me-letterSpacing").val(o.style?.letterSpacing||""),t.find("#me-iconSize").val(o.style?.iconSize||"");const F=t.find("#me-bind-slot"),T=t.find("#me-bind-projects"),z=t.find("#me-bind-pages");F.html('<option value="">- not bound -</option>'+(N||[]).map(e=>`<option value="${h(e.slot)}">${h(e.label||e.slot)} (${h(e.slot)})</option>`).join("")),T.html(p.map(e=>`<option value="${h(e.slug)}">${h(e.name||e.slug)}</option>`).join("")),F.val(o.binding?.slot||"");const nt=new Set(o.binding?.projects||[]);Array.from(T.get(0).options).forEach(e=>{e.selected=nt.has(e.value)}),z.val((o.binding?.pages||[]).join(`
44
+ `)),t.find("#me-bind-page-pick").html('<option value="">- pick a page -</option>'+L.map(e=>{const a=e.urlPath||e.url||"";return a?`<option value="${h(a)}">${h(e.title||a)} - ${h(a)}</option>`:""}).join("")),t.find("#me-bind-page-add").on("click",()=>{const e=t.find("#me-bind-page-pick").val();if(!e)return;const a=z.val().split(`
45
+ `).map(c=>c.trim()).filter(Boolean);a.includes(e)||a.push(e),z.val(a.join(`
46
+ `)),Z()});function dt(){const e=F.val();return e?{slot:e,projects:Array.from(T.get(0).selectedOptions).map(a=>a.value),pages:z.val().split(`
47
+ `).map(a=>a.trim()).filter(Boolean)}:null}function Z(){const e=dt(),a=t.find("#me-bind-summary");if(!e){a.html('<span class="text-muted">Not bound - this menu renders only where it is mapped or placed by a shortcode.</span>');return}const c=[];e.projects.length&&c.push(`pages in ${e.projects.map(f=>`<code>${h(f)}</code>`).join(", ")}`),e.pages.length&&c.push(`pages matching ${e.pages.map(f=>`<code>${h(f)}</code>`).join(", ")}`);const u=e.pages.filter(f=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(f)),r=u.length?`<div class="text-danger" style="margin-top:.4rem">Not valid page patterns: ${u.map(f=>`<code>${h(f)}</code>`).join(", ")}</div>`:"";a.html(c.length?`Takes over the <code>${h(e.slot)}</code> slot on ${c.join(" and ")}.${r}`:`<span class="text-warning">Slot <code>${h(e.slot)}</code> selected but no project or page chosen - the binding will never match.</span>`)}F.on("change",Z),T.on("change",Z),z.on("input",Z),Z();const{getProjectFromHash:bt}=await import("../lib/project-context.js"),vt=i?bt():null,ct=t.find("#me-project");ct.html('<option value="">- none -</option>'+p.map(e=>`<option value="${h(e.slug)}">${h(e.name||e.slug)}</option>`).join("")),t.find("#me-slug").val(o.slug||""),t.find("#me-name").val(o.name||""),t.find("#me-description").val(o.description||""),ct.val(o.meta?.project||vt||""),Domma.icons.scan(t.get(0)),t.find("#me-save").on("click",async()=>{j(x,l);const e={variant:t.find("#me-variant").val(),position:t.find("#me-position").val(),appearOnHover:!!(t.find("#me-appearOnHover").get(0)||{}).checked,floatAnchor:t.find("#me-float-anchor").val(),floatOffsetX:t.find("#me-float-offsetX").val(),floatOffsetY:t.find("#me-float-offsetY").val(),floatWidth:t.find("#me-float-width").val(),floatRadius:t.find("#me-float-radius").val(),floatShadow:t.find("#me-float-shadow").val(),floatAccent:tt(),floatOpacity:t.find("#me-float-opacity").val(),floatTitle:t.find("#me-float-title").val().trim(),floatCollapsible:!!(t.find("#me-float-collapsible").get(0)||{}).checked,floatCollapsed:!!(t.find("#me-float-collapsed").get(0)||{}).checked,floatMovable:!!(t.find("#me-float-movable").get(0)||{}).checked,floatResizable:!!(t.find("#me-float-resizable").get(0)||{}).checked,highlightHover:t.find("#me-highlight-hover").val(),highlightActive:t.find("#me-highlight-active").val(),highlightAccent:et(),orientation:t.find("#me-orientation").val(),align:t.find("#me-align").val(),side:t.find("#me-side").val(),fontFamily:t.find("#me-fontFamily").val(),fontSize:t.find("#me-fontSize").val(),fontWeight:t.find("#me-fontWeight").val(),letterSpacing:t.find("#me-letterSpacing").val(),iconSize:t.find("#me-iconSize").val()},a={slug:t.find("#me-slug").val().trim(),name:t.find("#me-name").val().trim(),description:t.find("#me-description").val().trim(),project:t.find("#me-project").val()},c=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"],u=Object.fromEntries(Object.entries(e).filter(([O,C])=>c.includes(O)&&C)),r={};if(e.position==="floating"){e.floatAnchor&&(r.anchor=e.floatAnchor);for(const[O,C]of[["offsetX",e.floatOffsetX],["offsetY",e.floatOffsetY],["width",e.floatWidth],["radius",e.floatRadius],["opacity",e.floatOpacity]])C!==""&&C!=null&&Number.isFinite(Number(C))&&(r[O]=Number(C));e.floatShadow&&(r.shadow=e.floatShadow),e.floatAccent&&(r.accent=e.floatAccent),e.floatTitle&&(r.title=e.floatTitle),e.floatCollapsible&&(r.collapsible=!0),e.floatCollapsible&&e.floatCollapsed&&(r.collapsed=!0),e.floatMovable&&(r.movable=!0),e.floatResizable&&(r.resizable=!0)}const f={};e.highlightHover&&(f.hover=e.highlightHover),e.highlightActive&&(f.active=e.highlightActive),e.highlightAccent&&(f.accent=e.highlightAccent);const _={slug:a.slug,name:a.name,description:a.description,...e.variant&&{variant:e.variant},...e.position&&{position:e.position},...e.orientation&&{orientation:e.orientation},...e.orientation==="vertical"&&e.side&&{side:e.side},...e.orientation!=="vertical"&&e.align&&{align:e.align},...Object.keys(r).length&&{float:r},...Object.keys(f).length&&{highlight:f},...e.appearOnHover&&{appearOnHover:!0},...Object.keys(u).length&&{style:u},binding:dt(),items:Ht(l),meta:{...o.meta||{},project:a.project||null}};try{if(i)await k.menus.create(_);else if(_.slug!==o.slug){await k.menus.create(_);const O=await k.menuLocations.get();let C=!1;for(const[yt,wt]of Object.entries(O))wt===o.slug&&(O[yt]=_.slug,C=!0);C&&await k.menuLocations.save(O),await k.menus.remove(o.slug)}else await k.menus.update(o.slug,_);E.toast("Saved.",{type:"success"}),location.hash="#/menus/edit/"+encodeURIComponent(_.slug)}catch(O){E.toast(`Save failed: ${O.message||O}`,{type:"error"})}})}};
@@ -1 +1 @@
1
- import{api as i}from"../api.js";export const pluginMarketplaceView={templateUrl:"/admin/js/templates/plugin-marketplace.html",async onMount(e){const a=E.loader(e.get(0),{type:"dots"});let t;try{t=await i.marketplace.catalogue()}catch(n){E.toast(`Failed to load marketplace: ${n.message}`,{type:"error"}),a.destroy();return}if(a.destroy(),!t.available){e.find("#marketplace-unavailable").show();return}e.find("#marketplace-available").show(),b(e,t.catalogue),Domma.icons.scan()}};function b(e,a){const t=e.find("#marketplace-grid").get(0);if(t){for(;t.firstChild;)t.removeChild(t.firstChild);if(!a||a.length===0){const n=document.createElement("p");n.className="text-muted",n.textContent="No plugins available in the marketplace.",t.appendChild(n);return}a.forEach(n=>{const u=$(n,e);t.appendChild(u)})}}function U(e){if(!e||e===0||e==="0")return"Free";const a=parseFloat(e);return isNaN(a)||a===0?"Free":`\xA3${a.toFixed(2)}/mo`}function $(e,a){const{slug:t,version:n,displayName:u,description:w,price:D,entitled:x,installed:F}=e,s=document.createElement("div");s.className="card plugin-card",s.dataset.slug=t;const o=document.createElement("div");o.className="card-body";const c=document.createElement("div");c.className="plugin-header";const h=document.createElement("div");h.className="plugin-icon";const k=document.createElement("span");k.dataset.icon="package",h.appendChild(k);const d=document.createElement("div");d.className="plugin-meta";const g=document.createElement("div");g.className="plugin-name",g.textContent=u||t;const f=document.createElement("div");f.className="plugin-version",f.textContent=`v${n}`,d.appendChild(g),d.appendChild(f);const r=document.createElement("div");r.style.cssText="display:flex;gap:.35rem;align-items:center;";const C=document.createElement("span");if(C.className="badge badge-outline",C.textContent=U(D),r.appendChild(C),x){const N=document.createElement("span");N.className="badge badge-success",N.textContent="Entitled",r.appendChild(N)}c.appendChild(h),c.appendChild(d),c.appendChild(r);const v=document.createElement("p");v.className="plugin-desc",v.textContent=w||"",o.appendChild(c),o.appendChild(v);const m=document.createElement("div");m.className="plugin-footer";const p=document.createElement("span");p.className="text-muted",p.style.fontSize=".8rem",p.textContent="Restart server after changes",m.appendChild(p);const y=document.createElement("div");y.className="plugin-footer-actions";const l=document.createElement("button");return l.className="btn btn-sm",F?(l.className+=" btn-ghost",l.textContent="Uninstall",l.onclick=()=>R(t,a)):x?(l.className+=" btn-primary",l.textContent="Install",l.onclick=()=>I(t,n,a)):(l.className+=" btn-ghost",l.textContent="Not entitled",l.disabled=!0),y.appendChild(l),m.appendChild(y),s.appendChild(o),s.appendChild(m),s}async function I(e,a,t){if(await E.confirm(`Install ${e} v${a}?`))try{await i.marketplace.install(e,a),E.toast(`${e} installed. Restart the server to activate.`,{type:"success"});const n=await i.marketplace.catalogue();b(t,n.catalogue),Domma.icons.scan()}catch(n){E.toast(`Install failed: ${n.message}`,{type:"error"})}}async function R(e,a){if(await E.confirm(`Uninstall ${e}? This will remove its files.`))try{await i.marketplace.uninstall(e),E.toast(`${e} uninstalled. Restart to apply.`,{type:"success"});const t=await i.marketplace.catalogue();b(a,t.catalogue),Domma.icons.scan()}catch(t){E.toast(`Uninstall failed: ${t.message}`,{type:"error"})}}
1
+ import{api as i}from"../api.js";import{pluginAccent as U,pluginWatermark as $}from"../lib/plugin-accent.js";export const pluginMarketplaceView={templateUrl:"/admin/js/templates/plugin-marketplace.html",async onMount(e){const a=E.loader(e.get(0),{type:"dots"});let t;try{t=await i.marketplace.catalogue()}catch(n){E.toast(`Failed to load marketplace: ${n.message}`,{type:"error"}),a.destroy();return}if(a.destroy(),!t.available){e.find("#marketplace-unavailable").show();return}e.find("#marketplace-available").show(),b(e,t.catalogue),Domma.icons.scan()}};function b(e,a){const t=e.find("#marketplace-grid").get(0);if(t){for(;t.firstChild;)t.removeChild(t.firstChild);if(!a||a.length===0){const n=document.createElement("p");n.className="text-muted",n.textContent="No plugins available in the marketplace.",t.appendChild(n);return}a.forEach(n=>{const u=P(n,e);t.appendChild(u)})}}function I(e){if(!e||e===0||e==="0")return"Free";const a=parseFloat(e);return isNaN(a)||a===0?"Free":`\xA3${a.toFixed(2)}/mo`}function P(e,a){const{slug:t,version:n,displayName:u,description:w,price:D,entitled:k,installed:F}=e,s=document.createElement("div");s.className="card plugin-card",s.dataset.slug=t,s.style.setProperty("--plugin-accent",U(t)),s.appendChild($(e.icon));const o=document.createElement("div");o.className="card-body";const c=document.createElement("div");c.className="plugin-header";const h=document.createElement("div");h.className="plugin-icon";const x=document.createElement("span");x.dataset.icon="package",h.appendChild(x);const r=document.createElement("div");r.className="plugin-meta";const g=document.createElement("div");g.className="plugin-name",g.textContent=u||t;const f=document.createElement("div");f.className="plugin-version",f.textContent=`v${n}`,r.appendChild(g),r.appendChild(f);const d=document.createElement("div");d.style.cssText="display:flex;gap:.35rem;align-items:center;";const C=document.createElement("span");if(C.className="badge badge-outline",C.textContent=I(D),d.appendChild(C),k){const N=document.createElement("span");N.className="badge badge-success",N.textContent="Entitled",d.appendChild(N)}c.appendChild(h),c.appendChild(r),c.appendChild(d);const v=document.createElement("p");v.className="plugin-desc",v.textContent=w||"",o.appendChild(c),o.appendChild(v);const m=document.createElement("div");m.className="plugin-footer";const p=document.createElement("span");p.className="text-muted",p.style.fontSize=".8rem",p.textContent="Restart server after changes",m.appendChild(p);const y=document.createElement("div");y.className="plugin-footer-actions";const l=document.createElement("button");return l.className="btn btn-sm",F?(l.className+=" btn-ghost",l.textContent="Uninstall",l.onclick=()=>M(t,a)):k?(l.className+=" btn-primary",l.textContent="Install",l.onclick=()=>R(t,n,a)):(l.className+=" btn-ghost",l.textContent="Not entitled",l.disabled=!0),y.appendChild(l),m.appendChild(y),s.appendChild(o),s.appendChild(m),s}async function R(e,a,t){if(await E.confirm(`Install ${e} v${a}?`))try{await i.marketplace.install(e,a),E.toast(`${e} installed. Restart the server to activate.`,{type:"success"});const n=await i.marketplace.catalogue();b(t,n.catalogue),Domma.icons.scan()}catch(n){E.toast(`Install failed: ${n.message}`,{type:"error"})}}async function M(e,a){if(await E.confirm(`Uninstall ${e}? This will remove its files.`))try{await i.marketplace.uninstall(e),E.toast(`${e} uninstalled. Restart to apply.`,{type:"success"});const t=await i.marketplace.catalogue();b(a,t.catalogue),Domma.icons.scan()}catch(t){E.toast(`Uninstall failed: ${t.message}`,{type:"error"})}}
@@ -1,39 +1,41 @@
1
- import{api as b,getUser as h}from"../api.js";import{pluginMarketplaceView as v}from"./plugin-marketplace.js";import{reportRestart as x}from"../lib/server-restart.js";export const pluginsView={templateUrl:"/admin/js/templates/plugins.html",async onMount(s){s.find("#new-plugin-btn").on("click",()=>k(s));let p=!1;s.find('[data-tab="marketplace"]').on("click",async function(){if(p)return;const a=s.find("#marketplace-mount");try{const n=await H.get(v.templateUrl),u=new DOMParser().parseFromString(n,"text/html");for(;u.body.firstChild;)a.get(0).appendChild(u.body.firstChild);await v.onMount(a),p=!0}catch{E.toast("Failed to load marketplace.",{type:"error"})}}),E.tabs(s.find("#plugin-tabs").get(0));const r=E.loader(s.get(0),{type:"dots"});let e=[];try{e=await b.plugins.list()}catch(a){E.toast(`Failed to load plugins: ${a.message}`,{type:"error"})}if(r.destroy(),!e.length){s.find("#plugins-empty").show();return}f(s,e),Domma.icons.scan()}};function k(s){const p=E.slideover({title:"New Plugin",size:"sm",position:"right"}),r=document.createElement("div");r.style.cssText="padding:.25rem 0 .5rem;";const e=h(),a=document.createElement("div");F.create({displayName:{type:"string",label:"Display Name",placeholder:"e.g. Event Manager",required:!0},slug:{type:"string",label:"Slug",placeholder:"event-manager",required:!0,help:"Folder name under plugins/ - lowercase letters, digits, hyphens."},description:{type:"string",label:"Description",placeholder:"What does this plugin do?"},author:{type:"string",label:"Author"},icon:{type:"string",label:"Icon",placeholder:"box",help:"A Domma icon name (as used in data-icon)."}},{author:e?.name||e?.username||"",icon:"box"},{showSubmitButton:!1}).renderTo(a),r.appendChild(a);const n=document.createElement("p");n.className="text-muted",n.style.cssText="font-size:.8rem;margin:.5rem 0 0;",n.textContent="The plugin is scaffolded from the built-in template with server routes, an admin view and a CLAUDE.md, and enabled automatically. Restart the server to activate it.",r.appendChild(n);const l=document.createElement("div");l.style.cssText="display:flex;justify-content:flex-end;gap:1rem;margin-top:.75rem;";const u=document.createElement("button");u.className="btn btn-ghost",u.textContent="Cancel";const o=document.createElement("button");o.className="btn btn-primary",o.textContent="Create Plugin",l.appendChild(u),l.appendChild(o),r.appendChild(l),p.element.appendChild(r),p.open();const m=a.querySelector('input[name="displayName"]'),t=a.querySelector('input[name="slug"]');setTimeout(()=>m?.focus(),50);let d=!1;t?.addEventListener("input",()=>{d=!0}),m?.addEventListener("input",()=>{d||!t||(t.value=m.value.toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,40))});async function g(){const c={};for(const y of["displayName","slug","description","author","icon"])c[y]=a.querySelector(`input[name="${y}"]`)?.value.trim()||"";if(!c.displayName||!c.slug){E.toast("Display name and slug are required.",{type:"error"});return}o.disabled=!0,o.textContent="Creating\u2026";try{await b.plugins.scaffold(c),p.close(),E.toast(`Plugin "${c.displayName}" created. Restart the server to activate it.`,{type:"success"});const y=await b.plugins.list();y.length&&(s.find("#plugins-empty").hide(),f(s,y))}catch(y){E.toast(y.message||"Failed to create plugin.",{type:"error"}),o.disabled=!1,o.textContent="Create Plugin"}}u.addEventListener("click",()=>p.close()),o.addEventListener("click",g),t?.addEventListener("keydown",c=>{c.key==="Enter"&&g()})}function f(s,p){const r=s.find("#plugins-grid").empty();p.forEach(e=>{const a=e.enabled?"badge-success":"badge-secondary",n=e.enabled?"Enabled":"Disabled",l=e.enabled?"Disable":"Enable",u=e.enabled?"btn-ghost":"btn-primary",o=e.bundled?"var(--dm-info,#2563eb)":"var(--dm-secondary,#6b7280)",m=e.bundled?"Bundled - click to unbundle":"Not bundled - click to bundle",t=`<button class="badge badge-outline btn-bundle-plugin" data-name="${e.name}" data-bundled="${e.bundled?"1":"0"}" title="${m}" style="font-size:0.65rem;padding:1px 6px;cursor:pointer;background:none;border:1px solid;color:${o};border-color:${o};">${e.bundled?"Bundled":"Unbundled"}</button>`,d=e.author?`by ${i(e.author)}`:"",g=e.date?i(e.date):"",c=`
2
- <div class="card plugin-card" data-plugin="${e.name}">
1
+ import{api as g,getUser as k}from"../api.js";import{pluginMarketplaceView as w}from"./plugin-marketplace.js";import{pluginAccent as S,pluginWatermark as N}from"../lib/plugin-accent.js";import{reportRestart as L}from"../lib/server-restart.js";export const pluginsView={templateUrl:"/admin/js/templates/plugins.html",async onMount(a){a.find("#new-plugin-btn").on("click",()=>T(a));let c=!1;a.find('[data-tab="marketplace"]').on("click",async function(){if(c)return;const n=a.find("#marketplace-mount");try{const s=await H.get(w.templateUrl),u=new DOMParser().parseFromString(s,"text/html");for(;u.body.firstChild;)n.get(0).appendChild(u.body.firstChild);await w.onMount(n),c=!0}catch{E.toast("Failed to load marketplace.",{type:"error"})}}),E.tabs(a.find("#plugin-tabs").get(0));const i=E.loader(a.get(0),{type:"dots"});let e=[];try{e=await g.plugins.list()}catch(n){E.toast(`Failed to load plugins: ${n.message}`,{type:"error"})}if(i.destroy(),!e.length){a.find("#plugins-empty").show();return}y(a,e),Domma.icons.scan()}};function T(a){const c=E.slideover({title:"New Plugin",size:"sm",position:"right"}),i=document.createElement("div");i.style.cssText="padding:.25rem 0 .5rem;";const e=k(),n=document.createElement("div");F.create({displayName:{type:"string",label:"Display Name",placeholder:"e.g. Event Manager",required:!0},slug:{type:"string",label:"Slug",placeholder:"event-manager",required:!0,help:"Folder name under plugins/ - lowercase letters, digits, hyphens."},description:{type:"string",label:"Description",placeholder:"What does this plugin do?"},author:{type:"string",label:"Author"},icon:{type:"string",label:"Icon",placeholder:"box",help:"A Domma icon name (as used in data-icon)."}},{author:e?.name||e?.username||"",icon:"box"},{showSubmitButton:!1}).renderTo(n),i.appendChild(n);const s=document.createElement("p");s.className="text-muted",s.style.cssText="font-size:.8rem;margin:.5rem 0 0;",s.textContent="The plugin is scaffolded from the built-in template with server routes, an admin view and a CLAUDE.md, and enabled automatically. Restart the server to activate it.",i.appendChild(s);const l=document.createElement("div");l.style.cssText="display:flex;justify-content:flex-end;gap:1rem;margin-top:.75rem;";const u=document.createElement("button");u.className="btn btn-ghost",u.textContent="Cancel";const r=document.createElement("button");r.className="btn btn-primary",r.textContent="Create Plugin",l.appendChild(u),l.appendChild(r),i.appendChild(l),c.element.appendChild(i),c.open();const m=n.querySelector('input[name="displayName"]'),t=n.querySelector('input[name="slug"]');setTimeout(()=>m?.focus(),50);let p=!1;t?.addEventListener("input",()=>{p=!0}),m?.addEventListener("input",()=>{p||!t||(t.value=m.value.toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,40))});async function f(){const d={};for(const b of["displayName","slug","description","author","icon"])d[b]=n.querySelector(`input[name="${b}"]`)?.value.trim()||"";if(!d.displayName||!d.slug){E.toast("Display name and slug are required.",{type:"error"});return}r.disabled=!0,r.textContent="Creating\u2026";try{await g.plugins.scaffold(d),c.close(),E.toast(`Plugin "${d.displayName}" created. Restart the server to activate it.`,{type:"success"});const b=await g.plugins.list();b.length&&(a.find("#plugins-empty").hide(),y(a,b))}catch(b){E.toast(b.message||"Failed to create plugin.",{type:"error"}),r.disabled=!1,r.textContent="Create Plugin"}}u.addEventListener("click",()=>c.close()),r.addEventListener("click",f),t?.addEventListener("keydown",d=>{d.key==="Enter"&&f()})}function B(a){if(!a)return"";const c=new Date(a);if(Number.isNaN(c.getTime()))return"";try{return D(a).format("DD.MM.YYYY")}catch{return""}}function y(a,c){const i=a.find("#plugins-grid").empty();c.forEach(e=>{const n=!!e.core,s=e.sourceEditable!==!1,l="var(--dm-warning,#d97706)",u=e.closedSource?`<span class="badge badge-outline" title="${o(e.licence||"Commercial licence")} - source is not editable here" style="font-size:0.65rem;padding:1px 6px;background:none;border:1px solid;color:${l};border-color:${l};">Licensed</span>`:"",r=n?"badge-info":e.enabled?"badge-success":"badge-secondary",m=n?"Built in":e.enabled?"Enabled":"Disabled",t=e.enabled?"Disable":"Enable",p=e.enabled?"btn-ghost":"btn-primary",f=e.bundled?"var(--dm-info,#2563eb)":"var(--dm-secondary,#6b7280)",d=e.bundled?"Bundled - click to unbundle":"Not bundled - click to bundle",b=`<button class="badge badge-outline btn-bundle-plugin" data-name="${e.name}" data-bundled="${e.bundled?"1":"0"}" title="${d}" style="font-size:0.65rem;padding:1px 6px;cursor:pointer;background:none;border:1px solid;color:${f};border-color:${f};">${e.bundled?"Bundled":"Unbundled"}</button>`,h=e.author?`by ${o(e.author)}`:"",v=B(e.date),x=e.icon||"package",C=`
2
+ <div class="card plugin-card" data-plugin="${e.name}" style="--plugin-accent:${S(e.name)}">
3
+ ${N(x).outerHTML}
3
4
  <div class="card-body">
4
5
  <div class="plugin-header">
5
- <div class="plugin-icon"><span data-icon="${e.icon||"package"}"></span></div>
6
+ <div class="plugin-icon"><span data-icon="${x}"></span></div>
6
7
  <div class="plugin-meta">
7
- <div class="plugin-name">${i(e.displayName)}</div>
8
- <div class="plugin-version">v${i(e.version)}${d?` &middot; ${d}`:""}</div>
8
+ <div class="plugin-name">${o(e.displayName)}</div>
9
+ <div class="plugin-version">v${o(e.version)}${h?` &middot; ${h}`:""}</div>
9
10
  </div>
10
- <div style="display:flex;gap:.35rem;align-items:center;">
11
- ${t}
12
- <span class="badge ${a}">${n}</span>
11
+ <div style="display:flex;gap:.35rem;align-items:center;flex-wrap:wrap;justify-content:flex-end;flex-shrink:0;margin-left:auto;">
12
+ ${b}
13
+ ${u}
14
+ <span class="badge ${r}">${m}</span>
13
15
  </div>
14
16
  </div>
15
- <p class="plugin-desc">${i(e.description)}</p>
16
- ${g?`<p class="plugin-date text-muted" style="font-size:.8rem;margin:0">Released ${g}</p>`:""}
17
+ <p class="plugin-desc">${o(e.description)}</p>
18
+ ${v?`<p class="plugin-date text-muted" style="font-size:.8rem;margin:0">Released ${v}</p>`:""}
17
19
  </div>
18
20
  <div class="plugin-footer">
19
- <span class="text-muted" style="font-size:.8rem">Restart server after changes</span>
21
+ <span class="text-muted" style="font-size:.8rem">${n?"Part of the CMS - always on":"Restart server after changes"}</span>
20
22
  <div class="plugin-footer-actions">
21
- <button class="btn btn-sm btn-ghost btn-plugin-code" data-name="${e.name}"><span data-icon="code"></span> Code</button>
23
+ ${s?`<button class="btn btn-sm btn-ghost btn-plugin-code" data-name="${e.name}"><span data-icon="code"></span> Code</button>`:""}
22
24
  ${e.settingsSchema?`<button class="btn btn-sm btn-ghost btn-plugin-settings" data-name="${e.name}"><span data-icon="settings"></span> Settings</button>`:""}
23
- <button class="btn btn-sm ${u} btn-toggle-plugin"
25
+ ${n?"":`<button class="btn btn-sm ${p} btn-toggle-plugin"
24
26
  data-name="${e.name}"
25
27
  data-enabled="${e.enabled?"1":"0"}">
26
- <span data-icon="${e.enabled?"pause":"play"}"></span> ${l}
27
- </button>
28
+ <span data-icon="${e.enabled?"pause":"play"}"></span> ${t}
29
+ </button>`}
28
30
  </div>
29
31
  </div>
30
- </div>`;r.append(c)}),Domma.icons.scan(),Domma.effects.reveal(".plugin-card",{animation:"fade",stagger:60,duration:400}),Domma.effects.ripple(".btn-toggle-plugin"),r.on("click",".btn-plugin-code",function(){R.navigate(`/plugins/code/${$(this).data("name")}`)}),r.on("click",".btn-plugin-settings",async function(){const e=$(this).data("name"),a=p.find(t=>t.name===e);if(!a?.settingsSchema)return;const l=`<form id="plugin-settings-form">${a.settingsSchema.map(t=>{const d=a.settings?.[t.key]??t.default??"";if(t.type==="select"){const g=t.options.map(c=>`<option value="${i(c.value)}"${String(d)===String(c.value)?" selected":""}>${i(c.label)}</option>`).join("");return`<div class="form-group">
31
- <label class="form-label">${i(t.label)}</label>
32
- <select class="form-control" name="${i(t.key)}">${g}</select>
32
+ </div>`;i.append(C)}),Domma.icons.scan(),Domma.effects.reveal(".plugin-card",{animation:"fade",stagger:60,duration:400}),Domma.effects.ripple(".btn-toggle-plugin"),i.on("click",".btn-plugin-code",function(){R.navigate(`/plugins/code/${$(this).data("name")}`)}),i.on("click",".btn-plugin-settings",async function(){const e=$(this).data("name"),n=c.find(t=>t.name===e);if(!n?.settingsSchema)return;const l=`<form id="plugin-settings-form">${n.settingsSchema.map(t=>{const p=n.settings?.[t.key]??t.default??"";if(t.type==="select"){const f=t.options.map(d=>`<option value="${o(d.value)}"${String(p)===String(d.value)?" selected":""}>${o(d.label)}</option>`).join("");return`<div class="form-group">
33
+ <label class="form-label">${o(t.label)}</label>
34
+ <select class="form-control" name="${o(t.key)}">${f}</select>
33
35
  </div>`}return t.type==="number"?`<div class="form-group">
34
- <label class="form-label">${i(t.label)}</label>
35
- <input class="form-control" type="number" name="${i(t.key)}" value="${i(String(d))}"${t.min!=null?` min="${t.min}"`:""}${t.max!=null?` max="${t.max}"`:""}>
36
+ <label class="form-label">${o(t.label)}</label>
37
+ <input class="form-control" type="number" name="${o(t.key)}" value="${o(String(p))}"${t.min!=null?` min="${t.min}"`:""}${t.max!=null?` max="${t.max}"`:""}>
36
38
  </div>`:`<div class="form-group">
37
- <label class="form-label">${i(t.label)}</label>
38
- <input class="form-control" type="text" name="${i(t.key)}" value="${i(String(d))}">
39
- </div>`}).join("")}</form>`;if(!await E.confirm(l,{title:`${i(a.displayName)} Settings`,confirmText:"Save",cancelText:"Cancel",html:!0}))return;const o=document.getElementById("plugin-settings-form");if(!o)return;const m={};a.settingsSchema.forEach(t=>{const d=o.elements[t.key];d&&(m[t.key]=t.type==="number"?Number(d.value):d.value)});try{await b.plugins.update(e,{settings:m}),a.settings={...a.settings,...m},E.toast("Settings saved.",{type:"success"})}catch(t){E.toast(`Failed to save: ${t.message}`,{type:"error"})}}),r.on("click",".btn-bundle-plugin",async function(){const e=$(this),a=e.data("name"),n=e.data("bundled")!==1&&e.data("bundled")!=="1";e.prop("disabled",!0).text(n?"Bundling\u2026":"Unbundling\u2026");try{await b.plugins.update(a,{bundled:n}),E.toast(`Plugin ${n?"marked as bundled":"unbundled"}.`,{type:"success"});const l=await b.plugins.list();f(s,l)}catch(l){E.toast(`Failed: ${l.message}`,{type:"error"}),e.prop("disabled",!1)}}),r.on("click",".btn-toggle-plugin",async function(){const e=$(this),a=e.data("name"),n=e.data("enabled")!==1&&e.data("enabled")!=="1";e.prop("disabled",!0).text(n?"Enabling\u2026":"Disabling\u2026");try{const l=await b.plugins.update(a,{enabled:n});E.toast(`Plugin ${n?"enabled":"disabled"}.`,{type:"success"}),await x(l,{onStatus:o=>e.text(o.slice(0,24))});const u=await b.plugins.list();f(s,u)}catch(l){E.toast(`Failed: ${l.message}`,{type:"error"}),e.prop("disabled",!1)}})}function i(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}
39
+ <label class="form-label">${o(t.label)}</label>
40
+ <input class="form-control" type="text" name="${o(t.key)}" value="${o(String(p))}">
41
+ </div>`}).join("")}</form>`;if(!await E.confirm(l,{title:`${o(n.displayName)} Settings`,confirmText:"Save",cancelText:"Cancel",html:!0}))return;const r=document.getElementById("plugin-settings-form");if(!r)return;const m={};n.settingsSchema.forEach(t=>{const p=r.elements[t.key];p&&(m[t.key]=t.type==="number"?Number(p.value):p.value)});try{await g.plugins.update(e,{settings:m}),n.settings={...n.settings,...m},E.toast("Settings saved.",{type:"success"})}catch(t){E.toast(`Failed to save: ${t.message}`,{type:"error"})}}),i.on("click",".btn-bundle-plugin",async function(){const e=$(this),n=e.data("name"),s=e.data("bundled")!==1&&e.data("bundled")!=="1";e.prop("disabled",!0).text(s?"Bundling\u2026":"Unbundling\u2026");try{await g.plugins.update(n,{bundled:s}),E.toast(`Plugin ${s?"marked as bundled":"unbundled"}.`,{type:"success"});const l=await g.plugins.list();y(a,l)}catch(l){E.toast(`Failed: ${l.message}`,{type:"error"}),e.prop("disabled",!1)}}),i.on("click",".btn-toggle-plugin",async function(){const e=$(this),n=e.data("name"),s=e.data("enabled")!==1&&e.data("enabled")!=="1";e.prop("disabled",!0).text(s?"Enabling\u2026":"Disabling\u2026");try{const l=await g.plugins.update(n,{enabled:s});E.toast(`Plugin ${s?"enabled":"disabled"}.`,{type:"success"}),await L(l,{onStatus:r=>e.text(r.slice(0,24))});const u=await g.plugins.list();y(a,u)}catch(l){E.toast(`Failed: ${l.message}`,{type:"error"}),e.prop("disabled",!1)}})}function o(a){return String(a).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}
@@ -0,0 +1 @@
1
+ import{apiRequest as q}from"/admin/js/api.js";export const searchView={templateUrl:"/admin/js/templates/search.html",async onMount(s){let n={};const A=E.loader(s.get(0),{type:"dots"});try{n=await q("/search/settings")}catch{E.toast("Could not load settings.",{type:"error"})}finally{A.destroy()}s.find("#field-enabled").prop("checked",n.enabled!==!1),s.find("#field-placeholder").val(n.placeholder||"Search pages..."),s.find("#field-keyboard-shortcut").prop("checked",n.keyboardShortcut!==!1),s.find("#field-max-results").val(n.maxResults??10),s.find("#field-min-query-length").val(n.minQueryLength??2),s.find("#field-debounce-ms").val(n.debounceMs??300);const g=n.displayMode||"navbar",d=document.getElementById("field-display-mode"),a=document.getElementById("field-position"),f=document.getElementById("field-offset-x"),u=document.getElementById("field-offset-y"),l=document.getElementById("field-icon-only"),o=document.getElementById("field-icon-radius"),c=document.getElementById("field-icon-size"),h=document.getElementById("ss-size-row"),v=document.getElementById("ss-size-value"),b=document.getElementById("ss-radius-max-hint"),I=document.getElementById("ss-radius-row"),x=document.getElementById("ss-radius-value"),r=document.getElementById("ss-radius-preview"),B=document.getElementById("ss-pos-section"),S=document.getElementById("ss-pos-grid"),p=document.getElementById("ss-display-mode");d&&(d.value=g),a&&(a.value=n.position??"bottom-right"),f&&(f.value=n.offsetX??32),u&&(u.value=n.offsetY??32),l&&(l.checked=n.iconOnly!==!1),c&&(c.value=n.iconSize??52),o&&(o.value=n.iconRadius??26);const k=()=>parseInt(c?.value,10)||52;function y(){const e=k(),t=parseInt(o?.value,10)||0;if(v&&(v.textContent=e+"px"),x&&(x.textContent=t+"px"),!r)return;r.style.width=e+"px",r.style.height=e+"px",r.style.borderRadius=t+"px";const i=Math.round(e*20/52),m=r.querySelector("svg, [data-icon]");m&&(m.style.width=i+"px",m.style.height=i+"px")}function w(){if(!o)return;const e=parseInt(o.max,10),t=Math.floor(k()/2),i=parseInt(o.value,10)||0;o.max=String(t),(i>=e||i>t)&&(o.value=String(t)),b&&(b.textContent=String(t))}function M(){const e=l?.checked?"":"none";I&&(I.style.display=e),h&&(h.style.display=e)}w(),y(),M(),o?.addEventListener("input",y),c?.addEventListener("input",()=>{w(),y()}),l?.addEventListener("change",M);function R(e){d&&(d.value=e),B&&(B.style.display=e==="floating"?"":"none"),p&&p.querySelectorAll(".ss-mode-btn").forEach(function(t){const i=t.getAttribute("data-mode")===e;t.classList.toggle("btn-primary",i),t.classList.toggle("btn-secondary",!i)})}function z(e){document.querySelectorAll(".ss-pos-btn").forEach(function(t){const i=t.getAttribute("data-pos")===e;t.style.background=i?"var(--dm-primary, #5b8cff)":"",t.style.color=i?"#fff":""})}R(g),z(n.position||"bottom-right"),p&&p.addEventListener("click",function(e){const t=e.target.closest(".ss-mode-btn");t&&R(t.getAttribute("data-mode"))}),S&&S.addEventListener("click",function(e){const t=e.target.closest("[data-pos]");if(!t)return;const i=t.getAttribute("data-pos");a&&(a.value=i),z(i)}),s.find("#save-settings-btn").off("click").on("click",async()=>{const e={enabled:s.find("#field-enabled").prop("checked"),placeholder:s.find("#field-placeholder").val()||"Search pages...",keyboardShortcut:s.find("#field-keyboard-shortcut").prop("checked"),maxResults:parseInt(s.find("#field-max-results").val(),10)||10,minQueryLength:parseInt(s.find("#field-min-query-length").val(),10)||2,debounceMs:parseInt(s.find("#field-debounce-ms").val(),10)||300,displayMode:d?d.value:"navbar",position:a?a.value:"bottom-right",offsetX:f&&parseInt(f.value,10)||32,offsetY:u&&parseInt(u.value,10)||32,iconOnly:l?l.checked:!0,iconSize:c&&parseInt(c.value,10)||52,iconRadius:o?parseInt(o.value,10)||0:26};try{await q("/search/settings",{method:"PUT",body:JSON.stringify(e)}),E.toast("Settings saved.",{type:"success"})}catch{E.toast("Failed to save settings.",{type:"error"})}});const L=s.find("#ss-settings-tabs").get(0);L&&E.tabs(L),Domma.icons.scan(),y()}};
@@ -1,3 +1,3 @@
1
- import{api as C,apiRequest as v}from"../api.js";import{populateThemeSelect as w}from"../lib/themes.js";export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(e){E.tabs(e.find("#settings-tabs").get(0));const k=E.loader(e.get(0),{type:"dots"}),l=await C.settings.get().catch(()=>({}));if(k.destroy(),e.find("#field-site-title").val(l.title||""),e.find("#field-tagline").val(l.tagline||""),e.find("#field-font-family").val(l.fontFamily||"Roboto"),e.find("#field-font-size").val(l.fontSize||16),w(e.find("#field-theme").get(0)),w(e.find("#field-admin-theme").get(0),{includeAdmin:!0}),e.find("#field-theme").val(l.baseTheme||l.theme||"charcoal-dark"),l.baseTheme){e.find("#field-theme").prop("disabled",!0);const t=document.createElement("p");t.className="form-hint",t.style.cssText="margin-top:.4rem;font-size:.8rem;color:var(--dm-info);";const s=document.createElement("a");s.href="#/plugins/theme-roller",s.textContent="Theme Roller",t.appendChild(document.createTextNode(`Custom theme \u201C${l.theme}\u201D is active (based on ${l.baseTheme}). Manage via `)),t.appendChild(s),t.appendChild(document.createTextNode(".")),e.find("#field-theme").get(0).closest(".col-6").appendChild(t)}e.find("#field-admin-theme").val(l.adminTheme||"charcoal-dark");const p=l.autoTheme||{},h=!!p.enabled,b=e.find("#field-auto-theme-enabled"),y=e.find("#auto-theme-fields"),u=e.find("#field-theme"),f=u.get(0).innerHTML;e.find("#field-day-theme").html(f),e.find("#field-night-theme").html(f),l.baseTheme?(b.prop("disabled",!0),e.find("#auto-theme-roller-hint").show()):(b.prop("checked",h),h&&(y.show(),u.prop("disabled",!0)),b.on("change",function(){const t=this.checked;y.toggle(t),u.prop("disabled",t)})),e.find("#field-day-theme").val(p.dayTheme||"charcoal-light"),e.find("#field-night-theme").val(p.nightTheme||"charcoal-dark"),e.find("#field-day-start").val(p.dayStart||"07:00"),e.find("#field-night-start").val(p.nightStart||"19:00"),e.find("#field-spacer-size").val(l.layoutOptions?.spacerSize??40),e.find("#field-seo-title").val(l.seo?.defaultTitle||""),e.find("#field-seo-separator").val(l.seo?.titleSeparator||" | "),e.find("#field-seo-desc").val(l.seo?.defaultDescription||""),e.find("#field-footer-copy").val(l.footer?.copyright||""),e.find("#field-footer-motion-toggle").prop("checked",l.footer?.showMotionToggle!==!1),e.find("#field-social-twitter").val(l.social?.twitter||""),e.find("#field-social-facebook").val(l.social?.facebook||""),e.find("#field-social-instagram").val(l.social?.instagram||""),e.find("#field-social-linkedin").val(l.social?.linkedin||""),e.find("#field-social-github").val(l.social?.github||""),e.find("#field-social-youtube").val(l.social?.youtube||""),e.find("#field-smtp-host").val(l.smtp?.host||""),e.find("#field-smtp-port").val(l.smtp?.port||587),e.find("#field-smtp-user").val(l.smtp?.user||""),e.find("#field-smtp-pass").val(l.smtp?.pass||""),e.find("#field-smtp-secure").prop("checked",l.smtp?.secure||!1),e.find("#field-smtp-from-address").val(l.smtp?.fromAddress||""),e.find("#field-smtp-from-name").val(l.smtp?.fromName||"");const i=l.backToTop||{};e.find("#field-btt-enabled").prop("checked",i.enabled!==!1),e.find("#field-btt-threshold").val(i.scrollThreshold??300),e.find("#field-btt-position").val(i.position||"bottom-right"),e.find("#field-btt-offset").val(i.offset??32),e.find("#field-btt-bottom-offset").val(i.bottomOffset??i.offset??32),e.find("#field-btt-label").val(i.label||""),e.find("#field-btt-smooth").prop("checked",i.smooth!==!1);const d=l.cookieConsent||{};e.find("#field-cc-enabled").prop("checked",d.enabled!==!1),e.find("#field-cc-message").val(d.message||""),e.find("#field-cc-accept-all").val(d.acceptAllText||"Accept All"),e.find("#field-cc-reject-all").val(d.rejectAllText||"Reject All"),e.find("#field-cc-customize").val(d.customizeText||"Customize"),e.find("#field-cc-save-prefs").val(d.savePreferencesText||"Save Preferences"),e.find("#field-cc-privacy-text").val(d.privacyPolicyText||"Privacy Policy"),e.find("#field-cc-privacy-url").val(d.privacyPolicyUrl||""),e.find("#field-cc-cookie-text").val(d.cookiePolicyText||"Cookie Policy"),e.find("#field-cc-cookie-url").val(d.cookiePolicyUrl||""),e.find("#field-cc-position").val(d.position||"bottom"),e.find("#field-cc-layout").val(d.layout||"bar"),e.find("#field-cc-theme").val(d.theme||"dark"),e.find("#field-cc-show-functional").prop("checked",d.showFunctional!==!1),e.find("#field-cc-show-analytics").prop("checked",d.showAnalytics!==!1),e.find("#field-cc-show-marketing").prop("checked",d.showMarketing!==!1),e.find("#field-cc-version").val(d.consentVersion||"1.0");const n=l.breadcrumbs||{};e.find("#field-breadcrumbs-enabled").prop("checked",n.enabled===!0),e.find("#field-breadcrumbs-home-label").val(n.homeLabel||"Home"),e.find("#field-breadcrumbs-position").val(n.position||"TL"),e.find("#field-bc-offset-x").val(n.offsetX??8),e.find("#field-bc-offset-y").val(n.offsetY??8);function c(t){e.find(".bc-pos-btn").each(function(){const s=this.dataset.pos===t;this.style.background=s?"var(--dm-primary, #5b8cff)":"",this.style.color=s?"var(--dm-primary-text)":""})}c(n.position||"TL"),e.find(".bc-pos-btn").on("click",function(){const t=this.dataset.pos;e.find("#field-breadcrumbs-position").val(t),c(t)});let a=null;try{const{css:t}=await v("/settings/custom-css");e.find("#field-custom-css").val(t||""),E.editor&&(a=E.editor(e.find("#field-custom-css").get(0),{mode:"code",language:"css",lineNumbers:!0,showToolbar:!1,minHeight:420,placeholder:"/* Add your custom CSS here */",characterCount:!0}))}catch{}a&&a._editorEl&&a._editorEl.addEventListener("keydown",t=>{if(!t.ctrlKey&&!t.metaKey)return;if(t.key==="s"){t.preventDefault(),e.find("#save-css-btn").get(0)?.click();return}const s=a._editorEl,m=s.selectionStart!==s.selectionEnd;if((t.key==="c"||t.key==="x")&&!m){t.preventDefault();const o=s.value,g=s.selectionStart,T=o.lastIndexOf(`
2
- `,g-1)+1,x=o.indexOf(`
3
- `,g),S=x===-1?o.slice(T):o.slice(T,x+1);if(navigator.clipboard.writeText(S),t.key==="x"){const z=o.slice(0,T),P=x===-1?"":o.slice(x+1);s.value=z+P,s.selectionStart=s.selectionEnd=T,s.dispatchEvent(new Event("input",{bubbles:!0})),E.toast("Line cut",{type:"info",duration:1500})}else E.toast("Line copied",{type:"info",duration:1500})}}),e.find("#save-settings-btn").on("click",async()=>{const t=e.find("#field-admin-theme").val(),s=!l.baseTheme&&e.find("#field-auto-theme-enabled").prop("checked"),m={enabled:s,dayTheme:e.find("#field-day-theme").val()||"charcoal-light",nightTheme:e.find("#field-night-theme").val()||"charcoal-dark",dayStart:e.find("#field-day-start").val()||"07:00",nightStart:e.find("#field-night-start").val()||"19:00"},o={title:e.find("#field-site-title").val().trim(),tagline:e.find("#field-tagline").val().trim(),fontFamily:e.find("#field-font-family").val()||"Roboto",fontSize:parseInt(e.find("#field-font-size").val(),10)||16,theme:l.baseTheme?l.theme:s?m.dayTheme:e.find("#field-theme").val(),...l.baseTheme?{baseTheme:l.baseTheme}:{},autoTheme:m,adminTheme:t,layoutOptions:{spacerSize:parseInt(e.find("#field-spacer-size").val(),10)||40},seo:{defaultTitle:e.find("#field-seo-title").val().trim(),titleSeparator:e.find("#field-seo-separator").val()||" | ",defaultDescription:e.find("#field-seo-desc").val().trim()},footer:{copyright:e.find("#field-footer-copy").val().trim(),showMotionToggle:e.find("#field-footer-motion-toggle").prop("checked"),links:l.footer?.links||[]},social:{twitter:e.find("#field-social-twitter").val().trim(),facebook:e.find("#field-social-facebook").val().trim(),instagram:e.find("#field-social-instagram").val().trim(),linkedin:e.find("#field-social-linkedin").val().trim(),github:e.find("#field-social-github").val().trim(),youtube:e.find("#field-social-youtube").val().trim()},smtp:{host:e.find("#field-smtp-host").val().trim(),port:parseInt(e.find("#field-smtp-port").val(),10)||587,user:e.find("#field-smtp-user").val().trim(),pass:e.find("#field-smtp-pass").val(),secure:e.find("#field-smtp-secure").prop("checked"),fromAddress:e.find("#field-smtp-from-address").val().trim(),fromName:e.find("#field-smtp-from-name").val().trim()},backToTop:{enabled:e.find("#field-btt-enabled").prop("checked"),scrollThreshold:parseInt(e.find("#field-btt-threshold").val(),10)||300,position:e.find("#field-btt-position").val()||"bottom-right",offset:parseInt(e.find("#field-btt-offset").val(),10)||32,bottomOffset:parseInt(e.find("#field-btt-bottom-offset").val(),10)||32,label:e.find("#field-btt-label").val().trim(),smooth:e.find("#field-btt-smooth").prop("checked")},cookieConsent:{enabled:e.find("#field-cc-enabled").prop("checked"),message:e.find("#field-cc-message").val().trim(),acceptAllText:e.find("#field-cc-accept-all").val().trim(),rejectAllText:e.find("#field-cc-reject-all").val().trim(),customizeText:e.find("#field-cc-customize").val().trim(),savePreferencesText:e.find("#field-cc-save-prefs").val().trim(),privacyPolicyText:e.find("#field-cc-privacy-text").val().trim(),privacyPolicyUrl:e.find("#field-cc-privacy-url").val().trim(),cookiePolicyText:e.find("#field-cc-cookie-text").val().trim(),cookiePolicyUrl:e.find("#field-cc-cookie-url").val().trim(),position:e.find("#field-cc-position").val(),layout:e.find("#field-cc-layout").val(),theme:e.find("#field-cc-theme").val(),showFunctional:e.find("#field-cc-show-functional").prop("checked"),showAnalytics:e.find("#field-cc-show-analytics").prop("checked"),showMarketing:e.find("#field-cc-show-marketing").prop("checked"),consentVersion:e.find("#field-cc-version").val().trim()||"1.0"}},g=e.find("#save-settings-btn");g.prop("disabled",!0);try{await C.settings.save(o),Domma.theme.set(t),E.toast("Settings saved.",{type:"success"})}catch{E.toast("Failed to save settings.",{type:"error"})}finally{g.prop("disabled",!1)}}),e.find("#send-test-email-btn").on("click",async()=>{const t=e.find("#field-test-email-to").val().trim(),s=e.find("#test-email-result").get(0),m=e.find("#send-test-email-btn");m.prop("disabled",!0),s&&(s.textContent="Sending\u2026",s.style.color="");try{const o=await v("/settings/test-email",{method:"POST",body:JSON.stringify({to:t||void 0})});s&&(s.textContent=o.message||"Test email sent.",s.style.color="var(--dm-success,#4ade80)")}catch(o){s&&(s.textContent=o.message||"Failed to send test email.",s.style.color="var(--dm-danger,#f87171)")}finally{m.prop("disabled",!1)}}),e.find("#save-breadcrumbs-btn").on("click",async()=>{const t=e.find("#save-breadcrumbs-btn");t.prop("disabled",!0);try{const s={enabled:e.find("#field-breadcrumbs-enabled").prop("checked"),homeLabel:e.find("#field-breadcrumbs-home-label").val().trim()||"Home",position:e.find("#field-breadcrumbs-position").val()||"TL",offsetX:parseInt(e.find("#field-bc-offset-x").val(),10)||8,offsetY:parseInt(e.find("#field-bc-offset-y").val(),10)||8};await C.settings.save({breadcrumbs:s}),l.breadcrumbs=s,E.toast("Breadcrumbs settings saved.",{type:"success"})}catch{E.toast("Failed to save breadcrumbs settings.",{type:"error"})}finally{t.prop("disabled",!1)}}),e.find("#save-css-btn").on("click",async()=>{const t=a?a.getValue():e.find("#field-custom-css").val(),s=e.find("#save-css-btn");s.prop("disabled",!0);try{await v("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:t})}),E.toast("Custom CSS saved.",{type:"success"})}catch(m){E.toast(m.message||"Failed to save CSS.",{type:"error"})}finally{s.prop("disabled",!1)}});async function r(){try{const t=await v("/cache/status"),s=t.enabled?`enabled (${t.driver})`:"disabled",m=t.maxItems?` \xB7 ${t.size} / ${t.maxItems} entries`:"";e.find("#cache-status").text(s+m)}catch{e.find("#cache-status").text("unknown")}try{const{entries:t}=await v("/cache/keys");A(e.find("#cache-keys-table"),t),e.find("#cache-key-count").text(`(${t.length})`)}catch(t){e.find("#cache-keys-table").get(0).textContent="Failed to load keys: "+t.message}}await r(),e.find("#btn-clear-cache").on("click",async()=>{if(!await E.confirm("Clear the entire response cache?"))return;const t=e.find("#btn-clear-cache");t.prop("disabled",!0);try{await v("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"}),await r()}catch(s){E.toast(s.message||"Failed to clear cache.",{type:"error"})}finally{t.prop("disabled",!1)}}),e.find("#btn-refresh-cache-keys").on("click",r)}};function A(e,k){const l=e.get(0);if(!l)return;for(;l.firstChild;)l.removeChild(l.firstChild);if(!k.length){const f=document.createElement("p");f.className="text-muted",f.style.fontSize=".875rem",f.textContent="Cache is empty.",l.appendChild(f);return}const p=[...k].reverse(),h=document.createElement("table");h.className="table table-compact";const b=document.createElement("thead"),y=document.createElement("tr");for(const f of["Key","Tags","Expires"]){const i=document.createElement("th");i.textContent=f,y.appendChild(i)}b.appendChild(y),h.appendChild(b);const u=document.createElement("tbody");for(const f of p){const i=document.createElement("tr"),d=document.createElement("td");d.style.fontFamily="monospace",d.style.fontSize="12px",d.textContent=f.key,i.appendChild(d);const n=document.createElement("td");n.style.fontSize="12px";for(const a of f.tags){const r=document.createElement("span");r.className="badge",r.style.marginRight="4px",r.style.fontSize="11px",r.textContent=a,n.appendChild(r)}i.appendChild(n);const c=document.createElement("td");if(c.style.fontSize="12px",c.style.color="var(--dm-text-muted, #6b7280)",f.expiresAt===null)c.textContent="no expiry";else{const a=f.expiresAt-Date.now();a<=0?c.textContent="expired":a<6e4?c.textContent=`${Math.round(a/1e3)}s`:a<36e5?c.textContent=`${Math.round(a/6e4)}m`:c.textContent=`${Math.round(a/36e5)}h`}i.appendChild(c),u.appendChild(i)}h.appendChild(u),l.appendChild(h)}
1
+ import{api as v,apiRequest as b}from"../api.js";export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(e){E.tabs(e.find("#settings-tabs").get(0));const p=E.loader(e.get(0),{type:"dots"}),l=await v.settings.get().catch(()=>({}));p.destroy(),e.find("#field-site-title").val(l.title||""),e.find("#field-tagline").val(l.tagline||""),e.find("#field-spacer-size").val(l.layoutOptions?.spacerSize??40),e.find("#field-seo-title").val(l.seo?.defaultTitle||""),e.find("#field-seo-separator").val(l.seo?.titleSeparator||" | "),e.find("#field-seo-desc").val(l.seo?.defaultDescription||""),e.find("#field-footer-copy").val(l.footer?.copyright||""),e.find("#field-footer-motion-toggle").prop("checked",l.footer?.showMotionToggle!==!1),e.find("#field-social-twitter").val(l.social?.twitter||""),e.find("#field-social-facebook").val(l.social?.facebook||""),e.find("#field-social-instagram").val(l.social?.instagram||""),e.find("#field-social-linkedin").val(l.social?.linkedin||""),e.find("#field-social-github").val(l.social?.github||""),e.find("#field-social-youtube").val(l.social?.youtube||""),e.find("#field-smtp-host").val(l.smtp?.host||""),e.find("#field-smtp-port").val(l.smtp?.port||587),e.find("#field-smtp-user").val(l.smtp?.user||""),e.find("#field-smtp-pass").val(l.smtp?.pass||""),e.find("#field-smtp-secure").prop("checked",l.smtp?.secure||!1),e.find("#field-smtp-from-address").val(l.smtp?.fromAddress||""),e.find("#field-smtp-from-name").val(l.smtp?.fromName||""),y(l.smtp?.host||""),e.find("#field-smtp-host").on("input",function(){y(this.value)});const c=l.backToTop||{};e.find("#field-btt-enabled").prop("checked",c.enabled!==!1),e.find("#field-btt-threshold").val(c.scrollThreshold??300),e.find("#field-btt-position").val(c.position||"bottom-right"),e.find("#field-btt-offset").val(c.offset??32),e.find("#field-btt-bottom-offset").val(c.bottomOffset??c.offset??32),e.find("#field-btt-label").val(c.label||""),e.find("#field-btt-smooth").prop("checked",c.smooth!==!1);const i=l.cookieConsent||{};e.find("#field-cc-enabled").prop("checked",i.enabled!==!1),e.find("#field-cc-message").val(i.message||""),e.find("#field-cc-accept-all").val(i.acceptAllText||"Accept All"),e.find("#field-cc-reject-all").val(i.rejectAllText||"Reject All"),e.find("#field-cc-customize").val(i.customizeText||"Customize"),e.find("#field-cc-save-prefs").val(i.savePreferencesText||"Save Preferences"),e.find("#field-cc-privacy-text").val(i.privacyPolicyText||"Privacy Policy"),e.find("#field-cc-privacy-url").val(i.privacyPolicyUrl||""),e.find("#field-cc-cookie-text").val(i.cookiePolicyText||"Cookie Policy"),e.find("#field-cc-cookie-url").val(i.cookiePolicyUrl||""),e.find("#field-cc-position").val(i.position||"bottom"),e.find("#field-cc-layout").val(i.layout||"bar"),e.find("#field-cc-theme").val(i.theme||"dark"),e.find("#field-cc-show-functional").prop("checked",i.showFunctional!==!1),e.find("#field-cc-show-analytics").prop("checked",i.showAnalytics!==!1),e.find("#field-cc-show-marketing").prop("checked",i.showMarketing!==!1),e.find("#field-cc-version").val(i.consentVersion||"1.0");const m=l.breadcrumbs||{};e.find("#field-breadcrumbs-enabled").prop("checked",m.enabled===!0),e.find("#field-breadcrumbs-home-label").val(m.homeLabel||"Home"),e.find("#field-breadcrumbs-position").val(m.position||"TL"),e.find("#field-bc-offset-x").val(m.offsetX??8),e.find("#field-bc-offset-y").val(m.offsetY??8);function u(t){e.find(".bc-pos-btn").each(function(){const s=this.dataset.pos===t;this.style.background=s?"var(--dm-primary, #5b8cff)":"",this.style.color=s?"var(--dm-primary-text)":""})}u(m.position||"TL"),e.find(".bc-pos-btn").on("click",function(){const t=this.dataset.pos;e.find("#field-breadcrumbs-position").val(t),u(t)});let n=null;try{const{css:t}=await b("/settings/custom-css");e.find("#field-custom-css").val(t||""),E.editor&&(n=E.editor(e.find("#field-custom-css").get(0),{mode:"code",language:"css",lineNumbers:!0,showToolbar:!1,minHeight:420,placeholder:"/* Add your custom CSS here */",characterCount:!0}))}catch{}n&&n._editorEl&&n._editorEl.addEventListener("keydown",t=>{if(!t.ctrlKey&&!t.metaKey)return;if(t.key==="s"){t.preventDefault(),e.find("#save-css-btn").get(0)?.click();return}const s=n._editorEl,a=s.selectionStart!==s.selectionEnd;if((t.key==="c"||t.key==="x")&&!a){t.preventDefault();const d=s.value,o=s.selectionStart,r=d.lastIndexOf(`
2
+ `,o-1)+1,h=d.indexOf(`
3
+ `,o),g=h===-1?d.slice(r):d.slice(r,h+1);if(navigator.clipboard.writeText(g),t.key==="x"){const k=d.slice(0,r),x=h===-1?"":d.slice(h+1);s.value=k+x,s.selectionStart=s.selectionEnd=r,s.dispatchEvent(new Event("input",{bubbles:!0})),E.toast("Line cut",{type:"info",duration:1500})}else E.toast("Line copied",{type:"info",duration:1500})}}),e.find("#save-settings-btn").on("click",async()=>{const t={title:e.find("#field-site-title").val().trim(),tagline:e.find("#field-tagline").val().trim(),layoutOptions:{spacerSize:parseInt(e.find("#field-spacer-size").val(),10)||40},seo:{defaultTitle:e.find("#field-seo-title").val().trim(),titleSeparator:e.find("#field-seo-separator").val()||" | ",defaultDescription:e.find("#field-seo-desc").val().trim()},footer:{copyright:e.find("#field-footer-copy").val().trim(),showMotionToggle:e.find("#field-footer-motion-toggle").prop("checked"),links:l.footer?.links||[]},social:{twitter:e.find("#field-social-twitter").val().trim(),facebook:e.find("#field-social-facebook").val().trim(),instagram:e.find("#field-social-instagram").val().trim(),linkedin:e.find("#field-social-linkedin").val().trim(),github:e.find("#field-social-github").val().trim(),youtube:e.find("#field-social-youtube").val().trim()},smtp:{host:e.find("#field-smtp-host").val().trim(),port:parseInt(e.find("#field-smtp-port").val(),10)||587,user:e.find("#field-smtp-user").val().trim(),pass:e.find("#field-smtp-pass").val(),secure:e.find("#field-smtp-secure").prop("checked"),fromAddress:e.find("#field-smtp-from-address").val().trim(),fromName:e.find("#field-smtp-from-name").val().trim()},backToTop:{enabled:e.find("#field-btt-enabled").prop("checked"),scrollThreshold:parseInt(e.find("#field-btt-threshold").val(),10)||300,position:e.find("#field-btt-position").val()||"bottom-right",offset:parseInt(e.find("#field-btt-offset").val(),10)||32,bottomOffset:parseInt(e.find("#field-btt-bottom-offset").val(),10)||32,label:e.find("#field-btt-label").val().trim(),smooth:e.find("#field-btt-smooth").prop("checked")},cookieConsent:{enabled:e.find("#field-cc-enabled").prop("checked"),message:e.find("#field-cc-message").val().trim(),acceptAllText:e.find("#field-cc-accept-all").val().trim(),rejectAllText:e.find("#field-cc-reject-all").val().trim(),customizeText:e.find("#field-cc-customize").val().trim(),savePreferencesText:e.find("#field-cc-save-prefs").val().trim(),privacyPolicyText:e.find("#field-cc-privacy-text").val().trim(),privacyPolicyUrl:e.find("#field-cc-privacy-url").val().trim(),cookiePolicyText:e.find("#field-cc-cookie-text").val().trim(),cookiePolicyUrl:e.find("#field-cc-cookie-url").val().trim(),position:e.find("#field-cc-position").val(),layout:e.find("#field-cc-layout").val(),theme:e.find("#field-cc-theme").val(),showFunctional:e.find("#field-cc-show-functional").prop("checked"),showAnalytics:e.find("#field-cc-show-analytics").prop("checked"),showMarketing:e.find("#field-cc-show-marketing").prop("checked"),consentVersion:e.find("#field-cc-version").val().trim()||"1.0"}},s=e.find("#save-settings-btn");s.prop("disabled",!0);try{await v.settings.save(t),E.toast("Settings saved.",{type:"success"})}catch{E.toast("Failed to save settings.",{type:"error"})}finally{s.prop("disabled",!1)}}),e.find("#send-test-email-btn").on("click",async()=>{const t=e.find("#field-test-email-to").val().trim(),s=e.find("#test-email-result").get(0),a=e.find("#send-test-email-btn");a.prop("disabled",!0),s&&(s.textContent="Sending\u2026",s.style.color="");try{const d=await b("/settings/test-email",{method:"POST",body:JSON.stringify({to:t||void 0})});s&&(s.textContent=d.message||"Test email sent.",s.style.color="var(--dm-success,#4ade80)")}catch(d){s&&(s.textContent=d.message||"Failed to send test email.",s.style.color="var(--dm-danger,#f87171)")}finally{a.prop("disabled",!1)}}),e.find("#save-breadcrumbs-btn").on("click",async()=>{const t=e.find("#save-breadcrumbs-btn");t.prop("disabled",!0);try{const s={enabled:e.find("#field-breadcrumbs-enabled").prop("checked"),homeLabel:e.find("#field-breadcrumbs-home-label").val().trim()||"Home",position:e.find("#field-breadcrumbs-position").val()||"TL",offsetX:parseInt(e.find("#field-bc-offset-x").val(),10)||8,offsetY:parseInt(e.find("#field-bc-offset-y").val(),10)||8};await v.settings.save({breadcrumbs:s}),l.breadcrumbs=s,E.toast("Breadcrumbs settings saved.",{type:"success"})}catch{E.toast("Failed to save breadcrumbs settings.",{type:"error"})}finally{t.prop("disabled",!1)}}),e.find("#save-css-btn").on("click",async()=>{const t=n?n.getValue():e.find("#field-custom-css").val(),s=e.find("#save-css-btn");s.prop("disabled",!0);try{await b("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:t})}),E.toast("Custom CSS saved.",{type:"success"})}catch(a){E.toast(a.message||"Failed to save CSS.",{type:"error"})}finally{s.prop("disabled",!1)}});async function f(){try{const t=await b("/cache/status"),s=t.enabled?`enabled (${t.driver})`:"disabled",a=t.maxItems?` \xB7 ${t.size} / ${t.maxItems} entries`:"";e.find("#cache-status").text(s+a)}catch{e.find("#cache-status").text("unknown")}try{const{entries:t}=await b("/cache/keys");w(e.find("#cache-keys-table"),t),e.find("#cache-key-count").text(`(${t.length})`)}catch(t){e.find("#cache-keys-table").get(0).textContent="Failed to load keys: "+t.message}}await f(),e.find("#btn-clear-cache").on("click",async()=>{if(!await E.confirm("Clear the entire response cache?"))return;const t=e.find("#btn-clear-cache");t.prop("disabled",!0);try{await b("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"}),await f()}catch(s){E.toast(s.message||"Failed to clear cache.",{type:"error"})}finally{t.prop("disabled",!1)}}),e.find("#btn-refresh-cache-keys").on("click",f)}};function y(e){const p=document.getElementById("smtp-unconfigured-notice");p&&(p.style.display=String(e||"").trim()?"none":"",p.style.display===""&&Domma.icons.scan(p))}function w(e,p){const l=e.get(0);if(!l)return;for(;l.firstChild;)l.removeChild(l.firstChild);if(!p.length){const f=document.createElement("p");f.className="text-muted",f.style.fontSize=".875rem",f.textContent="Cache is empty.",l.appendChild(f);return}const c=[...p].reverse(),i=document.createElement("table");i.className="table table-compact";const m=document.createElement("thead"),u=document.createElement("tr");for(const f of["Key","Tags","Expires"]){const t=document.createElement("th");t.textContent=f,u.appendChild(t)}m.appendChild(u),i.appendChild(m);const n=document.createElement("tbody");for(const f of c){const t=document.createElement("tr"),s=document.createElement("td");s.style.fontFamily="monospace",s.style.fontSize="12px",s.textContent=f.key,t.appendChild(s);const a=document.createElement("td");a.style.fontSize="12px";for(const o of f.tags){const r=document.createElement("span");r.className="badge",r.style.marginRight="4px",r.style.fontSize="11px",r.textContent=o,a.appendChild(r)}t.appendChild(a);const d=document.createElement("td");if(d.style.fontSize="12px",d.style.color="var(--dm-text-muted, #6b7280)",f.expiresAt===null)d.textContent="no expiry";else{const o=f.expiresAt-Date.now();o<=0?d.textContent="expired":o<6e4?d.textContent=`${Math.round(o/1e3)}s`:o<36e5?d.textContent=`${Math.round(o/6e4)}m`:d.textContent=`${Math.round(o/36e5)}h`}t.appendChild(d),n.appendChild(t)}i.appendChild(n),l.appendChild(i)}
@@ -0,0 +1 @@
1
+ import{api as T}from"../api.js";import{populateThemeSelect as w}from"../lib/themes.js";const se=[{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(r){E.tabs(r.find("#theme-tabs").get(0));const F=E.loader(r.get(0),{type:"dots"});let d,R;try{[d,R]=await Promise.all([T.theme.get(),T.theme.catalog().catch(()=>({themes:[],tokens:[]}))])}catch(e){F.destroy(),E.toast(`Could not load theme settings: ${e.message||e}`,{type:"error"});return}F.destroy();const z=R.tokens||[],A=new Map;async function K(e){if(!e)return null;if(A.has(e))return A.get(e);const t=await T.theme.themeTokens(e).catch(()=>null),n=t&&t.tokens?t.tokens:null;return A.set(e,n),n}function $(){const e=r.find("#theme-auto-enabled").prop("checked");return d.baseTheme||(e?S.val():f.val())||d.theme}const V=["--dm-card-bg","--dm-surface"],_=[...V,"--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 I(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=$();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,s]=await Promise.all([K(o),K(n)]);if(!a||!s)return;const c=Object.keys(s).filter(l=>String(a[l]??"").toLowerCase()!==String(s[l]).toLowerCase()),i=V.filter(l=>c.includes(l)),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(s).length} tokens here.`,m.appendChild(h),!i.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.",m.appendChild(l)}const u=document.createElement("div");u.style.cssText="display:flex;flex-wrap:wrap;gap:.15rem .75rem;";const v=[..._.filter(l=>c.includes(l)),...c.filter(l=>!_.includes(l))];for(const l of v.slice(0,6)){const p=document.createElement("span");p.style.cssText="white-space:nowrap;color:var(--dm-text-muted);",p.appendChild(H(a[l]||""));const C=document.createElement("span");C.textContent=`${l} `,p.appendChild(C),p.appendChild(H(s[l])),u.appendChild(p)}if(v.length>6){const l=document.createElement("span");l.style.cssText="color:var(--dm-text-muted);",l.textContent=`+${v.length-6} more`,u.appendChild(l)}m.appendChild(u),t.appendChild(m)}let b=Array.isArray(d.overrides)?d.overrides.map(G):[];const f=r.find("#theme-frontend"),B=r.find("#theme-admin");w(f.get(0)),w(B.get(0),{includeAdmin:!0}),f.val(d.baseTheme||d.theme||"charcoal-dark"),B.val(d.adminTheme||"charcoal-dark"),d.baseTheme&&(f.prop("disabled",!0),r.find("#theme-auto-enabled").prop("disabled",!0),r.find("#theme-auto-roller-hint").show());const S=r.find("#theme-day"),D=r.find("#theme-night");w(S.get(0)),w(D.get(0));const g=d.autoTheme||{};S.val(g.dayTheme||"charcoal-light"),D.val(g.nightTheme||"charcoal-dark"),r.find("#theme-day-start").val(g.dayStart||"07:00"),r.find("#theme-night-start").val(g.nightStart||"19:00");const U=r.find("#theme-auto-enabled"),W=r.find("#theme-auto-fields");d.baseTheme||(U.prop("checked",!!g.enabled),g.enabled&&(W.show(),f.prop("disabled",!0)),U.on("change",function(){W.toggle(this.checked),f.prop("disabled",this.checked)})),r.find("#theme-font-family").val(d.font?.family||"Roboto"),r.find("#theme-font-size").val(String(d.font?.size??16));function G(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"),ee=document.getElementById("theme-overrides-empty");function L(){for(;y.firstChild;)y.removeChild(y.firstChild);ee.style.display=b.length?"none":"",b.forEach((e,t)=>y.appendChild(te(e,t))),Domma.icons.scan("#theme-overrides")}function te(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 s=document.createElement("div");s.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")),s.appendChild(c);const m=document.createElement("button");m.type="button",m.className="btn btn-ghost btn-sm ov-remove",m.textContent="Remove",s.appendChild(m),o.appendChild(s),n.appendChild(o);const h=document.createElement("div");h.className="card-body";const u=document.createElement("div");u.className="row mb-3",u.appendChild(P("col-4","Name",X("ov-label",e.label,"Navbar island"))),u.appendChild(P("col-8","Target selector",ne(e.scope),"The region to re-theme. Everything inside it follows.")),h.appendChild(u);const v=document.createElement("div");v.className="row mb-3";const l=document.createElement("select");l.className="form-select ov-theme",w(l,{includeDefault:!0,defaultLabel:"Keep the site theme"}),l.value=e.theme||"",v.appendChild(P("col-6","Adopt a theme here",l,"Copies every token of that theme onto this region.")),h.appendChild(v);const p=document.createElement("div");p.className="ov-theme-diff",h.appendChild(p);const C=document.createElement("div");C.className="ov-tokens mb-2",Object.entries(e.tokens||{}).forEach(([oe,ae])=>{C.appendChild(J(oe,ae))}),h.appendChild(Q("Token overrides",C));const q=document.createElement("button");q.type="button",q.className="btn btn-ghost btn-sm ov-add-token",q.textContent="+ Add token",h.appendChild(q);const x=document.createElement("details");x.style.marginTop="1rem";const j=document.createElement("summary");j.style.cssText="cursor:pointer;font-size:.85rem;",j.textContent="Custom CSS for this override",x.appendChild(j);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",x.appendChild(k);const O=document.createElement("p");return O.className="form-hint text-muted",O.style.cssText="font-size:.8rem;margin-top:.35rem;",O.textContent="Appended verbatim after the generated rule - you own these selectors. Prefer tokens: they survive a Domma upgrade, hand-written rules fight the cascade.",x.appendChild(O),h.appendChild(x),n.appendChild(h),setTimeout(()=>I(n),0),n}function J(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",z.length){for(const i of z){const m=document.createElement("option");m.value=i,m.textContent=i,o.appendChild(m)}o.value=e||z[0]}else{const i=document.createElement("option");i.value=e||"",i.textContent=e||"(no tokens found)",o.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 s=document.createElement("input");s.type="color",s.className="ov-token-swatch",s.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||"")&&(s.value=t),s.addEventListener("input",()=>{a.value=s.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,s,c),n}function P(e,t,n,o){const a=document.createElement("div");return a.className=e,a.appendChild(Q(t,n,o)),a}function Q(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 s=document.createElement("p");s.className="form-hint text-muted",s.style.cssText="margin-top:.35rem;font-size:.8rem;",s.textContent=n,o.appendChild(s)}return o}function X(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 ne(e){const t=document.createElement("div"),n=X("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 se){const s=document.createElement("option");s.value=a.value,s.label=a.label,o.appendChild(s)}t.appendChild(o)}return t}function N(){const e=y.querySelectorAll(".card"),t=[];return e.forEach((n,o)=>{const a={};n.querySelectorAll(".ov-token-row").forEach(s=>{const c=s.querySelector(".ov-token-name")?.value?.trim(),i=s.querySelector(".ov-token-value")?.value?.trim();c&&i&&(a[c]=i)}),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}r.on("click",".ov-remove",function(){const e=Number(this.closest(".card").dataset.i);N(),b.splice(e,1),L()}),r.on("change",".ov-theme",function(){I(this.closest(".card"))}),r.on("change","#theme-frontend, #theme-auto-enabled, #theme-day",()=>{y.querySelectorAll(".card").forEach(I)}),r.on("click",".ov-add-token",function(){this.closest(".card").querySelector(".ov-tokens").appendChild(J("",""))}),r.on("click",".ov-token-remove",function(){this.closest(".ov-token-row").remove()}),r.find("#theme-add-override").on("click",()=>{N(),b.push(G({label:"",scope:"",enabled:!0})),L()}),L();async function M(){const e=document.getElementById("theme-css-preview"),t={...Y(),overrides:N()};try{const n=await T.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)"}}r.find("#theme-refresh-css").on("click",M);function Y(){const e=!d.baseTheme&&r.find("#theme-auto-enabled").prop("checked"),t={enabled:e,dayTheme:S.val()||"charcoal-light",nightTheme:D.val()||"charcoal-dark",dayStart:r.find("#theme-day-start").val()||"07:00",nightStart:r.find("#theme-night-start").val()||"19:00"};return{theme:d.baseTheme?d.theme:e?t.dayTheme:f.val(),...d.baseTheme?{baseTheme:d.baseTheme}:{},adminTheme:B.val(),autoTheme:t,font:{family:r.find("#theme-font-family").val()||"Roboto",size:parseInt(r.find("#theme-font-size").val(),10)||16}}}function Z(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"})}r.find("#theme-save-btn").on("click",async function(){Z(null);const e={...Y(),overrides:N()};this.disabled=!0;try{d=await T.theme.save(e),Domma.theme.set(e.adminTheme),E.toast("Theme saved.",{type:"success"}),await M()}catch(t){const n=t.validation||[t.message||"Could not save the theme."];Z(n),E.toast("Theme not saved.",{type:"error"})}finally{this.disabled=!1}}),await M()}};
package/bin/cli.js CHANGED
@@ -8,6 +8,7 @@ import {cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync}
8
8
  import {spawnSync} from 'node:child_process';
9
9
  import path from 'node:path';
10
10
  import {fileURLToPath} from 'node:url';
11
+ import {composeSmtp} from './lib/smtp-defaults.js';
11
12
 
12
13
  // ---------------------------------------------------------------------------
13
14
  // Paths
@@ -54,6 +55,7 @@ if (flags.has('--help') || args.includes('-h')) {
54
55
  --no-backup Skip creating a backup before updating
55
56
  --no-install Skip npm install after updating
56
57
  --dry-run Preview what would change without writing anything
58
+ --force Re-install even when the installed version already matches
57
59
 
58
60
  Examples:
59
61
  npx domma-cms my-blog
@@ -61,6 +63,7 @@ if (flags.has('--help') || args.includes('-h')) {
61
63
  npx domma-cms update
62
64
  npx domma-cms update --dry-run
63
65
  npx domma-cms update --yes --no-backup
66
+ npx domma-cms update --yes --force
64
67
  `);
65
68
  process.exit(0);
66
69
  }
@@ -205,8 +208,13 @@ done();
205
208
  // Reset config to clean defaults
206
209
  // ---------------------------------------------------------------------------
207
210
 
211
+ // One placeholder name, used for the title and for the From name the SMTP
212
+ // block is seeded with. `npm run setup` renames both together, and only while
213
+ // they still agree - see scripts/setup.js.
214
+ const DEFAULT_SITE_TITLE = 'My Site';
215
+
208
216
  const DEFAULT_SITE = {
209
- title: 'My Site',
217
+ title: DEFAULT_SITE_TITLE,
210
218
  tagline: 'Powered by Domma CMS',
211
219
  theme: 'charcoal-dark',
212
220
  seo: {
@@ -220,7 +228,8 @@ const DEFAULT_SITE = {
220
228
  {text: 'Privacy Policy', url: '/privacy'},
221
229
  {text: 'Contact', url: '/contact'}
222
230
  ]
223
- }
231
+ },
232
+ smtp: composeSmtp(process.env, DEFAULT_SITE_TITLE)
224
233
  };
225
234
 
226
235
  const DEFAULT_NAV = {
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Domma CMS - SMTP defaults for a newly scaffolded site.
3
+ *
4
+ * Kept out of bin/cli.js because that file is a script: it parses argv and
5
+ * writes a project the moment it is loaded, so nothing in it can be imported
6
+ * by a test. This module is pure.
7
+ */
8
+
9
+ /**
10
+ * Compose the SMTP block seeded into a new site's config/site.json.
11
+ *
12
+ * A site.json with a missing or empty `smtp.host` does not fail - it silently
13
+ * routes every email through a throwaway Ethereal test account, which reports
14
+ * success and delivers to nobody. Password resets and form notifications then
15
+ * "work" right up until someone asks why they never arrived. So a new site
16
+ * always ships with a host, and with the whole block present so the fields are
17
+ * visible in Settings from the first boot.
18
+ *
19
+ * Precedence per field: environment → a local MTA on port 25. The fleet
20
+ * manager's own scaffolder (plugins/site-manager/services/siteScaffolder.js,
21
+ * `composeSmtp`) does the same thing one layer up, inheriting the manager's
22
+ * relay host/port; this is the equivalent for a site created straight from the
23
+ * CLI, where there is no manager to inherit from.
24
+ *
25
+ * Credentials are read from the environment rather than baked in, so nothing
26
+ * secret lives in the scaffolder. They are written into config/site.json,
27
+ * which is where this CMS keeps them - that file is yours and is never merged
28
+ * by the updater, but it is not a secret store either, so treat a site
29
+ * directory holding a relay password accordingly.
30
+ *
31
+ * @param {Record<string, string|undefined>} [env] Defaults to process.env
32
+ * @param {string} [siteName] Display name, used as the From name
33
+ * @returns {{host: string, port: number, secure: boolean, user: string, pass: string, fromAddress: string, fromName: string}}
34
+ */
35
+ export function composeSmtp(env = process.env, siteName = 'My Site') {
36
+ const host = env.SMTP_HOST || '';
37
+ const port = Number.parseInt(env.SMTP_PORT, 10);
38
+ return {
39
+ // No host anywhere means a local MTA, which is the one thing that
40
+ // might plausibly be listening on a fresh box. It is still a guess,
41
+ // which is why the admin warns until someone confirms it.
42
+ host: host || 'localhost',
43
+ // 587 is the submission port a real relay listens on; 25 is what a
44
+ // local MTA takes. Defaulting to 25 for an env-supplied host would
45
+ // point a correctly configured relay at the wrong port.
46
+ port: Number.isFinite(port) && port > 0 ? port : (host ? 587 : 25),
47
+ secure: /^(1|true|yes|on)$/i.test(env.SMTP_SECURE || ''),
48
+ user: env.SMTP_USER || '',
49
+ pass: env.SMTP_PASS || '',
50
+ fromAddress: env.SMTP_FROM || '',
51
+ fromName: siteName
52
+ };
53
+ }