domma-cms 0.66.0 → 0.69.4

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 +64 -1
  2. package/README.md +14 -10
  3. package/admin/css/admin.css +1 -1
  4. package/admin/index.html +1 -0
  5. package/admin/js/api.js +1 -1
  6. package/admin/js/app.js +4 -4
  7. package/admin/js/lib/analytics-shape.js +1 -0
  8. package/admin/js/lib/contacts-arrange.js +1 -0
  9. package/admin/js/lib/help-popover.js +1 -0
  10. package/admin/js/lib/notes-arrange.js +1 -0
  11. package/admin/js/lib/plugin-accent.js +1 -1
  12. package/admin/js/lib/plugin-chrome.js +1 -1
  13. package/admin/js/lib/sidebar-grouping.js +1 -1
  14. package/admin/js/lib/sidebar-grouping.test.js +1 -1
  15. package/admin/js/lib/sidebar-renderer.js +3 -3
  16. package/admin/js/lib/todo-arrange.js +1 -0
  17. package/admin/js/lib/tool-kit.js +1 -0
  18. package/admin/js/templates/analytics.html +138 -0
  19. package/admin/js/templates/contacts.html +156 -0
  20. package/admin/js/templates/notes.html +137 -0
  21. package/admin/js/templates/plugin-marketplace.html +17 -4
  22. package/admin/js/templates/plugins.html +13 -7
  23. package/admin/js/templates/theme.html +38 -0
  24. package/admin/js/templates/todo.html +113 -0
  25. package/admin/js/views/analytics.js +62 -0
  26. package/admin/js/views/contacts.js +174 -0
  27. package/admin/js/views/index.js +1 -1
  28. package/admin/js/views/notes.js +104 -0
  29. package/admin/js/views/plugin-marketplace.js +1 -1
  30. package/admin/js/views/plugins.js +24 -22
  31. package/admin/js/views/theme.js +1 -1
  32. package/admin/js/views/todo.js +96 -0
  33. package/bin/cli.js +4 -10
  34. package/bin/lib/plugin-version.js +28 -0
  35. package/bin/update.js +14 -0
  36. package/config/menus/admin-sidebar.json +1 -1
  37. package/config/server.json +3 -1
  38. package/package.json +3 -7
  39. package/plugins/_lib/admin/mail/reader-view.js +20 -0
  40. package/plugins/_lib/mail/store.js +21 -1
  41. package/plugins/blog/CLAUDE.md +237 -0
  42. package/plugins/blog/admin/css/index.css +178 -1
  43. package/plugins/blog/admin/templates/blog.html +35 -53
  44. package/plugins/blog/admin/templates/categories.html +19 -6
  45. package/plugins/blog/admin/templates/comments.html +26 -9
  46. package/plugins/blog/admin/templates/post-editor.html +36 -13
  47. package/plugins/blog/admin/templates/settings.html +2 -11
  48. package/plugins/blog/admin/views/blog.js +288 -162
  49. package/plugins/blog/admin/views/categories.js +212 -201
  50. package/plugins/blog/admin/views/comments.js +207 -158
  51. package/plugins/blog/admin/views/kit.js +372 -0
  52. package/plugins/blog/admin/views/post-editor.js +269 -21
  53. package/plugins/blog/admin/views/settings-cog.js +56 -0
  54. package/plugins/blog/admin/views/settings.js +16 -86
  55. package/plugins/blog/blocks/blog-card-compact.css +17 -0
  56. package/plugins/blog/blocks/blog-card-compact.html +8 -0
  57. package/plugins/blog/blocks/blog-card-row.css +22 -0
  58. package/plugins/blog/blocks/blog-card-row.html +11 -0
  59. package/plugins/blog/blocks/blog-card.css +35 -0
  60. package/plugins/blog/blocks/blog-card.html +16 -0
  61. package/plugins/blog/blocks/blog-post-classic.css +15 -0
  62. package/plugins/blog/blocks/blog-post-classic.html +28 -0
  63. package/plugins/blog/blocks/blog-post-feature.css +20 -0
  64. package/plugins/blog/blocks/blog-post-feature.html +30 -0
  65. package/plugins/blog/blocks/blog-post-minimal.css +10 -0
  66. package/plugins/blog/blocks/blog-post-minimal.html +23 -0
  67. package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
  68. package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
  69. package/plugins/blog/collections/posts/schema.json +3 -1
  70. package/plugins/blog/config.js +6 -0
  71. package/plugins/blog/lib/layouts.js +304 -0
  72. package/plugins/blog/lib/page.js +158 -0
  73. package/plugins/blog/lib/render.js +119 -0
  74. package/plugins/blog/lib/samples.js +234 -0
  75. package/plugins/blog/plugin.js +225 -15
  76. package/plugins/blog/plugin.json +21 -81
  77. package/plugins/blog/plugin.public.js +115 -210
  78. package/plugins/blog/public/blog.css +177 -0
  79. package/plugins/blog/public/blog.js +586 -0
  80. package/plugins/blog/public/samples/aurora.svg +7 -0
  81. package/plugins/blog/public/samples/dusk.svg +7 -0
  82. package/plugins/blog/public/samples/ember.svg +7 -0
  83. package/plugins/blog/public/samples/harbour.svg +7 -0
  84. package/plugins/blog/public/samples/meadow.svg +7 -0
  85. package/plugins/blog/public/samples/rosewood.svg +7 -0
  86. package/plugins/blog/templates/index.html +9 -8
  87. package/plugins/blog/templates/post.html +4 -17
  88. package/plugins/blog/tests/layouts.test.js +97 -0
  89. package/plugins/blog/tests/public.test.js +71 -0
  90. package/plugins/free-tier.lock.json +74 -0
  91. package/plugins/mail-reader/CLAUDE.md +78 -0
  92. package/plugins/mail-reader/plugin.json +2 -1
  93. package/public/css/theme-switcher.css +1 -0
  94. package/public/js/analytics.js +1 -0
  95. package/public/js/theme-switcher.js +1 -0
  96. package/scripts/build.js +41 -2
  97. package/scripts/run-tests.mjs +94 -0
  98. package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
  99. package/{plugins/contacts/plugin.js → server/routes/api/contacts.js} +71 -43
  100. package/server/routes/api/dashboard.js +8 -2
  101. package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
  102. package/server/routes/api/plugin-marketplace.js +78 -23
  103. package/server/routes/api/plugins.js +148 -3
  104. package/server/routes/api/todo.js +178 -0
  105. package/server/server.js +61 -1
  106. package/server/services/collections.js +13 -3
  107. package/server/services/managerClient.js +72 -136
  108. package/server/services/markdown.js +54 -1
  109. package/server/services/permissionRegistry.js +50 -0
  110. package/server/services/pluginEntitlement.js +171 -0
  111. package/server/services/pluginEnvelope.js +242 -0
  112. package/server/services/pluginInstaller.js +246 -26
  113. package/server/services/pluginScaffold.js +5 -0
  114. package/server/services/plugins.js +117 -7
  115. package/server/services/presetCollections.js +344 -0
  116. package/server/services/promoted-plugins-migration.js +254 -0
  117. package/server/services/publicCors.js +46 -0
  118. package/server/services/renderer.js +83 -3
  119. package/server/services/sidebarBadges.js +37 -0
  120. package/server/services/siteGitignore.js +310 -0
  121. package/server/services/themeSettings.js +39 -0
  122. package/config/theme.json +0 -18
  123. package/plugins/analytics/admin/css/index.css +0 -1
  124. package/plugins/analytics/admin/templates/analytics.html +0 -70
  125. package/plugins/analytics/admin/views/analytics.js +0 -176
  126. package/plugins/analytics/config.js +0 -14
  127. package/plugins/analytics/plugin.json +0 -60
  128. package/plugins/analytics/public/inject-body.html +0 -60
  129. package/plugins/analytics/public/inject-head.html +0 -1
  130. package/plugins/blog/templates/author.html +0 -9
  131. package/plugins/blog/templates/category.html +0 -9
  132. package/plugins/blog/templates/tag.html +0 -9
  133. package/plugins/contacts/admin/templates/contacts.html +0 -254
  134. package/plugins/contacts/admin/views/contacts.js +0 -943
  135. package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
  136. package/plugins/contacts/collections/user-contacts/schema.json +0 -179
  137. package/plugins/contacts/config.js +0 -6
  138. package/plugins/contacts/plugin.json +0 -36
  139. package/plugins/demo-viewer/config.js +0 -4
  140. package/plugins/demo-viewer/plugin.js +0 -36
  141. package/plugins/demo-viewer/plugin.json +0 -9
  142. package/plugins/invoice/admin/css/index.css +0 -1
  143. package/plugins/invoice/admin/templates/editor.html +0 -220
  144. package/plugins/invoice/admin/templates/index.html +0 -173
  145. package/plugins/invoice/admin/templates/issuers.html +0 -2
  146. package/plugins/invoice/admin/templates/receivers.html +0 -2
  147. package/plugins/invoice/admin/views/contacts-source.js +0 -266
  148. package/plugins/invoice/admin/views/editor.js +0 -434
  149. package/plugins/invoice/admin/views/export.js +0 -199
  150. package/plugins/invoice/admin/views/help-content.js +0 -61
  151. package/plugins/invoice/admin/views/index.js +0 -643
  152. package/plugins/invoice/admin/views/issuers.js +0 -30
  153. package/plugins/invoice/admin/views/media.js +0 -172
  154. package/plugins/invoice/admin/views/party-view.js +0 -386
  155. package/plugins/invoice/admin/views/payments.js +0 -127
  156. package/plugins/invoice/admin/views/print.js +0 -130
  157. package/plugins/invoice/admin/views/receivers.js +0 -55
  158. package/plugins/invoice/admin/views/send.js +0 -212
  159. package/plugins/invoice/admin/views/settings.js +0 -594
  160. package/plugins/invoice/admin/views/view-lifecycle.js +0 -33
  161. package/plugins/invoice/collections/invoice-issuers/schema.json +0 -82
  162. package/plugins/invoice/collections/invoice-receivers/schema.json +0 -16
  163. package/plugins/invoice/collections/invoices/schema.json +0 -33
  164. package/plugins/invoice/config.js +0 -37
  165. package/plugins/invoice/pdf.js +0 -164
  166. package/plugins/invoice/plugin.js +0 -1456
  167. package/plugins/invoice/plugin.json +0 -86
  168. package/plugins/invoice/templates/_base.css +0 -1
  169. package/plugins/invoice/templates/classic-nologo.html +0 -100
  170. package/plugins/invoice/templates/classic.html +0 -91
  171. package/plugins/invoice/templates/invoice-print.html +0 -237
  172. package/plugins/invoice/templates/minimal.html +0 -99
  173. package/plugins/invoice/templates/modern-nologo.html +0 -114
  174. package/plugins/invoice/templates/modern.html +0 -113
  175. package/plugins/invoice/templates/templates.json +0 -11
  176. package/plugins/notes/admin/templates/notes.html +0 -83
  177. package/plugins/notes/admin/views/notes.js +0 -281
  178. package/plugins/notes/collections/user-notes/schema.json +0 -54
  179. package/plugins/notes/config.js +0 -6
  180. package/plugins/notes/plugin.json +0 -33
  181. package/plugins/surveys/admin/templates/audience.html +0 -47
  182. package/plugins/surveys/admin/templates/results.html +0 -58
  183. package/plugins/surveys/admin/templates/survey-editor.html +0 -141
  184. package/plugins/surveys/admin/templates/surveys.html +0 -25
  185. package/plugins/surveys/admin/views/audience.js +0 -301
  186. package/plugins/surveys/admin/views/results.js +0 -172
  187. package/plugins/surveys/admin/views/survey-editor.js +0 -211
  188. package/plugins/surveys/admin/views/surveys.js +0 -161
  189. package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
  190. package/plugins/surveys/collections/survey-groups/schema.json +0 -11
  191. package/plugins/surveys/collections/survey-invites/schema.json +0 -16
  192. package/plugins/surveys/collections/surveys/schema.json +0 -23
  193. package/plugins/surveys/config.js +0 -8
  194. package/plugins/surveys/lib/audience.js +0 -37
  195. package/plugins/surveys/lib/campaigns.js +0 -43
  196. package/plugins/surveys/lib/ledger.js +0 -110
  197. package/plugins/surveys/lib/sending.js +0 -106
  198. package/plugins/surveys/lib/stats.js +0 -62
  199. package/plugins/surveys/lib/submit.js +0 -95
  200. package/plugins/surveys/lib/tokens.js +0 -28
  201. package/plugins/surveys/plugin.js +0 -174
  202. package/plugins/surveys/plugin.json +0 -65
  203. package/plugins/surveys/plugin.public.js +0 -149
  204. package/plugins/surveys/public/dist-shared.mjs +0 -1
  205. package/plugins/surveys/public/survey.css +0 -1
  206. package/plugins/surveys/public/survey.mjs +0 -1
  207. package/plugins/surveys/templates/survey-page.html +0 -63
  208. package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
  209. package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
  210. package/plugins/theme-switcher/config.js +0 -10
  211. package/plugins/theme-switcher/plugin.js +0 -26
  212. package/plugins/theme-switcher/plugin.json +0 -74
  213. package/plugins/theme-switcher/public/inject-body.html +0 -153
  214. package/plugins/theme-switcher/public/inject-head.html +0 -260
  215. package/plugins/todo/admin/templates/todo.html +0 -158
  216. package/plugins/todo/admin/views/todo.js +0 -343
  217. package/plugins/todo/collections/todos/schema.json +0 -60
  218. package/plugins/todo/config.js +0 -7
  219. package/plugins/todo/plugin.js +0 -102
  220. package/plugins/todo/plugin.json +0 -33
  221. /package/{plugins/contacts/admin/css/index.css → admin/css/contacts.css} +0 -0
@@ -0,0 +1,174 @@
1
+ import{apiRequest as Ue}from"../api.js";import{addBannerAction as Fe}from"../lib/plugin-chrome.js";import{helpPopovers as J}from"../lib/help-popover.js";import{onceClosed as Z,openMenuAt as ee,settingsStore as He,viewLifetime as ze}from"../lib/tool-kit.js";import{arrangeContacts as Ke,countContacts as We,DEFAULT_SETTINGS as ve,duplicateBody as Ye,initials as Je,normaliseSettings as ye,primaryEmail as he,primaryPhone as ge,withGroup as te}from"../lib/contacts-arrange.js";const ke=He("contacts.settings",ye),h=(x,r="GET",m)=>Ue(x,{method:r,...m!==void 0?{body:JSON.stringify(m)}:{}}),we={company:"Company",jobTitle:"Job title",website:"Website",billingEmail:"Billing email",vatNumber:"VAT number",companyNumber:"Company number",reference:"Their reference",paymentTerms:"Payment terms",currency:"Preferred currency",accountName:"Account name",bankName:"Bank",sortCode:"Sort code",accountNumber:"Account number",iban:"IBAN",bic:"BIC / SWIFT"},T={addresses:{title:"Addresses",add:"Add address",empty:"No address yet.",help:'Tick "Use for invoices" on the one an invoice should be addressed to.',columns:[{key:"label",placeholder:"Label (Office, Home\u2026)"},{key:"line1",placeholder:"Address line 1",wide:!0},{key:"line2",placeholder:"Address line 2",wide:!0},{key:"city",placeholder:"City"},{key:"region",placeholder:"County / Region"},{key:"postcode",placeholder:"Postcode"},{key:"country",placeholder:"Country"},{key:"isBilling",label:"Use for invoices",check:!0}]},emails:{title:"More email addresses",add:"Add email",empty:"None.",help:"Besides the main email above. Search finds a contact by these too.",columns:[{key:"label",placeholder:"Label (Accounts, Personal\u2026)"},{key:"address",placeholder:"name@example.com",wide:!0}]},phones:{title:"More phone numbers",add:"Add phone",empty:"None.",help:"Besides the main phone above. Call in the menu uses the main one, or the first of these.",columns:[{key:"label",placeholder:"Label (Mobile, Office\u2026)"},{key:"number",placeholder:"+44 \u2026",wide:!0}]}},_e=`
2
+ <div class="ct-row{{selClass}}" data-id="{{id}}" tabindex="0">
3
+ <input type="checkbox" class="ct-select" aria-label="Select {{name}}" data-bind-checked="selected">
4
+ <button type="button" class="ct-fav{{favClass}}" title="{{favTitle}}" aria-label="{{favTitle}}">\u2605</button>
5
+ <span class="ct-name">
6
+ <span class="ct-avatar" aria-hidden="true">{{initials}}</span>
7
+ <span class="ct-name-text"><strong>{{name}}</strong><small>{{sub}}</small></span>
8
+ </span>
9
+ <span class="ct-col ct-company" title="{{company}}">{{company}}</span>
10
+ <span class="ct-col ct-email" title="{{email}}">{{email}}</span>
11
+ <span class="ct-col ct-phone">{{phone}}</span>
12
+ <span class="ct-col ct-groups"><span class="ct-groups-text" title="{{groupsText}}">{{groupsText}}</span></span>
13
+ <button type="button" class="ct-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
14
+ </div>`,Ve='<button type="button" class="ct-chip{{cls}}" data-chip="{{key}}" title="{{title}}">{{label}} <b>{{count}}</b></button>',Qe='<button type="button" class="ct-chip{{cls}}" data-group="{{name}}" aria-pressed="{{on}}">{{name}}</button>',Xe=`
15
+ <div class="ct-group-row" data-group="{{name}}">
16
+ <span>{{name}}</span><small>{{countText}}</small>
17
+ <button type="button" class="btn btn-sm btn-ghost" data-act="show">Show</button>
18
+ <button type="button" class="btn btn-sm btn-ghost" data-act="rename">Rename</button>
19
+ <button type="button" class="btn btn-sm btn-ghost text-danger" data-act="delete">Delete</button>
20
+ </div>`;function ae(x){const r=document.createElement("template");return r.innerHTML=x.trim(),r.content.firstElementChild}const A=(x,r,m=`${r}s`)=>`${x} ${x===1?r:m}`,k=ze(/^#\/contacts\b/);export const contactsView={templateUrl:"/admin/js/templates/contacts.html",async onMount(x){k.reset();const r=x.get(0),m=M.observable(ke.read()),p=M.observable([]),b=M.observable([]),K=M.observable(!0),oe=M.observable(""),j=M.observable({kind:"all"}),i=M.observable(new Set),se=M.observable(!1),ne=M.computed(()=>We(p.value,b.value)),Ee=M.computed(()=>Ke(p.value,{query:oe.value,filter:j.value,sort:m.value.sort,favouritesFirst:m.value.favouritesFirst})),S=M.observable([]);k.add(M.effect(()=>{const t=i.value;S.value=Ee.value.map(e=>({id:e.id,name:e.name||"Unnamed",sub:[e.jobTitle,e.company&&!m.value.showCompany?e.company:""].filter(Boolean).join(" \xB7 "),initials:Je(e.name),company:e.company||"",email:he(e),phone:ge(e),groupsText:(e.groups||[]).join(", "),favClass:e.favourite===!0?" is-on":"",favTitle:e.favourite===!0?"Starred - click to unstar":"Star this contact",selected:t.has(e.id),selClass:t.has(e.id)?" is-selected":""}))}));const ce=M.observable([]);k.add(M.effect(()=>{const t=ne.value,e=j.value;ce.value=[{key:"all",label:"All",count:t.all,title:"Everyone",cls:e.kind==="all"?" is-active":""},{key:"fav",label:"\u2605 Starred",count:t.fav,title:"Only starred contacts",cls:e.kind==="fav"?" is-active":""},...b.value.map(a=>({key:`group:${a}`,label:a,count:t.groups[a]??0,title:`Only the ${a} group`,cls:e.kind==="group"&&e.value===a?" is-active":""}))]})),k.add(M.effect(()=>{const t=j.value;t.kind==="group"&&!b.value.includes(t.value)&&(j.value={kind:"all"})}));const Se={rows:S,chips:ce,loading:K,query:oe,searchFocus:se,hasRows:M.computed(()=>S.value.length>0),isEmpty:M.computed(()=>!K.value&&S.value.length===0),emptyText:M.computed(()=>p.value.length?"No contacts match. Clear the search or pick All.":"No contacts yet. New contact adds one, or \u22EE > Import brings a file in."),countText:M.computed(()=>{if(K.value)return"";const t=S.value.length,e=p.value.length;return t===e?A(e,"contact"):`${t} of ${e}`}),hasSelection:M.computed(()=>i.value.size>0),selectionText:M.computed(()=>`${A(i.value.size,"contact")} selected`),allSelected:M.computed(()=>S.value.length>0&&S.value.every(t=>i.value.has(t.id)))};k.add(M.effect(()=>ke.write(m.value)));const $e=(t,e)=>{m.value=ye({...m.peek(),[t]:e})},N=t=>p.peek().find(e=>e.id===t),Me=t=>{p.value=p.peek().map(e=>e.id===t.id?t:e)};async function W(){K.value=!0;try{const[t,e]=await Promise.all([h("/contacts/contacts"),h("/contacts/groups")]);p.value=Array.isArray(t)?t:t?.data??[],b.value=Array.isArray(e)?e:[]}catch{E.toast("Failed to load contacts.",{type:"error"})}finally{K.value=!1}}async function Ce(){try{b.value=await h("/contacts/groups")}catch{}}async function q(t,e,a){try{return Me(await h(`/contacts/contacts/${t}`,"PUT",e)),a&&E.toast(a,{type:"success"}),!0}catch(s){return E.toast(s?.message||"Failed to update the contact.",{type:"error"}),!1}}async function le(t){const e=N(t);if(m.peek().confirmDelete&&!await E.confirm("Delete this contact? This cannot be undone."))return!1;try{await h(`/contacts/contacts/${t}`,"DELETE"),p.value=p.peek().filter(s=>s.id!==t);const a=new Set(i.peek());return a.delete(t)&&(i.value=a),E.toast(`${e?.name?"Contact":"It"} deleted.`,{type:"success"}),!0}catch{return E.toast("Failed to delete the contact.",{type:"error"}),!1}}async function xe(t){const e=N(t);if(e)try{const a=await h("/contacts/contacts","POST",Ye(e));p.value=[...p.peek(),a],E.toast("Copied - the copy is open to edit.",{type:"success"}),F(a.id)}catch{E.toast("Failed to duplicate the contact.",{type:"error"})}}async function U(t){if(b.peek().includes(t))return!0;try{return await h("/contacts/groups","POST",{name:t}),await Ce(),!0}catch(e){return E.toast(e?.message||"Could not create the group.",{type:"error"}),!1}}async function Te(t,e,a){const s=N(t);s&&(a&&!await U(e)||await q(t,{groups:te(s.groups,e,a)},a?`Added to ${e}.`:`Removed from ${e}.`))}async function re(t){const e=String(await E.prompt("New group name:")??"").trim();if(!(!e||!await U(e))){for(const a of t){const s=N(a);s&&!(s.groups||[]).includes(e)&&await q(a,{groups:te(s.groups,e,!0)})}E.toast(`Filed in ${e}.`,{type:"success"})}}async function ie(t,e){try{await navigator.clipboard.writeText(t),E.toast(`${e} copied.`,{type:"success"})}catch{E.toast("The browser would not allow copying.",{type:"error"})}}async function Ae(){const t=[...i.peek()];if(t.length&&await E.confirm(`Delete ${A(t.length,"contact")}? This cannot be undone.`))try{const e=await h("/contacts/contacts/bulk","DELETE",{ids:t});p.value=p.peek().filter(a=>!t.includes(a.id)),i.value=new Set,E.toast(`${A(e?.deleted??t.length,"contact")} deleted.`,{type:"success"})}catch{E.toast("Bulk delete failed.",{type:"error"})}}async function Ne(t){const e=[...i.peek()];if(!(!e.length||!await U(t))){for(const a of e){const s=N(a);s&&!(s.groups||[]).includes(t)&&await q(a,{groups:te(s.groups,t,!0)})}E.toast(`${A(e.length,"contact")} filed in ${t}.`,{type:"success"})}}const de=t=>{const e=new Set(i.peek());e.delete(t)||e.add(t),i.value=e};async function Le(){try{const t=await h("/contacts/export"),e=new Blob([JSON.stringify(t,null,2)],{type:"application/json"}),a=URL.createObjectURL(e),s=document.createElement("a");s.href=a,s.download="contacts.json",document.body.appendChild(s),s.click(),s.remove(),setTimeout(()=>URL.revokeObjectURL(a),1e3),E.toast("Contacts exported.",{type:"success"})}catch{E.toast("Export failed.",{type:"error"})}}const Y=r.querySelector(".ct-import-file");Y.addEventListener("change",async()=>{const t=Y.files?.[0];if(Y.value="",!!t)try{const e=JSON.parse(await t.text()),a=Array.isArray(e)?e:e.contacts;if(!Array.isArray(a))throw new Error("Invalid format");const s=await h("/contacts/import","POST",{contacts:a});E.toast(`Imported ${A(s?.imported??0,"contact")}.`,{type:"success"}),await W()}catch{E.toast("Import failed. The file must be a contacts JSON export.",{type:"error"})}});function F(t){const e=t?N(t):null;if(t&&!e)return;const a=E.slideover({title:e?"Edit contact":"New contact",size:"lg",position:"right"}),s=(o,c,{type:L="text",mono:f=!1,placeholder:y="",wide:w=!1}={})=>`
21
+ <div${w?' class="ct-wide"':""}>
22
+ <label class="form-label" for="ct-f-${o}">${c}</label>
23
+ <input id="ct-f-${o}" type="${L}" class="form-input${f?" font-mono":""}" placeholder="${y}" data-model="${o}.value">
24
+ </div>`,u=o=>`
25
+ <div class="ct-repeater" data-repeater="${o}">
26
+ <div class="ct-subhead">
27
+ <strong>${T[o].title} <span data-help="${T[o].help}"></span></strong>
28
+ <button type="button" class="btn btn-sm btn-ghost" data-add="${o}">
29
+ <span data-icon="plus" data-icon-size="13"></span> ${T[o].add}</button>
30
+ </div>
31
+ <div class="ct-repeater-list"></div>
32
+ <p class="text-muted text-sm contacts-empty-row">${T[o].empty}</p>
33
+ </div>`,l=ae(`
34
+ <div class="ct-panel">
35
+ <div class="ct-grid">
36
+ <div class="ct-wide">
37
+ <label class="form-label" for="ct-f-name">Name <span class="text-danger">*</span></label>
38
+ <input id="ct-f-name" type="text" class="form-input" placeholder="Full name" data-model="name.value">
39
+ </div>
40
+ ${s("email","Email",{type:"email",placeholder:"email@example.com"})}
41
+ ${s("phone","Phone",{placeholder:"+44 \u2026"})}
42
+ <div class="ct-wide">
43
+ <label class="form-label" for="ct-f-notes">Notes</label>
44
+ <textarea id="ct-f-notes" class="form-input" rows="3" placeholder="Optional notes\u2026" data-model="notes.value"></textarea>
45
+ </div>
46
+ </div>
47
+ <div class="ct-setting-row">
48
+ <label class="form-label" for="ct-f-fav" style="margin:0">Starred
49
+ <span data-help="Starred contacts sit at the top of the list (unless switched off in settings) and have their own chip."></span>
50
+ </label>
51
+ <input id="ct-f-fav" type="checkbox" class="form-switch" data-model="favourite.value">
52
+ </div>
53
+ <div>
54
+ <div class="ct-subhead">
55
+ <strong>Groups <span data-help="Click a group to file this contact in it, again to take it out. A contact can be in several."></span></strong>
56
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="newGroup">
57
+ <span data-icon="folder-plus" data-icon-size="13"></span> New group</button>
58
+ </div>
59
+ <div class="ct-pick" data-each="pick.value key=name"></div>
60
+ <p class="text-muted text-sm" data-if="noGroups.value" style="margin:0">No groups yet - New group makes one.</p>
61
+ </div>
62
+
63
+ <details class="ct-section">
64
+ <summary>Company, bank &amp; invoicing <span class="ct-section-sum" data-bind-text="billingSummary.value"></span></summary>
65
+ <div class="ct-section-body">
66
+ <p class="ct-panel-meta" style="margin:0">Everything an invoice needs. The Invoice plugin reads these when you bill this contact.</p>
67
+ <div class="ct-grid">
68
+ ${s("company","Company",{placeholder:"Acme Ltd"})}
69
+ ${s("jobTitle","Job title",{placeholder:"Finance Manager"})}
70
+ ${s("website","Website",{placeholder:"acme.co.uk"})}
71
+ ${s("billingEmail","Billing email",{type:"email",placeholder:"accounts@acme.co.uk"})}
72
+ ${s("vatNumber","VAT number",{mono:!0,placeholder:"GB123456789"})}
73
+ ${s("companyNumber","Company number",{mono:!0,placeholder:"01234567"})}
74
+ ${s("reference","Their reference",{placeholder:"PO-4471"})}
75
+ <div>
76
+ <label class="form-label" for="ct-f-paymentTerms">Payment terms (days)
77
+ <span data-help="Days they have to pay. Empty uses the invoice default."></span></label>
78
+ <input id="ct-f-paymentTerms" type="number" min="0" max="365" class="form-input" placeholder="30" data-model="paymentTerms.value">
79
+ </div>
80
+ <div>
81
+ <label class="form-label" for="ct-f-currency">Preferred currency</label>
82
+ <select id="ct-f-currency" class="form-input" data-model="currency.value">
83
+ <option value="">Use the invoice default</option>
84
+ <option value="GBP">GBP - British Pound</option>
85
+ <option value="EUR">EUR - Euro</option>
86
+ <option value="USD">USD - US Dollar</option>
87
+ </select>
88
+ </div>
89
+ </div>
90
+ <div>
91
+ <div class="ct-subhead"><strong>Bank details <span data-help="Printed on an invoice so it can be paid."></span></strong></div>
92
+ <div class="ct-grid">
93
+ ${s("accountName","Account name",{placeholder:"Acme Ltd"})}
94
+ ${s("bankName","Bank",{placeholder:"Barclays"})}
95
+ ${s("sortCode","Sort code",{mono:!0,placeholder:"20-00-00"})}
96
+ ${s("accountNumber","Account number",{mono:!0,placeholder:"12345678"})}
97
+ ${s("iban","IBAN",{mono:!0,placeholder:"GB29 NWBK 6016 1331 9268 19"})}
98
+ ${s("bic","BIC / SWIFT",{mono:!0,placeholder:"NWBKGB2L"})}
99
+ </div>
100
+ </div>
101
+ ${u("addresses")}
102
+ </div>
103
+ </details>
104
+
105
+ <details class="ct-section">
106
+ <summary>More emails &amp; phones <span class="ct-section-sum" data-bind-text="extraSummary.value"></span></summary>
107
+ <div class="ct-section-body">
108
+ ${u("emails")}
109
+ ${u("phones")}
110
+ </div>
111
+ </details>
112
+
113
+ <small class="ct-panel-meta" data-bind-text="meta"></small>
114
+ <div class="ct-panel-actions">
115
+ <button type="button" class="btn btn-sm btn-ghost text-danger" data-on-click="del" data-if="isEdit">
116
+ <span data-icon="trash" data-icon-size="13"></span> Delete
117
+ </button>
118
+ <span class="spacer"></span>
119
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
120
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannotSave.value">
121
+ <span data-icon="save" data-icon-size="13"></span> Save</button>
122
+ </div>
123
+ </div>`),$=o=>o==null?"":String(o),n={name:M.observable($(e?.name)),email:M.observable($(e?.email)),phone:M.observable($(e?.phone)),notes:M.observable($(e?.notes)),favourite:M.observable(e?.favourite===!0),chosen:M.observable(Array.isArray(e?.groups)?[...e.groups]:[]),isEdit:!!e,meta:e?`Added ${e.createdAt?D(e.createdAt).format("D MMM YYYY"):"earlier"}${e.updatedAt?`, last changed ${D(e.updatedAt).format("D MMM YYYY HH:mm")}`:""}. Ctrl+Enter saves.`:"Ctrl+Enter saves."};for(const o of Object.keys(we))n[o]=M.observable($(e?.[o]));const d=Object.fromEntries(Object.keys(T).map(o=>[o,M.observable(0)]));n.pick=M.observable([]);const v=M.effect(()=>{const o=n.chosen.value;n.pick.value=[...new Set([...b.value,...o])].map(c=>({name:c,on:String(o.includes(c)),cls:o.includes(c)?" is-active":""}))});n.noGroups=M.computed(()=>n.pick.value.length===0),n.cannotSave=M.computed(()=>!n.name.value.trim()),n.billingSummary=M.computed(()=>{const o=[];n.company.value.trim()&&o.push(n.company.value.trim()),n.vatNumber.value.trim()&&o.push("VAT"),(n.sortCode.value.trim()||n.iban.value.trim())&&o.push("bank details");const c=d.addresses.value;return c&&o.push(A(c,"address","addresses")),o.join(" \xB7 ")}),n.extraSummary=M.computed(()=>{const o=d.emails.value+d.phones.value;return o?A(o,"more"):""});function C(o,c){const L=T[o],f=l.querySelector(`[data-repeater="${o}"]`),y=f.querySelector(".ct-repeater-list");y.replaceChildren(),f.querySelector(".contacts-empty-row").style.display=c.length?"none":"",c.forEach((w,B)=>{const P=document.createElement("div");P.className="contacts-row";for(const g of L.columns){if(g.check){const Q=document.createElement("label");Q.className="form-check contacts-row-check";const G=document.createElement("input");G.type="checkbox",G.className="form-check-input",G.dataset.key=g.key,G.checked=w[g.key]===!0,G.addEventListener("change",()=>{G.checked&&y.querySelectorAll(`input[data-key="${g.key}"]`).forEach(be=>{be!==G&&(be.checked=!1)})});const X=document.createElement("span");X.className="form-check-label",X.textContent=g.label,Q.append(G,X),P.appendChild(Q);continue}const z=document.createElement("input");z.type="text",z.className="form-input"+(g.wide?" contacts-row-wide":""),z.dataset.key=g.key,z.placeholder=g.placeholder??"",z.value=w[g.key]??"",P.appendChild(z)}const H=document.createElement("button");H.type="button",H.className="btn btn-sm btn-ghost contacts-row-remove",H.title="Remove",H.textContent="\xD7",H.addEventListener("click",()=>{const g=R(o,{keepBlank:!0});g.splice(B,1),C(o,g)}),P.appendChild(H),y.appendChild(P)}),d[o].value=c.length}function R(o,{keepBlank:c=!1}={}){const L=T[o],f=[...l.querySelectorAll(`[data-repeater="${o}"] .contacts-row`)].map(y=>{const w={};for(const B of L.columns){const P=y.querySelector(`[data-key="${B.key}"]`);w[B.key]=B.check?P?.checked===!0:(P?.value??"").trim()}return w});return c?f:f.filter(y=>Object.entries(y).some(([w,B])=>w!=="label"&&B!==""&&B!==!1))}l.addEventListener("click",o=>{const c=o.target.closest("[data-add]");if(c){const f=c.dataset.add;C(f,[...R(f,{keepBlank:!0}),{}]),l.querySelector(`[data-repeater="${f}"] .contacts-row:last-child input`)?.focus();return}const L=o.target.closest(".ct-pick [data-group]");if(L){const f=L.dataset.group,y=n.chosen.peek();n.chosen.value=y.includes(f)?y.filter(w=>w!==f):[...y,f]}});const _=()=>{const o={name:n.name.peek().trim(),email:n.email.peek().trim(),phone:n.phone.peek().trim(),notes:n.notes.peek().trim(),favourite:n.favourite.peek()===!0,groups:[...n.chosen.peek()]};for(const c of Object.keys(we))o[c]=String(n[c].peek()??"").trim();for(const c of Object.keys(T))o[c]=R(c);return o};let V=!1;const fe=async()=>{if(!(V||n.cannotSave.peek())){V=!0;try{const o=_();for(const c of o.groups)b.peek().includes(c)||await U(c);if(e){if(!await q(e.id,o,"Contact saved."))return}else{const c=await h("/contacts/contacts","POST",o);p.value=[...p.peek(),c],E.toast("Contact added.",{type:"success"})}a.close()}catch(o){E.toast(o?.message||"Failed to save the contact.",{type:"error"})}finally{V=!1}}};a.element.appendChild(l);const je=J(l);for(const o of Object.keys(T))C(o,Array.isArray(e?.[o])?e[o]:[]);l.querySelector(".ct-pick").innerHTML=Qe;const qe=M.applyBindings(n,l,{methods:{save:fe,cancel:()=>a.close(),del:async()=>{e&&await le(e.id)&&a.close()},async newGroup(){const o=String(await E.prompt("New group name:")??"").trim();!o||!await U(o)||n.chosen.peek().includes(o)||(n.chosen.value=[...n.chosen.peek(),o])}}});n.billingSummary.peek()&&(l.querySelector(".ct-section").open=!0),l.addEventListener("keydown",o=>{(o.ctrlKey||o.metaKey)&&o.key==="Enter"&&(o.preventDefault(),fe())}),I.scan(l),Z(a,()=>{v?.(),qe.dispose(),je()}),a.open(),setTimeout(()=>l.querySelector("#ct-f-name")?.focus(),60)}function Be(){const t=E.slideover({title:"Contacts settings",size:"sm",position:"right"}),e=(n,d,v)=>`
124
+ <div class="ct-setting-row">
125
+ <label class="form-label" for="ct-set-${n}" style="margin:0">${d}${v?` <span data-help="${v}"></span>`:""}</label>
126
+ <input id="ct-set-${n}" type="checkbox" class="form-switch" data-model="${n}.value">
127
+ </div>`,a=ae(`
128
+ <div class="ct-panel">
129
+ <div>
130
+ <label class="form-label" for="ct-set-sort">Sort by</label>
131
+ <select id="ct-set-sort" class="form-input" data-model="sort.value">
132
+ <option value="name">Name</option>
133
+ <option value="company">Company</option>
134
+ <option value="added">Newest added first</option>
135
+ <option value="updated">Recently changed first</option>
136
+ </select>
137
+ </div>
138
+ ${e("favouritesFirst","Starred first","Starred contacts sit at the top, whatever the sort.")}
139
+ <div>
140
+ <label class="form-label" for="ct-set-density">Rows
141
+ <span data-help="Compact fits more on the screen: smaller avatars and no job title line."></span></label>
142
+ <select id="ct-set-density" class="form-input" data-model="density.value">
143
+ <option value="comfortable">Comfortable</option>
144
+ <option value="compact">Compact</option>
145
+ </select>
146
+ </div>
147
+ <div>
148
+ <div class="form-label">Columns
149
+ <span data-help="Name is always shown. On a narrow screen only the name shows; the rest is a click away."></span></div>
150
+ ${e("showCompany","Company")}
151
+ ${e("showEmail","Email")}
152
+ ${e("showPhone","Phone")}
153
+ ${e("showGroups","Groups")}
154
+ </div>
155
+ ${e("confirmDelete","Confirm before deleting","Off deletes a single contact straight away - there is no undo. Deleting several always asks.")}
156
+ <small class="ct-panel-meta">Saved in this browser as you change them.</small>
157
+ <div class="ct-panel-actions">
158
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="reset">Reset to defaults</button>
159
+ <span class="spacer"></span>
160
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="done">Done</button>
161
+ </div>
162
+ </div>`),s=n=>M.computed({read:()=>m.value[n],write:d=>$e(n,d)}),u=Object.fromEntries(Object.keys(ve).map(n=>[n,s(n)]));t.element.appendChild(a);const l=J(a),$=M.applyBindings(u,a,{methods:{done:()=>t.close(),reset:()=>{m.value={...ve}}}});Z(t,()=>{$.dispose(),l()}),t.open()}function Pe(){const t=E.slideover({title:"Groups",size:"sm",position:"right"}),e=ae(`
163
+ <div class="ct-panel">
164
+ <form class="ct-add-group" data-on-submit="add">
165
+ <input type="text" class="form-input" placeholder="New group name\u2026" aria-label="New group name" data-model="draft.value">
166
+ <button type="submit" class="btn btn-sm btn-primary" data-bind-disabled="cannotAdd.value">Add</button>
167
+ </form>
168
+ <p class="ct-panel-meta" style="margin:0">Renaming a group renames it on every contact in it; deleting one takes it off them.
169
+ <span data-help="Contacts themselves are never deleted from here."></span></p>
170
+ <div class="ct-groups-list" data-if="hasGroups.value">
171
+ <div class="ct-groups-rows" data-each="list.value key=name"></div>
172
+ </div>
173
+ <p class="text-muted" data-if="noGroups.value">No groups yet.</p>
174
+ </div>`),a=M.observable(""),s={draft:a,list:M.observable([])},u=M.effect(()=>{const d=ne.value.groups;s.list.value=b.value.map(v=>({name:v,countText:A(d[v]??0,"contact")}))});s.hasGroups=M.computed(()=>s.list.value.length>0),s.noGroups=M.computed(()=>s.list.value.length===0),s.cannotAdd=M.computed(()=>!a.value.trim());async function l(){const d=a.peek().trim();d&&await U(d)&&(a.value="",E.toast("Group added.",{type:"success"}))}e.addEventListener("click",async d=>{const v=d.target.closest("[data-act]"),C=v?.closest("[data-group]")?.dataset.group;if(!(!v||!C)){if(v.dataset.act==="show")j.value={kind:"group",value:C},t.close();else if(v.dataset.act==="rename"){const R=String(await E.prompt("Rename the group to:",{inputValue:C})??"").trim();if(!R||R===C)return;try{await h(`/contacts/groups/${encodeURIComponent(C)}`,"PUT",{newName:R}),await W(),E.toast("Group renamed.",{type:"success"})}catch(_){E.toast(_?.message||"Failed to rename the group.",{type:"error"})}}else if(v.dataset.act==="delete"){if(!await E.confirm("Delete this group? Its contacts stay - they are just no longer in it."))return;try{await h(`/contacts/groups/${encodeURIComponent(C)}`,"DELETE"),await W(),E.toast("Group deleted.",{type:"success"})}catch{E.toast("Failed to delete the group.",{type:"error"})}}}}),t.element.appendChild(e);const $=J(e);e.querySelector(".ct-groups-rows").innerHTML=Xe;const n=M.applyBindings(s,e,{methods:{add:()=>(l(),!1)}});Z(t,()=>{u?.(),n.dispose(),$()}),t.open()}Fe(x,{id:"contacts-settings",icon:"settings",title:"Contacts settings",onClick:Be}),r.querySelector(".ct-rows").innerHTML=_e,r.querySelector(".ct-chips").innerHTML=Ve;const O=r.querySelector(".ct-list"),pe=r.querySelector(".ct-bar"),Ge=r.querySelector(".ct-chips-row"),ue=r.querySelector(".ct-bulk");k.add(J(r,{position:"bottom"}));const Oe=M.applyBindings(Se,r,{methods:{create:()=>F(null),bulkDelete:Ae,clearSelection:()=>{i.value=new Set}}});k.add(()=>Oe.dispose()),k.add(M.effect(()=>{const t=m.value;O.classList.toggle("is-compact",t.density==="compact");const e={company:!t.showCompany,email:!t.showEmail,phone:!t.showPhone,groups:!t.showGroups};for(const[s,u]of Object.entries(e))O.classList.toggle(`hide-${s}`,u);const a=["1.1rem","1.5rem","minmax(10rem, 1.4fr)"];e.company||a.push("minmax(0, 1fr)"),e.email||a.push("minmax(0, 1.2fr)"),e.phone||a.push("minmax(0, .9fr)"),e.groups||a.push("minmax(0, 1fr)"),a.push("1.75rem"),O.style.setProperty("--ct-cols",a.join(" "))}));const De=E.contextMenu(O,{match:".ct-row",inherit:!1,density:"compact",items:t=>{const e=t.dataset.id,a=N(e);if(!a)return[];const s=he(a),u=ge(a),l=a.groups||[];return(n=>n.filter(d=>d.visible!==!1))([{label:"Edit\u2026",icon:"edit",action:()=>F(e)},{type:"divider"},{label:`Email ${s}`,icon:"mail",visible:!!s,action:()=>{location.href=`mailto:${encodeURIComponent(s).replace(/%40/g,"@")}`}},{label:`Call ${u}`,icon:"phone",visible:!!u,action:()=>{location.href=`tel:${u.replace(/[^\d+]/g,"")}`}},{label:"Copy email",icon:"copy",visible:!!s,action:()=>ie(s,"Email")},{label:"Copy phone",icon:"copy",visible:!!u,action:()=>ie(u,"Phone")},{type:"divider",visible:!!(s||u)},{label:a.favourite===!0?"Unstar":"Star",icon:"star",action:()=>q(e,{favourite:a.favourite!==!0})},{label:"Groups",icon:"folder",submenu:[...b.peek().map(n=>({label:`${l.includes(n)?"\u2713 ":""}${n}`,action:()=>Te(e,n,!l.includes(n))})),...b.peek().length?[{type:"divider"}]:[],{label:"New group\u2026",action:()=>re([e])}]},{label:"Duplicate",icon:"copy",action:()=>xe(e)},{label:i.peek().has(e)?"Deselect":"Select",icon:"check-square",action:()=>de(e)},{type:"divider"},{label:"Delete",icon:"trash",danger:!0,action:()=>le(e)}])}});k.add(()=>De?.destroy?.());const Re=E.contextMenu(pe,{match:".ct-more",inherit:!1,density:"compact",items:()=>[{label:"Manage groups\u2026",icon:"folder",action:Pe},{type:"divider"},{label:"Export all (JSON)",icon:"download",action:Le},{label:"Import from file\u2026",icon:"upload",action:()=>Y.click()},{type:"divider"},{label:"Select all shown",icon:"check-square",disabled:!S.peek().length,action:()=>{i.value=new Set(S.peek().map(t=>t.id))}},{label:"Clear selection",icon:"x",disabled:!i.peek().size,action:()=>{i.value=new Set}}]});k.add(()=>Re?.destroy?.());const Ie=E.contextMenu(ue,{match:".ct-bulk-group",inherit:!1,density:"compact",items:()=>[...b.peek().map(t=>({label:t,action:()=>Ne(t)})),...b.peek().length?[{type:"divider"}]:[],{label:"New group\u2026",action:()=>re([...i.peek()])}]});k.add(()=>Ie?.destroy?.()),pe.addEventListener("click",t=>{const e=t.target.closest(".ct-more");e&&ee(e)}),ue.addEventListener("click",t=>{const e=t.target.closest(".ct-bulk-group");e&&ee(e)}),Ge.addEventListener("click",t=>{const e=t.target.closest(".ct-chip");if(!e)return;const a=e.dataset.chip;j.value=a==="fav"?{kind:"fav"}:a.startsWith("group:")?{kind:"group",value:a.slice(6)}:{kind:"all"}}),O.addEventListener("click",t=>{if(t.target.closest(".ct-select-all"))return;const e=t.target.closest(".ct-row");if(!e)return;const a=e.dataset.id;if(t.target.closest(".ct-kebab"))return ee(t.target.closest(".ct-kebab"));if(!t.target.closest(".ct-select")){if(t.target.closest(".ct-fav")){const s=N(a);s&&q(a,{favourite:s.favourite!==!0});return}F(a)}}),O.addEventListener("change",t=>{if(t.target.matches(".ct-select")&&de(t.target.closest(".ct-row").dataset.id),t.target.matches(".ct-select-all")){const e=S.peek().map(a=>a.id);i.value=t.target.checked?new Set(e):new Set}}),O.addEventListener("keydown",t=>{const e=t.target.closest?.(".ct-row");e&&t.key==="Enter"&&t.target===e&&F(e.dataset.id)});const me=t=>{t.ctrlKey||t.metaKey||t.altKey||t.target.closest?.("input, textarea, select, [contenteditable], .dm-slideover")||(t.key==="/"?(t.preventDefault(),se.value=!0):(t.key==="n"||t.key==="N")&&(t.preventDefault(),F(null)))};document.addEventListener("keydown",me),k.add(()=>document.removeEventListener("keydown",me)),I.scan(r),await W()}};
@@ -1 +1 @@
1
- import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{themeView as e}from"./theme.js";import{navigationView as m}from"./navigation.js";import{notificationsView as p}from"./notifications.js";import{layoutsView as n}from"./layouts.js";import{mediaView as s}from"./media.js";import{loginView as f}from"./login.js";import{usersView as w}from"./users.js";import{userEditorView as c}from"./user-editor.js";import{pluginsView as V}from"./plugins.js";import{pluginCodeView as a}from"./plugin-code.js";import{documentationView as d}from"./documentation.js";import{tutorialsView as l}from"./tutorials.js";import{docViews as E}from"./doc-pages.js";import{apiReferenceView as u}from"./api-reference.js";import{collectionsView as g}from"./collections.js";import{collectionEditorView as v}from"./collection-editor.js";import{collectionEntriesView as b}from"./collection-entries.js";import{formsView as h}from"./forms.js";import{formEditorView as j}from"./form-editor.js";import{formSubmissionsView as k}from"./form-submissions.js";import{viewsListView as L}from"./views-list.js";import{viewEditorView as x}from"./view-editor.js";import{viewPreviewView as y}from"./view-preview.js";import{actionsListView as M}from"./actions-list.js";import{actionEditorView as D}from"./action-editor.js";import{proDocsView as P}from"./pro-docs.js";import{blocksView as R}from"./blocks.js";import{blockEditorView as S}from"./block-editor.js";import"./block-editor-enhance.js";import{componentsView as C}from"./components.js";import{componentEditorView as T}from"./component-editor.js";import{myProfileView as U}from"./my-profile.js";import{rolesView as q}from"./roles.js";import{roleEditorView as z}from"./role-editor.js";import{effectsView as A}from"./effects.js";import{searchView as B}from"./search.js";import{menusView as F}from"./menus.js";import{contextMenusView as G}from"./context-menus.js";import{contextMenuEditorView as H}from"./context-menu-editor.js";import{menuEditorView as I}from"./menu-editor.js";import{menuLocationsView as J}from"./menu-locations.js";import{projectsView as K}from"./projects.js";import{projectEditorView as N}from"./project-editor.js";import{projectDetailView as O}from"./project-detail.js";import{projectSettingsView as Q}from"./project-settings.js";import{apiTokensView as W}from"./api-tokens.js";import{apiEndpointsView as X}from"./api-endpoints.js";import{apiEndpointEditorView as Y}from"./api-endpoint-editor.js";const Z={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:K,projectEditor:N,projectDetail:O,projectSettings:Q,apiTokens:W,apiEndpoints:X,apiEndpointEditor:Y,dashboard:o,pages:i,pageEditor:r,settings:t,theme:e,navigation:m,layouts:n,media:s,login:f,users:w,userEditor:c,plugins:V,pluginCode:a,documentation:d,tutorials:l,apiReference:u,collections:g,collectionEditor:v,collectionEntries:b,forms:h,formEditor:j,formSubmissions:k,viewsList:L,viewEditor:x,viewPreview:y,actionsList:M,actionEditor:D,proDocs:P,blocks:R,blockEditor:S,components:C,componentEditor:T,myProfile:U,roles:q,roleEditor:z,effects:A,search:B,notifications:p,menus:F,contextMenus:G,contextMenuEditor:H,menuEditor:I,menuLocations:J,menusRedirect:Z,...E};
1
+ import{dashboardView as o}from"./dashboard.js";import{pagesView as i}from"./pages.js";import{pageEditorView as r}from"./page-editor.js";import{settingsView as t}from"./settings.js";import{themeView as e}from"./theme.js";import{navigationView as m}from"./navigation.js";import{notificationsView as p}from"./notifications.js";import{layoutsView as n}from"./layouts.js";import{mediaView as s}from"./media.js";import{loginView as f}from"./login.js";import{usersView as w}from"./users.js";import{userEditorView as c}from"./user-editor.js";import{pluginsView as V}from"./plugins.js";import{pluginCodeView as a}from"./plugin-code.js";import{documentationView as d}from"./documentation.js";import{tutorialsView as l}from"./tutorials.js";import{docViews as E}from"./doc-pages.js";import{apiReferenceView as u}from"./api-reference.js";import{collectionsView as g}from"./collections.js";import{collectionEditorView as v}from"./collection-editor.js";import{collectionEntriesView as b}from"./collection-entries.js";import{formsView as h}from"./forms.js";import{formEditorView as j}from"./form-editor.js";import{formSubmissionsView as y}from"./form-submissions.js";import{viewsListView as k}from"./views-list.js";import{viewEditorView as L}from"./view-editor.js";import{viewPreviewView as x}from"./view-preview.js";import{actionsListView as M}from"./actions-list.js";import{actionEditorView as D}from"./action-editor.js";import{proDocsView as P}from"./pro-docs.js";import{blocksView as R}from"./blocks.js";import{blockEditorView as S}from"./block-editor.js";import"./block-editor-enhance.js";import{componentsView as C}from"./components.js";import{componentEditorView as T}from"./component-editor.js";import{myProfileView as U}from"./my-profile.js";import{rolesView as q}from"./roles.js";import{roleEditorView as z}from"./role-editor.js";import{effectsView as A}from"./effects.js";import{searchView as B}from"./search.js";import{notesView as F}from"./notes.js";import{todoView as G}from"./todo.js";import{analyticsView as H}from"./analytics.js";import{contactsView as I}from"./contacts.js";import{menusView as J}from"./menus.js";import{contextMenusView as K}from"./context-menus.js";import{contextMenuEditorView as N}from"./context-menu-editor.js";import{menuEditorView as O}from"./menu-editor.js";import{menuLocationsView as Q}from"./menu-locations.js";import{projectsView as W}from"./projects.js";import{projectEditorView as X}from"./project-editor.js";import{projectDetailView as Y}from"./project-detail.js";import{projectSettingsView as Z}from"./project-settings.js";import{apiTokensView as _}from"./api-tokens.js";import{apiEndpointsView as $}from"./api-endpoints.js";import{apiEndpointEditorView as oo}from"./api-endpoint-editor.js";const io={templateUrl:"",async onMount(){location.hash="#/menus"}};export const views={projects:W,projectEditor:X,projectDetail:Y,projectSettings:Z,apiTokens:_,apiEndpoints:$,apiEndpointEditor:oo,dashboard:o,pages:i,pageEditor:r,settings:t,theme:e,navigation:m,layouts:n,media:s,login:f,users:w,userEditor:c,plugins:V,pluginCode:a,documentation:d,tutorials:l,apiReference:u,collections:g,collectionEditor:v,collectionEntries:b,forms:h,formEditor:j,formSubmissions:y,viewsList:k,viewEditor:L,viewPreview:x,actionsList:M,actionEditor:D,proDocs:P,blocks:R,blockEditor:S,components:C,componentEditor:T,myProfile:U,roles:q,roleEditor:z,effects:A,search:B,notes:F,todo:G,analytics:H,contacts:I,notifications:p,menus:J,contextMenus:K,contextMenuEditor:N,menuEditor:O,menuLocations:Q,menusRedirect:io,...E};
@@ -0,0 +1,104 @@
1
+ import{apiRequest as te}from"../api.js";import{addBannerAction as ae}from"../lib/plugin-chrome.js";import{helpPopovers as S}from"../lib/help-popover.js";import{onceClosed as B,openMenuAt as se,settingsStore as oe,sidebarCount as ne,viewLifetime as le}from"../lib/tool-kit.js";import{arrangeNotes as ie,collectCategories as ce,DEFAULT_SETTINGS as re,normaliseSettings as K,parseCategories as de,preview as ue,PREVIEW_MAX as pe,PREVIEW_MIN as ve}from"../lib/notes-arrange.js";const V=oe("notes.settings",K),L=(v,i="GET",l)=>te(v,{method:i,...l!==void 0?{body:JSON.stringify(l)}:{}});function O(v){const i=document.createElement("template");return i.innerHTML=v.trim(),i.content.firstElementChild}const me=`
2
+ <article class="note-card" data-id="{{id}}" tabindex="0">
3
+ <h3 class="note-title">{{title}}</h3>
4
+ <p class="note-preview">{{preview}}</p>
5
+ <footer class="note-foot">
6
+ <span class="note-cats">{{#each cats key=name}}<span class="note-cat" data-cat="{{name}}" title="Show only {{name}}">{{name}}</span>{{/each}}</span>
7
+ <time class="note-time" title="{{updatedFull}}">{{updatedRel}}</time>
8
+ </footer>
9
+ <button type="button" class="note-kebab" aria-label="Note actions" title="Actions">\u22EE</button>
10
+ </article>`,d=le(/^#\/notes\b/);export const notesView={templateUrl:"/admin/js/templates/notes.html",async onMount(v){d.reset();const i=v.get(0),l=M.observableArray([]),T=M.observable(""),u=M.observable(""),m=M.observable(!0),A=M.observable(!1),o=M.observable(V.read()),k=M.computed(()=>ce(l.value));d.add(M.effect(()=>{l.value,m.peek()||ne("#/notes")}));const Y=M.computed(()=>ie(l.value,{query:T.value,category:u.value,sort:o.value.sort})),C=M.observable([]);d.add(M.effect(()=>{const e=o.value.previewLength;C.value=Y.value.map(t=>({id:t.id,title:t.title||"Untitled",preview:ue(t.content,e),cats:(t.categories||[]).map(a=>({name:a})),updatedRel:t.updatedAt?D(t.updatedAt).fromNow():"",updatedFull:t.updatedAt?"Updated "+D(t.updatedAt).format("DD MMM YYYY HH:mm"):""}))}));const $=M.computed(()=>!!(T.value.trim()||u.value)),j={query:T,category:u,loading:m,searchFocus:A,categories:k,visible:C,isCards:M.computed(()=>o.value.layout==="cards"),listClass:M.computed(()=>(o.value.layout==="cards"?"is-cards":"is-list")+(o.value.showCategories?"":" hide-cats")),isEmpty:M.computed(()=>!m.value&&C.value.length===0),emptyText:M.computed(()=>$.value?"No notes match. Clear the search or pick another category.":"No notes yet. Click New to write your first."),countText:M.computed(()=>{if(m.value)return"";const e=l.value.length,t=C.value.length;return $.value?`${t} of ${e}`:`${e} note${e===1?"":"s"}`})};d.add(M.effect(()=>{const e=u.value.toLowerCase();e&&!k.value.some(t=>t.toLowerCase()===e)&&(u.value="")})),d.add(M.effect(()=>V.write(o.value)));const x=(e,t)=>{o.value=K({...o.peek(),[e]:t})};i.querySelector(".notes-items").innerHTML=me;const z=M.applyBindings(j,i,{methods:{create:()=>h(null),useCards:()=>x("layout","cards"),useList:()=>x("layout","list")}});d.add(()=>z.dispose()),d.add(S(i,{position:"bottom"}));const b=e=>l.peek().find(t=>t.id===e);function W(e){const t=l.peek(),a=t.findIndex(s=>s.id===e.id);l.value=a===-1?[e,...t]:t.map(s=>s.id===e.id?e:s)}async function _(){m.value=!0;try{const e=await L("/notes/items");l.value=Array.isArray(e)?e:e.data??[]}catch{E.toast("Failed to load notes",{type:"error"})}finally{m.value=!1}}async function R(e,t){const a=e?await L("/notes/items/"+e,"PUT",t):await L("/notes/items","POST",t);return W(a),a}async function q(e){if(o.peek().confirmDelete&&!await E.confirm("Delete this note? This cannot be undone."))return!1;try{return await L("/notes/items/"+e,"DELETE"),l.value=l.peek().filter(t=>t.id!==e),E.toast("Note deleted",{type:"success"}),!0}catch{return E.toast("Failed to delete note",{type:"error"}),!1}}async function G(e){const t=b(e);if(t)try{await R(null,{title:`${t.title||"Untitled"} (copy)`,content:t.content||"",categories:t.categories||[]}),E.toast("Note duplicated",{type:"success"})}catch{E.toast("Failed to duplicate note",{type:"error"})}}async function J(e){const t=b(e);if(t)try{await navigator.clipboard.writeText([t.title,t.content].filter(Boolean).join(`
11
+
12
+ `)),E.toast("Copied to clipboard",{type:"success"})}catch{E.toast("The browser would not allow copying",{type:"error"})}}let y=null;function h(e){y?.close();const t=!e,a=E.slideover({title:t?"New note":"Edit note",size:"md",position:"right"});y=a;const s=O(`
13
+ <div class="notes-panel">
14
+ <div>
15
+ <label class="form-label" for="note-title-input">Title</label>
16
+ <input id="note-title-input" type="text" class="form-input" placeholder="Untitled"
17
+ data-model="title.value">
18
+ </div>
19
+ <div>
20
+ <label class="form-label">Categories
21
+ <span data-help-title="Categories"
22
+ data-help="Type a category and press Enter to add it; pick an existing one from the list. Categories become filters in the toolbar and chips on the note."></span>
23
+ </label>
24
+ <input type="text" class="notes-cats-input">
25
+ </div>
26
+ <div class="notes-field-grow">
27
+ <label class="form-label" for="note-content-input">Content
28
+ <span data-help-title="Writing"
29
+ data-help="Plain text. Line breaks are kept. Ctrl+Enter (Cmd+Enter on a Mac) saves; Escape closes without saving."></span>
30
+ </label>
31
+ <textarea id="note-content-input" class="form-input" placeholder="Write your note\u2026"
32
+ data-model="content.value"></textarea>
33
+ <div class="notes-panel-meta">
34
+ <span data-bind-text="stats.value"></span>
35
+ <span data-if="dirty.value">Unsaved changes</span>
36
+ </div>
37
+ </div>
38
+ <div class="notes-panel-actions">
39
+ <button type="button" class="btn btn-sm btn-ghost text-danger" data-if="canDelete" data-on-click="del">
40
+ <span data-icon="trash" data-icon-size="13"></span> Delete
41
+ </button>
42
+ <span class="spacer"></span>
43
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
44
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="submit"
45
+ data-bind-disabled="saving.value">
46
+ <span data-icon="save" data-icon-size="13"></span> Save
47
+ </button>
48
+ </div>
49
+ </div>`),n={title:e?.title||"",content:e?.content||"",cats:t?o.peek().defaultCategories:e.categories||[]},g=M.observable(n.title),p=M.observable(n.content),c=M.observable([...n.cats]),f=M.observable(!1),w={title:g,content:p,saving:f,canDelete:!t,stats:M.computed(()=>{const r=p.value.trim(),H=r?r.split(/\s+/).length:0;return`${H} word${H===1?"":"s"} \xB7 ${p.value.length} characters`}),dirty:M.computed(()=>g.value!==n.title||p.value!==n.content||c.value.join("\0")!==n.cats.join("\0"))};async function P(){if(!f.peek()){f.value=!0;try{await R(e?.id,{title:g.peek().trim(),content:p.peek(),categories:c.peek()}),E.toast(t?"Note created":"Note saved",{type:"success"}),a.close()}catch{E.toast("Failed to save note",{type:"error"})}finally{f.value=!1}}}a.element.appendChild(s);const Q=M.applyBindings(w,s,{methods:{submit:P,cancel:()=>a.close(),del:async()=>{await q(e.id)&&a.close()}}}),U=E.pillbox(s.querySelector(".notes-cats-input"),{data:k.peek().map(r=>({value:r,label:r})),value:c.peek(),creatable:!0,placeholder:"Add a category\u2026",onChange:()=>{c.value=de(U.getValue())}}),ee=S(s);s.addEventListener("keydown",r=>{(r.ctrlKey||r.metaKey)&&r.key==="Enter"&&(r.preventDefault(),P())}),B(a,()=>{Q.dispose(),ee(),U.destroy?.(),y===a&&(y=null)}),a.open(),setTimeout(()=>s.querySelector(t?"#note-title-input":"#note-content-input")?.focus(),60)}function X(){const e=E.slideover({title:"Notes settings",size:"sm",position:"right"}),t=O(`
50
+ <div class="notes-panel">
51
+ <div class="notes-setting">
52
+ <label class="form-label" for="notes-set-layout">Layout
53
+ <span data-help="Cards show more of each note; List fits many more on screen. The two buttons in the toolbar switch this too."></span>
54
+ </label>
55
+ <select id="notes-set-layout" class="form-input" data-model="layout.value">
56
+ <option value="cards">Cards</option>
57
+ <option value="list">List</option>
58
+ </select>
59
+ </div>
60
+ <div class="notes-setting">
61
+ <label class="form-label" for="notes-set-sort">Sort by</label>
62
+ <select id="notes-set-sort" class="form-input" data-model="sort.value">
63
+ <option value="updated">Recently updated</option>
64
+ <option value="created">Recently created</option>
65
+ <option value="title">Title, A to Z</option>
66
+ </select>
67
+ </div>
68
+ <div class="notes-setting">
69
+ <label class="form-label" for="notes-set-preview">Preview length
70
+ <span data-help="How much of each note's content shows on its card. 0 shows titles only."></span>
71
+ </label>
72
+ <div class="notes-range">
73
+ <input id="notes-set-preview" type="range" min="${ve}" max="${pe}" step="20"
74
+ data-model="previewLength.value">
75
+ <output data-bind-text="previewText.value"></output>
76
+ </div>
77
+ </div>
78
+ <div class="notes-setting">
79
+ <div class="notes-setting-row">
80
+ <label class="form-label" for="notes-set-cats" style="margin:0">Show category chips</label>
81
+ <input id="notes-set-cats" type="checkbox" class="form-switch" data-model="showCategories.value">
82
+ </div>
83
+ </div>
84
+ <div class="notes-setting">
85
+ <div class="notes-setting-row">
86
+ <label class="form-label" for="notes-set-confirm" style="margin:0">Confirm before deleting
87
+ <span data-help="Off deletes straight away - there is no undo."></span>
88
+ </label>
89
+ <input id="notes-set-confirm" type="checkbox" class="form-switch" data-model="confirmDelete.value">
90
+ </div>
91
+ </div>
92
+ <div class="notes-setting">
93
+ <label class="form-label">Categories for new notes
94
+ <span data-help="Every new note starts with these. You can still remove them before saving."></span>
95
+ </label>
96
+ <input type="text" class="notes-set-defaults">
97
+ </div>
98
+ <small class="notes-panel-meta">Saved in this browser as you change them.</small>
99
+ <div class="notes-panel-actions">
100
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="reset">Reset to defaults</button>
101
+ <span class="spacer"></span>
102
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="done">Done</button>
103
+ </div>
104
+ </div>`),a=(c,f=w=>w)=>M.computed({read:()=>o.value[c],write:w=>x(c,f(w))}),s={layout:a("layout"),sort:a("sort"),previewLength:a("previewLength",Number),showCategories:a("showCategories"),confirmDelete:a("confirmDelete"),previewText:M.computed(()=>o.value.previewLength?`${o.value.previewLength} ch`:"Off")};e.element.appendChild(t);let n;const g=M.applyBindings(s,t,{methods:{done:()=>e.close(),reset:()=>{o.value={...re},n?.setValue([])}}});n=E.pillbox(t.querySelector(".notes-set-defaults"),{data:k.peek().map(c=>({value:c,label:c})),value:o.peek().defaultCategories,creatable:!0,placeholder:"None",onChange:()=>x("defaultCategories",n.getValue())});const p=S(t);B(e,()=>{g.dispose(),p(),n.destroy?.()}),e.open()}ae(v,{id:"notes-settings",icon:"settings",title:"Notes settings",onClick:X});const N=i.querySelector(".notes-list"),Z=E.contextMenu(N,{match:".note-card",inherit:!1,density:"compact",items:e=>{const t=e.dataset.id,s=b(t)?.categories||[];return[{label:"Edit",icon:"edit",action:()=>h(b(t))},{label:"Duplicate",icon:"copy",action:()=>G(t)},{label:"Copy text",icon:"clipboard",action:()=>J(t)},{label:"Show only\u2026",icon:"filter",disabled:s.length===0,submenu:s.map(n=>({label:n,icon:"tag",action:()=>{u.value=n}}))},{type:"divider"},{label:"Delete",icon:"trash",danger:!0,action:()=>q(t)}]}});d.add(()=>Z?.destroy?.()),N.addEventListener("click",e=>{const t=e.target.closest(".note-card");if(!t)return;const a=e.target.closest(".note-cat");if(a){u.value=a.dataset.cat;return}const s=e.target.closest(".note-kebab");if(s){se(s);return}h(b(t.dataset.id))}),N.addEventListener("keydown",e=>{const t=e.target.closest?.(".note-card");t&&e.key==="Enter"&&e.target===t&&h(b(t.dataset.id))});const F=e=>{e.ctrlKey||e.metaKey||e.altKey||e.target.closest?.("input, textarea, select, [contenteditable], .dm-slideover")||(e.key==="/"?(e.preventDefault(),A.value=!0):(e.key==="n"||e.key==="N")&&(e.preventDefault(),h(null)))};document.addEventListener("keydown",F),d.add(()=>document.removeEventListener("keydown",F)),d.add(()=>y?.close()),I.scan(i),await _()}};
@@ -1 +1 @@
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
+ import{api as u}from"../api.js";import{pluginAccent as P,pluginWatermark as S}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 u.marketplace.catalogue()}catch(s){E.toast(`Failed to load marketplace: ${s.message}`,{type:"error"}),a.destroy();return}if(a.destroy(),!t.available){e.find("#marketplace-unavailable").show();return}e.find("#marketplace-available").show(),t.site&&e.find("#marketplace-site").text(`Licensed to ${t.site}.`).show(),U(e,t.catalogue),Domma.icons.scan()}};function U(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 s=document.createElement("p");s.className="text-muted",s.textContent="No plugins available in the marketplace.",t.appendChild(s);return}a.forEach(s=>{const i=R(s,e);t.appendChild(i)})}}function F(e){if(e.tier!=="paid")return"Free";const a=e.terms?.managed?.monthly;return a==null||!Number.isFinite(Number(a))?"Paid":`${e.terms?.currency==="GBP"?"\xA3":(e.terms?.currency||"")+" "}${(Number(a)/100).toFixed(2)}/mo`}function R(e,a){const{slug:t,version:s,displayName:i,description:y,entitled:o,installed:l,installedVersion:p,updatable:b}=e,r=document.createElement("div");r.className="card plugin-card",r.dataset.slug=t,r.style.setProperty("--plugin-accent",P(t)),r.appendChild(S(e.icon));const h=document.createElement("div");h.className="card-body";const m=document.createElement("div");m.className="plugin-header";const N=document.createElement("div");N.className="plugin-icon";const I=document.createElement("span");I.dataset.icon=e.icon||"package",N.appendChild(I);const g=document.createElement("div");g.className="plugin-meta";const x=document.createElement("div");x.className="plugin-name",x.textContent=i||t;const k=document.createElement("div");k.className="plugin-version",k.textContent=l&&p&&p!==s?`v${p} \u2192 v${s}`:`v${s}`,g.appendChild(x),g.appendChild(k);const f=document.createElement("div");f.style.cssText="display:flex;gap:.35rem;align-items:center;";const w=document.createElement("span");if(w.className="badge badge-outline",w.textContent=F(e),f.appendChild(w),o&&e.tier==="paid"){const c=document.createElement("span");c.className="badge badge-success",c.textContent=e.licenceStatus==="grace"?"Renew":"Licensed",c.title=e.licenceStatus==="grace"?"The rental term has ended - still running during the grace period":"This site holds a live licence",f.appendChild(c)}m.appendChild(N),m.appendChild(g),m.appendChild(f);const $=document.createElement("p");$.className="plugin-desc",$.textContent=y||"",h.appendChild(m),h.appendChild($);const C=document.createElement("div");C.className="plugin-footer";const v=document.createElement("span");v.className="text-muted",v.style.fontSize=".8rem",v.textContent="Restart server after changes",C.appendChild(v);const d=document.createElement("div");d.className="plugin-footer-actions";const n=document.createElement("button");if(n.className="btn btn-sm",l&&b&&o){n.className+=" btn-primary",n.textContent="Update",n.onclick=()=>T(e,a,{update:!0}),d.appendChild(n);const c=document.createElement("button");c.className="btn btn-sm btn-ghost",c.textContent="Uninstall",c.onclick=()=>D(t,a),d.appendChild(c)}else l?(n.className+=" btn-ghost",n.textContent="Uninstall",n.onclick=()=>D(t,a),d.appendChild(n)):o?(n.className+=" btn-primary",n.textContent="Install",n.onclick=()=>T(e,a),d.appendChild(n)):(n.className+=" btn-ghost",n.textContent=e.licenceStatus==="expired"?"Licence expired":"Not licensed",n.disabled=!0,n.title="Contact us to add this to your site.",d.appendChild(n));return C.appendChild(d),r.appendChild(h),r.appendChild(C),r}async function T(e,a,{update:t=!1}={}){const{slug:s,version:i,installedVersion:y}=e,o=t?`<p>Update ${s} from ${y} to ${i}?</p><p class="text-muted">The plugin keeps its data, and the current version is put back if the update fails.</p>`:`<p>Install ${e.displayName||s} v${i}?</p><p class="text-muted">It arrives licensed to this site and takes effect after a restart.</p>`;if(await E.confirm(o,{title:t?"Update plugin":"Install plugin",confirmText:t?"Update":"Install",cancelText:"Cancel",html:!0}))try{const l=await u.marketplace.install(s,i,{update:t}),p=l?.action==="upgrade"?`${s} updated${l.from?` from ${l.from}`:""} to ${l.version}.`:`${s} ${l?.version??i} installed.`;E.toast(`${p} Restart the server to activate it.`,{type:"success"});const b=await u.marketplace.catalogue();U(a,b.catalogue),Domma.icons.scan()}catch(l){E.toast(l.message||"Install failed.",{type:"error"})}}async function D(e,a){if(await E.confirm(`Uninstall ${e}? This will remove its files.`))try{await u.marketplace.uninstall(e),E.toast(`${e} uninstalled. Restart to apply.`,{type:"success"});const t=await u.marketplace.catalogue();U(a,t.catalogue),Domma.icons.scan()}catch(t){E.toast(`Uninstall failed: ${t.message}`,{type:"error"})}}
@@ -1,41 +1,43 @@
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=`
1
+ import{api as f,getUser as C}from"../api.js";import{pluginMarketplaceView as x}from"./plugin-marketplace.js";import{pluginAccent as S,pluginWatermark as T}from"../lib/plugin-accent.js";import{reportRestart as N}from"../lib/server-restart.js";export const pluginsView={templateUrl:"/admin/js/templates/plugins.html",async onMount(s){s.find("#new-plugin-btn").on("click",()=>I(s));const c=s.find("#install-file-input").get(0);s.find("#install-file-btn").on("click",()=>c?.click()),c?.addEventListener("change",async()=>{const i=c.files?.[0];c.value="",i&&await L(s,i)});let o=!1;s.find('[data-tab="marketplace"]').on("click",async function(){if(o)return;const i=s.find("#marketplace-mount");try{const n=await fetch(x.templateUrl);if(!n.ok)throw new Error(`Template ${n.status}`);const d=await n.text(),p=new DOMParser().parseFromString(d,"text/html");for(;p.body.firstChild;)i.get(0).appendChild(p.body.firstChild);await x.onMount(i),o=!0}catch{E.toast("Failed to load marketplace.",{type:"error"})}}),E.tabs(s.find("#plugin-tabs").get(0));const e=E.loader(s.get(0),{type:"dots"});let t=[];try{t=await f.plugins.list()}catch(i){E.toast(`Failed to load plugins: ${i.message}`,{type:"error"})}if(e.destroy(),!t.length){s.find("#plugins-empty").show();return}y(s,t),Domma.icons.scan()}};async function L(s,c){const o=s.find("#install-file-btn"),e=()=>o.prop("disabled",!1).html('<span data-icon="upload"></span> Install from File');o.prop("disabled",!0).text("Installing\u2026");const t={};for(let i=0;i<4;i++)try{const n=await f.plugins.installFile(c,t),d=n.action==="upgrade"?`${n.name} updated${n.from?` from ${n.from}`:""} to ${n.version}.`:`${n.name} ${n.version} installed.`;E.toast(`${d} Restart the server to activate it.`,{type:"success"}),e();const l=await f.plugins.list();s.find("#plugins-empty").hide(),y(s,l);return}catch(n){if(n.code==="CONFLICT"&&!t.confirmUpgrade){const d=n.installedVersion||"the installed version",l=n.incomingVersion||"this file";if(!await E.confirm(`<p>${r(n.name)} ${r(d)} is already installed. Update it to ${r(l)}?</p><p class="text-muted">The plugin keeps its data, and the current version is put back if the update fails.</p>`,{title:"Update plugin",confirmText:"Update",cancelText:"Cancel",html:!0}))break;t.confirmUpgrade=!0;continue}if(/refusing to downgrade/i.test(n.message)&&!t.confirmDowngrade){if(!await E.confirm(`<p>${r(n.message)} Install it anyway?</p><p class="text-muted">An older version may not understand data written by the newer one.</p>`,{title:"Downgrade plugin",confirmText:"Downgrade",cancelText:"Cancel",html:!0}))break;t.confirmDowngrade=!0;continue}if(/unsigned|signature/i.test(n.message)&&!t.confirmUnsigned){if(!await E.confirm(`<p>${r(n.message)}</p><p class="text-muted">An unsigned plugin has not been verified as coming from the marketplace. Its code runs on this server with full access. Install it anyway?</p>`,{title:"Unsigned plugin",confirmText:"Install anyway",cancelText:"Cancel",html:!0}))break;t.confirmUnsigned=!0;continue}E.toast(n.message||"Install failed.",{type:"error"});break}e(),Domma.icons.scan()}function I(s){const c=E.slideover({title:"New Plugin",size:"sm",position:"right"}),o=document.createElement("div");o.style.cssText="padding:.25rem 0 .5rem;";const e=C(),t=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(t),o.appendChild(t);const i=document.createElement("p");i.className="text-muted",i.style.cssText="font-size:.8rem;margin:.5rem 0 0;",i.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.",o.appendChild(i);const n=document.createElement("div");n.style.cssText="display:flex;justify-content:flex-end;gap:1rem;margin-top:.75rem;";const d=document.createElement("button");d.className="btn btn-ghost",d.textContent="Cancel";const l=document.createElement("button");l.className="btn btn-primary",l.textContent="Create Plugin",n.appendChild(d),n.appendChild(l),o.appendChild(n),c.element.appendChild(o),c.open();const p=t.querySelector('input[name="displayName"]'),a=t.querySelector('input[name="slug"]');setTimeout(()=>p?.focus(),50);let u=!1;a?.addEventListener("input",()=>{u=!0}),p?.addEventListener("input",()=>{u||!a||(a.value=p.value.toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,40))});async function b(){const m={};for(const g of["displayName","slug","description","author","icon"])m[g]=t.querySelector(`input[name="${g}"]`)?.value.trim()||"";if(!m.displayName||!m.slug){E.toast("Display name and slug are required.",{type:"error"});return}l.disabled=!0,l.textContent="Creating\u2026";try{await f.plugins.scaffold(m),c.close(),E.toast(`Plugin "${m.displayName}" created. Restart the server to activate it.`,{type:"success"});const g=await f.plugins.list();g.length&&(s.find("#plugins-empty").hide(),y(s,g))}catch(g){E.toast(g.message||"Failed to create plugin.",{type:"error"}),l.disabled=!1,l.textContent="Create Plugin"}}d.addEventListener("click",()=>c.close()),l.addEventListener("click",b),a?.addEventListener("keydown",m=>{m.key==="Enter"&&b()})}function U(s){if(!s)return"";const c=new Date(s);if(Number.isNaN(c.getTime()))return"";try{return D(s).format("DD.MM.YYYY")}catch{return""}}function B(s){const o={grace:["Renew","var(--dm-warning,#d97706)","The rental term has ended - still running during the grace period"],"support-ended":["Support ended","var(--dm-info,#0284c7)","Owned outright and still running, but no longer entitled to updates"],expired:["Expired","var(--dm-danger,#dc2626)","The rental term and grace period have ended - this plugin is not running"],unlicensed:["Unlicensed","var(--dm-danger,#dc2626)","No valid entitlement for this site - this plugin is not running"]}[s];if(!o)return"";const[e,t,i]=o;return`<span class="badge badge-outline" title="${r(i)}" style="font-size:0.65rem;padding:1px 6px;background:none;border:1px solid;color:${t};border-color:${t};">${e}</span>`}function y(s,c){const o=s.find("#plugins-grid").empty();c.forEach(e=>{const t=!!e.core,i=e.sourceEditable!==!1,n="var(--dm-warning,#d97706)",d=e.closedSource?`<span class="badge badge-outline" title="${r(e.licence||"Commercial licence")} - source is not editable here" style="font-size:0.65rem;padding:1px 6px;background:none;border:1px solid;color:${n};border-color:${n};">Licensed</span>`:"",l=B(e.entitlement),p=e.enabled&&["expired","unlicensed"].includes(e.entitlement),a=e.enabled&&e.supersededBy,u=t?"badge-info":p?"badge-danger":a?"badge-secondary":e.enabled?"badge-success":"badge-secondary",b=t?"Built in":p?"Not running":a?"Replaced":e.enabled?"Enabled":"Disabled",m=e.enabled?"Disable":"Enable",g=e.enabled?"btn-ghost":"btn-primary",h=e.author?`by ${r(e.author)}`:"",v=U(e.date),w=e.icon||"package",k=`
2
2
  <div class="card plugin-card" data-plugin="${e.name}" style="--plugin-accent:${S(e.name)}">
3
- ${N(x).outerHTML}
3
+ ${T(w).outerHTML}
4
4
  <div class="card-body">
5
5
  <div class="plugin-header">
6
- <div class="plugin-icon"><span data-icon="${x}"></span></div>
6
+ <div class="plugin-icon"><span data-icon="${w}"></span></div>
7
7
  <div class="plugin-meta">
8
- <div class="plugin-name">${o(e.displayName)}</div>
9
- <div class="plugin-version">v${o(e.version)}${h?` &middot; ${h}`:""}</div>
8
+ <div class="plugin-name">${r(e.displayName)}</div>
9
+ <div class="plugin-version">v${r(e.version)}${h?` &middot; ${h}`:""}</div>
10
10
  </div>
11
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>
12
+ ${d}
13
+ ${l}
14
+ <span class="badge ${u}">${b}</span>
15
15
  </div>
16
16
  </div>
17
- <p class="plugin-desc">${o(e.description)}</p>
17
+ <p class="plugin-desc">${r(e.description)}</p>
18
18
  ${v?`<p class="plugin-date text-muted" style="font-size:.8rem;margin:0">Released ${v}</p>`:""}
19
+ ${a?`<p class="text-muted" style="font-size:.8rem;margin:.25rem 0 0">Replaced by ${r(e.supersededBy)}.</p>`:""}
19
20
  </div>
20
21
  <div class="plugin-footer">
21
- <span class="text-muted" style="font-size:.8rem">${n?"Part of the CMS - always on":"Restart server after changes"}</span>
22
+ <span class="text-muted" style="font-size:.8rem">${t?"Part of the CMS - always on":"Restart server after changes"}</span>
22
23
  <div class="plugin-footer-actions">
23
- ${s?`<button class="btn btn-sm btn-ghost btn-plugin-code" data-name="${e.name}"><span data-icon="code"></span> Code</button>`:""}
24
+ ${i?`<button class="btn btn-sm btn-ghost btn-plugin-code" data-name="${e.name}"><span data-icon="code"></span> Code</button>`:""}
25
+ ${e.closedSource?"":`<button class="btn btn-sm btn-ghost btn-plugin-export" data-name="${e.name}" data-tooltip="Export as .dcmsplugin"><span data-icon="download" data-icon-size="14"></span></button>`}
24
26
  ${e.settingsSchema?`<button class="btn btn-sm btn-ghost btn-plugin-settings" data-name="${e.name}"><span data-icon="settings"></span> Settings</button>`:""}
25
- ${n?"":`<button class="btn btn-sm ${p} btn-toggle-plugin"
27
+ ${t?"":`<button class="btn btn-sm ${g} btn-toggle-plugin"
26
28
  data-name="${e.name}"
27
29
  data-enabled="${e.enabled?"1":"0"}">
28
- <span data-icon="${e.enabled?"pause":"play"}"></span> ${t}
30
+ <span data-icon="${e.enabled?"pause":"play"}"></span> ${m}
29
31
  </button>`}
30
32
  </div>
31
33
  </div>
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>
35
- </div>`}return t.type==="number"?`<div class="form-group">
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}"`:""}>
34
+ </div>`;o.append(k)}),Domma.icons.scan(),Domma.effects.reveal(".plugin-card",{animation:"fade",stagger:60,duration:400}),Domma.effects.ripple(".btn-toggle-plugin"),o.on("click",".btn-plugin-code",function(){R.navigate(`/plugins/code/${$(this).data("name")}`)}),o.on("click",".btn-plugin-export",async function(){const e=$(this).data("name");try{await f.plugins.exportBundle(e)}catch(t){E.toast(t.message||"Export failed.",{type:"error"})}}),o.on("click",".btn-plugin-settings",async function(){const e=$(this).data("name"),t=c.find(a=>a.name===e);if(!t?.settingsSchema)return;const n=`<form id="plugin-settings-form">${t.settingsSchema.map(a=>{const u=t.settings?.[a.key]??a.default??"";if(a.type==="select"){const b=a.options.map(m=>`<option value="${r(m.value)}"${String(u)===String(m.value)?" selected":""}>${r(m.label)}</option>`).join("");return`<div class="form-group">
35
+ <label class="form-label">${r(a.label)}</label>
36
+ <select class="form-control" name="${r(a.key)}">${b}</select>
37
+ </div>`}return a.type==="number"?`<div class="form-group">
38
+ <label class="form-label">${r(a.label)}</label>
39
+ <input class="form-control" type="number" name="${r(a.key)}" value="${r(String(u))}"${a.min!=null?` min="${a.min}"`:""}${a.max!=null?` max="${a.max}"`:""}>
38
40
  </div>`:`<div class="form-group">
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;")}
41
+ <label class="form-label">${r(a.label)}</label>
42
+ <input class="form-control" type="text" name="${r(a.key)}" value="${r(String(u))}">
43
+ </div>`}).join("")}</form>`;if(!await E.confirm(n,{title:`${r(t.displayName)} Settings`,confirmText:"Save",cancelText:"Cancel",html:!0}))return;const l=document.getElementById("plugin-settings-form");if(!l)return;const p={};t.settingsSchema.forEach(a=>{const u=l.elements[a.key];u&&(p[a.key]=a.type==="number"?Number(u.value):u.value)});try{await f.plugins.update(e,{settings:p}),t.settings={...t.settings,...p},E.toast("Settings saved.",{type:"success"})}catch(a){E.toast(`Failed to save: ${a.message}`,{type:"error"})}}),o.on("click",".btn-toggle-plugin",async function(){const e=$(this),t=e.data("name"),i=e.data("enabled")!==1&&e.data("enabled")!=="1";e.prop("disabled",!0).text(i?"Enabling\u2026":"Disabling\u2026");try{const n=await f.plugins.update(t,{enabled:i});E.toast(`Plugin ${i?"enabled":"disabled"}.`,{type:"success"}),await N(n,{onStatus:l=>e.text(l.slice(0,24))});const d=await f.plugins.list();y(s,d)}catch(n){E.toast(`Failed: ${n.message}`,{type:"error"}),e.prop("disabled",!1)}})}function r(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}