domma-cms 0.66.0 → 0.69.3

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 +92 -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 +307 -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 @@
1
+ export const DEFAULT_SETTINGS=Object.freeze({layout:"cards",sort:"updated",previewLength:160,showCategories:!0,confirmDelete:!0,defaultCategories:[]});const p=new Set(["cards","list"]),f=new Set(["updated","created","title"]);export const PREVIEW_MIN=0,PREVIEW_MAX=600;export function normaliseSettings(r){const e=r&&typeof r=="object"?r:{},t=DEFAULT_SETTINGS,o=Number(e.previewLength);return{layout:p.has(e.layout)?e.layout:t.layout,sort:f.has(e.sort)?e.sort:t.sort,previewLength:Number.isFinite(o)?Math.min(PREVIEW_MAX,Math.max(PREVIEW_MIN,Math.round(o))):t.previewLength,showCategories:typeof e.showCategories=="boolean"?e.showCategories:t.showCategories,confirmDelete:typeof e.confirmDelete=="boolean"?e.confirmDelete:t.confirmDelete,defaultCategories:parseCategories(e.defaultCategories)}}export function parseCategories(r){const e=Array.isArray(r)?r:String(r??"").split(","),t=new Set,o=[];for(const i of e){const a=String(i??"").trim();!a||t.has(a.toLowerCase())||(t.add(a.toLowerCase()),o.push(a))}return o}export function collectCategories(r){return parseCategories((r||[]).flatMap(t=>Array.isArray(t.categories)?t.categories:[])).sort((t,o)=>t.localeCompare(o,void 0,{sensitivity:"base"}))}export function preview(r,e){const t=String(r??"").replace(/\s+/g," ").trim();if(!e||e<=0)return"";if(t.length<=e)return t;const o=t.slice(0,e),i=o.lastIndexOf(" ");return(i>e*.6?o.slice(0,i):o).trimEnd()+"\u2026"}const c=r=>{const e=Date.parse(r);return Number.isNaN(e)?0:e};export function arrangeNotes(r,{query:e="",category:t="",sort:o="updated"}={}){const i=String(e).trim().toLowerCase(),a=String(t).trim().toLowerCase(),l=(r||[]).filter(s=>{const n=Array.isArray(s.categories)?s.categories:[];return a&&!n.some(u=>String(u).toLowerCase()===a)?!1:i?String(s.title??"").toLowerCase().includes(i)||String(s.content??"").toLowerCase().includes(i)||n.some(u=>String(u).toLowerCase().includes(i)):!0}),d=o==="title"?(s,n)=>String(s.title||"").localeCompare(String(n.title||""),void 0,{sensitivity:"base"}):o==="created"?(s,n)=>c(n.createdAt)-c(s.createdAt):(s,n)=>c(n.updatedAt)-c(s.updatedAt);return[...l].sort(d)}
@@ -1 +1 @@
1
- const a={invoice:150,contacts:210,blog:265,analytics:285,"mail-reader":190,"email-pro":190,messages:220,notes:45,todo:25,surveys:330,calendar:10,"site-search":200,"theme-switcher":310,"demo-viewer":175,docs:240,garage:20,"form-builder":255,"site-manager":205,"site-tweaks":95,"shopping-cart":140,"job-board":35,"customer-registration":170,_template:0};function n(t){let e=0;for(let r=0;r<t.length;r++)e=(e*31+t.charCodeAt(r))%360;return e}function i(t){return t==="_template"?0:62}export function pluginAccent(t){const e=String(t||"");return`hsl(${a[e]??n(e)} ${i(e)}% 52%)`}export function pluginWatermark(t){const e=document.createElement("span");return e.className="plugin-watermark",e.setAttribute("data-icon",t||"package"),e.setAttribute("data-icon-size","148"),e.setAttribute("aria-hidden","true"),e}
1
+ function r(e){let t=0;for(let n=0;n<e.length;n++)t=(t*31+e.charCodeAt(n))%360;return t}function a(e){return e==="_template"?0:62}export function pluginAccent(e){const t=String(e||"");return`hsl(${r(t)} ${a(t)}% 52%)`}export function pluginWatermark(e){const t=document.createElement("span");return t.className="plugin-watermark",t.setAttribute("data-icon",e||"package"),t.setAttribute("data-icon-size","148"),t.setAttribute("aria-hidden","true"),t}
@@ -1 +1 @@
1
- const p=/^(built in|included\b)/i;export function formatReleased(n){if(!n||typeof n!="string"||!/^\d{4}-\d{2}-\d{2}/.test(n))return"";const e=new Date(n);if(Number.isNaN(e.getTime()))return"";const t=c=>String(c).padStart(2,"0");return`${t(e.getDate())}.${t(e.getMonth()+1)}.${e.getFullYear()}`}export function buildBanner(n){const e=n||{},t=document.createElement("header");t.className="dm-plugin-banner";const c=document.createElement("div");c.className="dm-plugin-banner-identity";const a=document.createElement("span");a.className="dm-plugin-banner-icon",a.setAttribute("data-icon",e.icon||"package"),a.setAttribute("data-icon-size","18"),c.appendChild(a);const r=document.createElement("h1");r.className="dm-plugin-banner-name",r.textContent=e.displayName||e.plugin||"Plugin",c.appendChild(r),t.appendChild(c);const o=document.createElement("div");o.className="dm-plugin-banner-facts",((i,l)=>{if(!i)return;const d=document.createElement("span");d.className="dm-plugin-fact"+(l?" "+l:""),d.textContent=i,o.appendChild(d)})(e.version?`v${e.version}`:"","dm-plugin-fact--version");const s=formatReleased(e.date);if(s){const i=document.createElement("span");i.className="dm-plugin-fact dm-plugin-fact--date",i.title="Release date",i.textContent=s,o.appendChild(i)}if(e.licence){const i=document.createElement("span");i.className="dm-plugin-fact dm-plugin-fact--licence"+(p.test(e.licence)?" is-included":" is-licensed"),i.title="Licence",i.textContent=e.licence,o.appendChild(i)}return t.appendChild(o),t}export function withPluginChrome(n,e){if(!n||typeof n!="object")return n;const t=Object.create(Object.getPrototypeOf(n));Object.assign(t,n);const c=typeof n.onMount=="function"?n.onMount:null;return t.onMount=async function(...a){const r=a[0],o=r&&typeof r.get=="function"?r.get(0):r;return o&&o.nodeType===1&&!o.querySelector(":scope > .dm-plugin-shell")&&applyChrome(o,e),c?c.apply(n,a):void 0},t}export function applyChrome(n,e){const t=document.createElement("section");t.className="dm-plugin-shell",e&&e.plugin&&t.setAttribute("data-plugin",e.plugin),t.appendChild(buildBanner(e));const c=document.createElement("div");for(c.className="dm-plugin-body";n.firstChild;)c.appendChild(n.firstChild);t.appendChild(c),n.appendChild(t),g(c,e),f(c),typeof I<"u"&&I.scan?I.scan(t):typeof Domma<"u"&&Domma.icons&&Domma.icons.scan()}function m(n){return[...n.children].filter(e=>!/^(STYLE|SCRIPT|TEMPLATE|LINK)$/.test(e.tagName))}function f(n){let e=n;for(let t=0;t<2;t++){const c=m(e);if(c.length!==1)return;if(e=c[0],e.classList.contains("card")){e.classList.add("dm-plugin-unwrap");return}}}function g(n,e){const t=String(e&&e.displayName||"").trim().toLowerCase(),c=n.querySelector(":scope > .view-header, :scope > * > .view-header");if(c){const a=c.querySelector("h1, h2"),r=c.querySelector("button, a, input, select");a&&r?a.remove():a&&c.remove();return}if(t){for(const a of n.querySelectorAll("h1, h2"))if(a.textContent.trim().toLowerCase()===t){a.remove();return}}}export function addBannerAction(n,{icon:e,title:t,onClick:c,id:a="",label:r=""}){const o=n&&typeof n.get=="function"?n.get(0):n;if(!o||o.nodeType!==1)return null;const u=o.querySelector(":scope > .dm-plugin-shell > .dm-plugin-banner")||o.closest(".dm-plugin-shell")?.querySelector(":scope > .dm-plugin-banner");if(!u)return null;let s=u.querySelector(":scope > .dm-plugin-banner-facts");s||(s=document.createElement("div"),s.className="dm-plugin-banner-facts",u.appendChild(s));let i=s.querySelector(":scope > .dm-plugin-banner-actions");i||(i=document.createElement("div"),i.className="dm-plugin-banner-actions",s.insertBefore(i,s.firstChild)),a&&i.querySelector(`[data-banner-action="${CSS.escape(a)}"]`)?.remove();const l=document.createElement("button");l.type="button",l.className="btn btn-sm btn-ghost dm-plugin-banner-action",l.title=t||"",l.setAttribute("aria-label",t||r||"Action"),a&&(l.dataset.bannerAction=a);const d=document.createElement("span");return d.setAttribute("data-icon",e||"settings"),d.setAttribute("data-icon-size","16"),l.appendChild(d),r&&l.appendChild(document.createTextNode(` ${r}`)),l.addEventListener("click",c),i.appendChild(l),typeof I<"u"&&I.scan?I.scan(l):typeof Domma<"u"&&Domma.icons&&Domma.icons.scan(l),l}
1
+ const m=/^(built in|included\b)/i;export function formatReleased(e){if(!e||typeof e!="string"||!/^\d{4}-\d{2}-\d{2}/.test(e))return"";const t=new Date(e);if(Number.isNaN(t.getTime()))return"";const n=a=>String(a).padStart(2,"0");return`${n(t.getDate())}.${n(t.getMonth()+1)}.${t.getFullYear()}`}export function expiryFact(e,t=new Date){if(!e||typeof e!="object")return null;const n=formatReleased(e.date);if(!n)return null;const a=e.kind==="rental",i=e.state==="support-ended"||e.state==="expired"||!a&&Date.parse(e.date)<t.getTime(),s=Date.parse(e.date)-t.getTime()<30*864e5;let o;return a?o=e.state==="grace"?`Expired ${n} (grace)`:`Expires ${n}`:o=i?`Support ended ${n}`:`Support to ${n}`,{text:o,title:a?"Licence expiry":"Support and updates expiry - the licence itself does not expire",tone:i||e.state==="grace"?a?"danger":"warning":s?"warning":"ok"}}export function buildBanner(e){const t=e||{},n=document.createElement("header");n.className="dm-plugin-banner";const a=document.createElement("div");a.className="dm-plugin-banner-identity";const i=document.createElement("span");i.className="dm-plugin-banner-icon",i.setAttribute("data-icon",t.icon||"package"),i.setAttribute("data-icon-size","18"),a.appendChild(i);const s=document.createElement("h1");s.className="dm-plugin-banner-name",s.textContent=t.displayName||t.plugin||"Plugin",a.appendChild(s),n.appendChild(a);const o=document.createElement("div");o.className="dm-plugin-banner-facts",((c,d)=>{if(!c)return;const p=document.createElement("span");p.className="dm-plugin-fact"+(d?" "+d:""),p.textContent=c,o.appendChild(p)})(t.version?`v${t.version}`:"","dm-plugin-fact--version");const r=formatReleased(t.date);if(r){const c=document.createElement("span");c.className="dm-plugin-fact dm-plugin-fact--date",c.title="Release date",c.textContent=r,o.appendChild(c)}if(t.licence){const c=document.createElement("span");c.className="dm-plugin-fact dm-plugin-fact--licence"+(m.test(t.licence)?" is-included":" is-licensed"),c.title="Licence",c.textContent=t.licence,o.appendChild(c)}const l=expiryFact(t.expiry);if(l){const c=document.createElement("span");c.className=`dm-plugin-fact dm-plugin-fact--expiry is-${l.tone}`,c.title=l.title,c.textContent=l.text,o.appendChild(c)}return n.appendChild(o),n}export function withPluginChrome(e,t){if(!e||typeof e!="object")return e;const n=Object.create(Object.getPrototypeOf(e));Object.assign(n,e);const a=typeof e.onMount=="function"?e.onMount:null;return n.onMount=async function(...i){const s=i[0],o=s&&typeof s.get=="function"?s.get(0):s;return o&&o.nodeType===1&&!o.querySelector(":scope > .dm-plugin-shell")&&applyChrome(o,t),a?a.apply(e,i):void 0},n}export function applyChrome(e,t){const n=document.createElement("section");n.className="dm-plugin-shell",t&&t.plugin&&n.setAttribute("data-plugin",t.plugin),n.appendChild(buildBanner(t));const a=document.createElement("div");for(a.className="dm-plugin-body";e.firstChild;)a.appendChild(e.firstChild);n.appendChild(a),e.appendChild(n),h(a,t),g(a),typeof I<"u"&&I.scan?I.scan(n):typeof Domma<"u"&&Domma.icons&&Domma.icons.scan()}function f(e){return[...e.children].filter(t=>!/^(STYLE|SCRIPT|TEMPLATE|LINK)$/.test(t.tagName))}function g(e){let t=e;for(let n=0;n<2;n++){const a=f(t);if(a.length!==1)return;if(t=a[0],t.classList.contains("card")){t.classList.add("dm-plugin-unwrap");return}}}function h(e,t){const n=String(t&&t.displayName||"").trim().toLowerCase(),a=e.querySelector(":scope > .view-header, :scope > * > .view-header");if(a){const i=a.querySelector("h1, h2"),s=a.querySelector("button, a, input, select");i&&s?i.remove():i&&a.remove();return}if(n){for(const i of e.querySelectorAll("h1, h2"))if(i.textContent.trim().toLowerCase()===n){i.remove();return}}}export function addBannerAction(e,{icon:t,title:n,onClick:a,id:i="",label:s=""}){const o=e&&typeof e.get=="function"?e.get(0):e;if(!o||o.nodeType!==1)return null;const u=o.querySelector(":scope > .dm-plugin-shell > .dm-plugin-banner")||o.closest(".dm-plugin-shell")?.querySelector(":scope > .dm-plugin-banner");if(!u)return null;let r=u.querySelector(":scope > .dm-plugin-banner-facts");r||(r=document.createElement("div"),r.className="dm-plugin-banner-facts",u.appendChild(r));let l=r.querySelector(":scope > .dm-plugin-banner-actions");l||(l=document.createElement("div"),l.className="dm-plugin-banner-actions",r.insertBefore(l,r.firstChild)),i&&l.querySelector(`[data-banner-action="${CSS.escape(i)}"]`)?.remove();const c=document.createElement("button");c.type="button",c.className="btn btn-sm btn-ghost dm-plugin-banner-action",c.title=n||"",c.setAttribute("aria-label",n||s||"Action"),i&&(c.dataset.bannerAction=i);const d=document.createElement("span");return d.setAttribute("data-icon",t||"settings"),d.setAttribute("data-icon-size","16"),c.appendChild(d),s&&c.appendChild(document.createTextNode(` ${s}`)),c.addEventListener("click",a),l.appendChild(c),typeof I<"u"&&I.scan?I.scan(c):typeof Domma<"u"&&Domma.icons&&Domma.icons.scan(c),c}
@@ -1 +1 @@
1
- export const TOOLS_FOLDER_TEXT="Tools",MANAGE_PLUGINS_URL="#/plugins",MANAGE_PLUGINS_ITEM={text:"Manage Plugins",url:MANAGE_PLUGINS_URL,icon:"sliders",permission:"plugins"};export function stripItemByUrl(e,o){const t=[];for(const s of e||[]){if(!s||s.url===o)continue;const n={...s};Array.isArray(s.items)&&s.items.length&&(n.items=stripItemByUrl(s.items,o)),t.push(n)}return t}export function groupPluginItems(e){const o=[],t=[];for(const n of e||[])if(!(!n||!n.item)){if(n.parent){o.push(n);continue}t.push(n.item)}return{toolsFolder:t.length?{text:TOOLS_FOLDER_TEXT,icon:"tool",items:t}:null,managePluginsItem:{...MANAGE_PLUGINS_ITEM},parented:o}}export function insertFoldersBeforeSystem(e,o){const t=(o||[]).filter(Boolean);if(!t.length)return e;const s=["system","documentation"],n=e.findIndex(r=>s.includes(String(r&&r.text||"").toLowerCase()));return n===-1?[...e,...t]:[...e.slice(0,n),...t,...e.slice(n)]}export function pruneEmptySynthesisedFolders(e){const o=new Set([TOOLS_FOLDER_TEXT]);return(e||[]).filter(t=>!t||!o.has(t.text)?!0:Array.isArray(t.items)&&t.items.length>0)}
1
+ export const TOOLS_FOLDER_TEXT="Tools",MANAGE_PLUGINS_URL="#/plugins",MANAGE_PLUGINS_ITEM={text:"Marketplace",url:MANAGE_PLUGINS_URL,icon:"store",permission:"plugins"};export function stripItemByUrl(n,o){const t=[];for(const s of n||[]){if(!s||s.url===o)continue;const e={...s};Array.isArray(s.items)&&s.items.length&&(e.items=stripItemByUrl(s.items,o)),t.push(e)}return t}export function groupPluginItems(n){const o=[],t=[];for(const e of n||[])if(!(!e||!e.item)){if(e.parent){o.push(e);continue}t.push(e.item)}return{toolsFolder:t.length?{text:TOOLS_FOLDER_TEXT,icon:"tool",items:t}:null,managePluginsItem:{...MANAGE_PLUGINS_ITEM},parented:o}}export function insertFoldersBeforeSystem(n,o){const t=(o||[]).filter(Boolean);if(!t.length)return n;const s=["system","documentation"],e=n.findIndex(r=>s.includes(String(r&&r.text||"").toLowerCase()));return e===-1?[...n,...t]:[...n.slice(0,e),...t,...n.slice(e)]}export function pruneEmptySynthesisedFolders(n){const o=new Set([TOOLS_FOLDER_TEXT]);return(n||[]).filter(t=>!t||!o.has(t.text)?!0:Array.isArray(t.items)&&t.items.length>0)}
@@ -1 +1 @@
1
- import{test as n}from"node:test";import s from"node:assert/strict";import{groupPluginItems as l,stripItemByUrl as m,insertFoldersBeforeSystem as u,pruneEmptySynthesisedFolders as i,TOOLS_FOLDER_TEXT as g,MANAGE_PLUGINS_URL as p}from"./sidebar-grouping.js";const a=(e,t=null)=>({parent:t,item:e});n("groupPluginItems routes every enabled item to Tools, whatever its core flag",()=>{const{toolsFolder:e}=l([a({text:"Analytics",url:"#/plugins/analytics",core:!0}),a({text:"Todo",url:"#/plugins/todo",core:!1}),a({text:"Notes",url:"#/plugins/notes"})]);s.equal(e.text,g),s.deepEqual(e.items.map(t=>t.text),["Analytics","Todo","Notes"])}),n("Manage Plugins is a standalone entry, not a folder",()=>{const{managePluginsItem:e}=l([a({text:"Todo",url:"#/plugins/todo"})]);s.equal(e.text,"Manage Plugins"),s.equal(e.url,p),s.equal(e.permission,"plugins"),s.ok(!("items"in e),"it must not carry a sub-tree")}),n("groupPluginItems omits the Tools folder only when nothing is enabled",()=>{const e=l([a({text:"Todo",url:"#/plugins/todo"})]);s.deepEqual(e.toolsFolder.items.map(o=>o.text),["Todo"]);const t=l([]);s.equal(t.toolsFolder,null),s.equal(t.managePluginsItem.text,"Manage Plugins")}),n("groupPluginItems keeps explicitly-parented items separate",()=>{const{parented:e,toolsFolder:t}=l([a({text:"Nested",url:"#/x"},"Content"),a({text:"Todo",url:"#/plugins/todo",core:!1})]);s.equal(e.length,1),s.equal(e[0].parent,"Content"),s.deepEqual(t.items.map(o=>o.text),["Todo"])}),n("each call returns its own Manage Plugins object",()=>{const e=l([]).managePluginsItem,t=l([]).managePluginsItem;s.notEqual(e,t),s.deepEqual(e,t)}),n("stripItemByUrl removes the management link at any depth",()=>{const e=[{text:"Overview",items:[{text:"Dashboard",url:"#/"}]},{text:"System",items:[{text:"Users",url:"#/users"},{text:"Plugins",url:"#/plugins"}]}],o=m(e,"#/plugins").find(r=>r.text==="System");s.deepEqual(o.items.map(r=>r.text),["Users"]),s.equal(e.find(r=>r.text==="System").items.length,2)}),n("insertFoldersBeforeSystem places Tools then Manage Plugins before System",()=>{const t=u([{text:"Overview"},{text:"Data"},{text:"System"},{text:"Documentation"}],[{text:"Tools",items:[]},{text:"Manage Plugins",url:"#/plugins"},null]);s.deepEqual(t.map(o=>o.text),["Overview","Data","Tools","Manage Plugins","System","Documentation"])}),n("insertFoldersBeforeSystem appends when no System/Documentation anchor",()=>{const t=u([{text:"Overview"},{text:"Data"}],[{text:"Manage Plugins",url:"#/plugins"}]);s.deepEqual(t.map(o=>o.text),["Overview","Data","Manage Plugins"])}),n("pruneEmptySynthesisedFolders drops an empty Tools folder but keeps built-ins",()=>{const e=[{text:"Overview",items:[]},{text:"Tools",items:[]}];s.deepEqual(i(e).map(t=>t.text),["Overview"])}),n("pruneEmptySynthesisedFolders leaves the standalone Manage Plugins alone",()=>{const e=[{text:"Tools",items:[{text:"Analytics"}]},{text:"Manage Plugins",url:"#/plugins"}];s.deepEqual(i(e).map(t=>t.text),["Tools","Manage Plugins"])});
1
+ import{test as o}from"node:test";import s from"node:assert/strict";import{groupPluginItems as a,stripItemByUrl as p,insertFoldersBeforeSystem as u,pruneEmptySynthesisedFolders as i,TOOLS_FOLDER_TEXT as m,MANAGE_PLUGINS_URL as d}from"./sidebar-grouping.js";const n=(e,t=null)=>({parent:t,item:e});o("groupPluginItems routes every enabled item to Tools, whatever its core flag",()=>{const{toolsFolder:e}=a([n({text:"Analytics",url:"#/plugins/analytics",core:!0}),n({text:"Todo",url:"#/plugins/todo",core:!1}),n({text:"Notes",url:"#/plugins/notes"})]);s.equal(e.text,m),s.deepEqual(e.items.map(t=>t.text),["Analytics","Todo","Notes"])}),o("Marketplace is a standalone entry, not a folder",()=>{const{managePluginsItem:e}=a([n({text:"Todo",url:"#/plugins/todo"})]);s.equal(e.text,"Marketplace"),s.equal(e.url,d),s.equal(e.permission,"plugins"),s.ok(!("items"in e),"it must not carry a sub-tree")}),o("groupPluginItems omits the Tools folder only when nothing is enabled",()=>{const e=a([n({text:"Todo",url:"#/plugins/todo"})]);s.deepEqual(e.toolsFolder.items.map(l=>l.text),["Todo"]);const t=a([]);s.equal(t.toolsFolder,null),s.equal(t.managePluginsItem.text,"Marketplace")}),o("groupPluginItems keeps explicitly-parented items separate",()=>{const{parented:e,toolsFolder:t}=a([n({text:"Nested",url:"#/x"},"Content"),n({text:"Todo",url:"#/plugins/todo",core:!1})]);s.equal(e.length,1),s.equal(e[0].parent,"Content"),s.deepEqual(t.items.map(l=>l.text),["Todo"])}),o("each call returns its own Marketplace object",()=>{const e=a([]).managePluginsItem,t=a([]).managePluginsItem;s.notEqual(e,t),s.deepEqual(e,t)}),o("stripItemByUrl removes the management link at any depth",()=>{const e=[{text:"Overview",items:[{text:"Dashboard",url:"#/"}]},{text:"System",items:[{text:"Users",url:"#/users"},{text:"Plugins",url:"#/plugins"}]}],l=p(e,"#/plugins").find(r=>r.text==="System");s.deepEqual(l.items.map(r=>r.text),["Users"]),s.equal(e.find(r=>r.text==="System").items.length,2)}),o("insertFoldersBeforeSystem places Tools then Marketplace before System",()=>{const t=u([{text:"Overview"},{text:"Data"},{text:"System"},{text:"Documentation"}],[{text:"Tools",items:[]},{text:"Marketplace",url:"#/plugins"},null]);s.deepEqual(t.map(l=>l.text),["Overview","Data","Tools","Marketplace","System","Documentation"])}),o("insertFoldersBeforeSystem appends when no System/Documentation anchor",()=>{const t=u([{text:"Overview"},{text:"Data"}],[{text:"Marketplace",url:"#/plugins"}]);s.deepEqual(t.map(l=>l.text),["Overview","Data","Marketplace"])}),o("pruneEmptySynthesisedFolders drops an empty Tools folder but keeps built-ins",()=>{const e=[{text:"Overview",items:[]},{text:"Tools",items:[]}];s.deepEqual(i(e).map(t=>t.text),["Overview"])}),o("pruneEmptySynthesisedFolders leaves the standalone Marketplace alone",()=>{const e=[{text:"Tools",items:[{text:"Analytics"}]},{text:"Marketplace",url:"#/plugins"}];s.deepEqual(i(e).map(t=>t.text),["Tools","Marketplace"])});
@@ -1,4 +1,4 @@
1
- import{api as c}from"../api.js";import{colourToCss as U,applyFloat as K,applyHighlight as O,itemHighlightAttrs as G}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{groupPluginItems as V,stripItemByUrl as Y,insertFoldersBeforeSystem as J,pruneEmptySynthesisedFolders as Q,MANAGE_PLUGINS_URL as Z}from"./sidebar-grouping.js";function h(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const ee=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu"}];function T(e,s){return s?!e||!e.length?!1:e.includes(s)?!0:["read","create","update","delete"].some(t=>e.includes(`${s}.${t}`)):!0}function q(e,s){const t=[];for(const n of e){if(n.hidden||n.permission&&!T(s,n.permission))continue;const a=Array.isArray(n.items)&&n.items.length?q(n.items,s):[];t.push({...n,items:a})}return t}function te(e,s){if(!s||!s.length)return e;const t=(n,a)=>{for(const m of n){if((m.text||"").toLowerCase()===a.toLowerCase())return m;if(Array.isArray(m.items)){const r=t(m.items,a);if(r)return r}}return null};for(const n of s)if(n.parent){const a=t(e,n.parent);a?(a.items=Array.isArray(a.items)?a.items:[],a.items.push(n.item)):e.push(n.item)}else e.push(n.item);return e}async function ne(e){if(!T(e,"projects"))return[];let s=[];try{s=await c.projects.list()}catch{return[]}const t={};await Promise.all(s.map(async a=>{try{const m=await c.projects.artefacts(a.slug);t[a.slug]=m}catch{t[a.slug]={}}}));const n=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return s.map(a=>{const r="#/projects/"+encodeURIComponent(a.slug),d=t[a.slug]||{},g=[{text:"Overview",url:r,icon:a.icon||"folder"}];for(const p of n){const b=d[p.key],f=Array.isArray(b)?b.length:0;f<=0||g.push({text:`${p.text} (${f})`,url:r+p.path,icon:p.icon})}g.push({text:"Settings",url:r+"/settings",icon:"settings"});const y=a.enabled===!1;return{text:a.name||a.slug,icon:a.icon||"folder",items:g,...y&&{colour:"neutral",badge:{text:"off"}}}})}function se(e,s){for(const t of e)if((t.text||"").toLowerCase()==="projects"){t.items=Array.isArray(t.items)?t.items.slice():[],t.items.push(...s);return}}function ie(e,s){if(!e.url||!s)return"";const t=s[e.url];return t==null||t<=0?"":`<span class="sidebar-badge">${h(String(t))}</span>`}function ae(e){const s=(e.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),t=(e.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${t?`project-${decodeURIComponent(t[1])}.`:""}${s}`}function X(e,s){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="spacer"){const f=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${h(f)}" aria-hidden="true"></div>`}const t=e.icon?`<span data-icon="${h(e.icon)}"></span>`:"",n=h(e.text||""),a=e.url?h(e.url):"",m=Array.isArray(e.items)&&e.items.length>0,r=e.colour?U(e.colour):"",d=G(e),g=[d.hover&&` data-menu-item-hover="${h(d.hover)}"`,d.active&&` data-menu-item-active="${h(d.active)}"`].filter(Boolean).join(""),y=[r&&`color:${h(r)}`,d.accent&&`--dm-menu-highlight:${h(d.accent)}`].filter(Boolean).join(";"),p=y?` style="${y}"`:"";let b="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const f=e.badge.variant?U(e.badge.variant):"";b=`<span class="dm-menu-badge"${f?` style="background:${h(f)};color:var(--dm-text-inverse,#fff)"`:""}>${h(String(e.badge.text))}</span>`}if(m){const f=ae(e),v=S.get(f)!==!1,w=e.items.map(k=>X(k,s)).join("");return`<details data-state-key="${h(f)}"${v?" open":""}>
2
- <summary${p}>${t} <span class="sidebar-text">${n}</span>${b}</summary>
1
+ import{api as f}from"../api.js";import{colourToCss as K,applyFloat as ne,applyHighlight as se,itemHighlightAttrs as ie}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{groupPluginItems as ae,stripItemByUrl as oe,insertFoldersBeforeSystem as re,pruneEmptySynthesisedFolders as le,MANAGE_PLUGINS_URL as ce}from"./sidebar-grouping.js";function g(e){return String(e??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}const de=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu"}];function V(e,s){return s?!e||!e.length?!1:e.includes(s)?!0:["read","create","update","delete"].some(n=>e.includes(`${s}.${n}`)):!0}function G(e,s){const n=[];for(const t of e){if(t.hidden||t.permission&&!V(s,t.permission))continue;const i=Array.isArray(t.items)&&t.items.length?G(t.items,s):[];n.push({...t,items:i})}return n}function ue(e,s){if(!s||!s.length)return e;const n=(t,i)=>{for(const o of t){if((o.text||"").toLowerCase()===i.toLowerCase())return o;if(Array.isArray(o.items)){const a=n(o.items,i);if(a)return a}}return null};for(const t of s)if(t.parent){const i=n(e,t.parent);i?(i.items=Array.isArray(i.items)?i.items:[],i.items.push(t.item)):e.push(t.item)}else e.push(t.item);return e}async function me(e){if(!V(e,"projects"))return[];let s=[];try{s=await f.projects.list()}catch{return[]}const n={};await Promise.all(s.map(async i=>{try{const o=await f.projects.artefacts(i.slug);n[i.slug]=o}catch{n[i.slug]={}}}));const t=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return s.map(i=>{const a="#/projects/"+encodeURIComponent(i.slug),r=n[i.slug]||{},c=[{text:"Overview",url:a,icon:i.icon||"folder"}];for(const d of t){const b=r[d.key],h=Array.isArray(b)?b.length:0;h<=0||c.push({text:`${d.text} (${h})`,url:a+d.path,icon:d.icon})}c.push({text:"Settings",url:a+"/settings",icon:"settings"});const u=i.enabled===!1;return{text:i.name||i.slug,icon:i.icon||"folder",items:c,...u&&{colour:"neutral",badge:{text:"off"}}}})}function fe(e,s){for(const n of e)if((n.text||"").toLowerCase()==="projects"){n.items=Array.isArray(n.items)?n.items.slice():[],n.items.push(...s);return}}function J(e,s){const n=Q.includes(s.tone)?s.tone:"info",t=s.label||String(s.count);s.details?N.set(e,s.details):N.delete(e);const i=C.some(o=>o.url===e)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${n}" data-count-for="${g(e)}"${i} data-badge-label="${g(t)}" aria-label="${g(t)}">${g(String(s.count))}</span>`}function Y(e){for(const s of e.querySelectorAll("details")){const n=s.querySelector(":scope > summary");if(!n)continue;let t=0,i=!1;const o=[];for(const d of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const b=Number(d.textContent)||0;b&&(t+=b,i||(i=d.classList.contains("sidebar-badge--danger")),o.push({text:d.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:d.dataset.badgeLabel,tone:d.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const a=`folder:${s.dataset.stateKey||n.textContent.trim()}`;let r=n.querySelector(":scope > .sidebar-badge--sum");if(!t){r?.remove(),N.delete(a);continue}r||(r=document.createElement("span"),n.querySelector(".sidebar-text")?.after(r));const c=n.querySelector(".sidebar-text")?.textContent.trim()||"This folder",u=o.map(d=>`${d.text}: ${d.meta}`).join(" \xB7 ");r.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${i?"danger":"info"}`,r.dataset.countFor=a,r.dataset.badgeLabel=u,r.setAttribute("aria-label",u),r.textContent=String(t),N.set(a,{title:c,items:o})}}const N=new Map;function ge(e,s){if(!e.url||!s)return"";const n=s[e.url];return!n||!(n.count>0)?"":J(e.url,n)}const Q=["info","attention","danger","live"],pe={"#/pages":["page","pages"],"#/media":["file in the media library","files in the media library"],"#/menus":["menu","menus"],"#/context-menus":["context menu","context menus"],"#/theme":["element override in the theme","element overrides in the theme"],"#/collections":["collection","collections"],"#/forms":["form","forms"],"#/views":["view","views"],"#/actions":["action","actions"],"#/blocks":["block","blocks"],"#/components":["component","components"],"#/users":["user","users"],"#/plugins":["plugin installed","plugins installed"],"#/system/notifications":["unread notification","unread notifications"]};function T(e,{fallbackLabel:s="",tone:n="info"}={}){const t=Number(typeof e=="object"&&e!==null?e.count:e);if(!Number.isFinite(t)||t<0)return null;const i=typeof e?.label=="string"&&e.label||(s?`${t} - ${s}`:String(t)),o={count:Math.round(t),label:i,tone:Q.includes(e?.tone)?e.tone:n};return e?.details&&typeof e.details=="object"&&(o.details=e.details),o}function he(e){const s=(e.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),n=(e.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${n?`project-${decodeURIComponent(n[1])}.`:""}${s}`}function Z(e,s){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';if(e&&e.type==="spacer"){const h=["xs","sm","md","lg","xl","flex"].includes(e.size)?e.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${g(h)}" aria-hidden="true"></div>`}const n=e.icon?`<span data-icon="${g(e.icon)}"></span>`:"",t=g(e.text||""),i=e.url?g(e.url):"",o=Array.isArray(e.items)&&e.items.length>0,a=e.colour?K(e.colour):"",r=ie(e),c=[r.hover&&` data-menu-item-hover="${g(r.hover)}"`,r.active&&` data-menu-item-active="${g(r.active)}"`].filter(Boolean).join(""),u=[a&&`color:${g(a)}`,r.accent&&`--dm-menu-highlight:${g(r.accent)}`].filter(Boolean).join(";"),d=u?` style="${u}"`:"";let b="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const h=e.badge.variant?K(e.badge.variant):"";b=`<span class="dm-menu-badge"${h?` style="background:${g(h)};color:var(--dm-text-inverse,#fff)"`:""}>${g(String(e.badge.text))}</span>`}if(o){const h=he(e),v=S.get(h)!==!1,w=e.items.map(k=>Z(k,s)).join("");return`<details data-state-key="${g(h)}"${v?" open":""}>
2
+ <summary${d}>${n} <span class="sidebar-text">${t}</span>${b}</summary>
3
3
  <div class="sidebar-children">${w}</div>
4
- </details>`}return`<a href="${a}" class="sidebar-link" data-url="${a}"${p}${g}>${t} <span class="sidebar-text">${n}</span>${ie(e,s)}${b}</a>`}async function re(e=[]){const s=o=>o.then(x=>Array.isArray(x)?x.length:Array.isArray(x?.entries)?x.entries.length:0).catch(()=>0),[t,n,a,m,r,d,g,y,p,b,f,v,w,k]=await Promise.all([s(c.pages.list()),s(c.media.list()),s(c.menus.list()),s(c.contextMenus.list()),c.theme.get().then(o=>(o?.overrides||[]).length).catch(()=>0),s(c.collections.list()),s(c.forms.list()),s(c.views.list()),s(c.actions.list()),s(c.blocks.list()),s(c.components.list()),s(c.users.list()),s(c.plugins.list()),c.system?.notifications?.unreadCount?.().then(o=>o?.count??0).catch(()=>0)??Promise.resolve(0)]),E={};return await Promise.all((e??[]).map(o=>o?.item??o).filter(o=>o?.url&&typeof o.countUrl=="string"&&o.countUrl).map(async o=>{try{const x=await H.get(o.countUrl),C=Number(x?.count);Number.isFinite(C)&&C>0&&(E[o.url]=C)}catch{}})),{...E,"#/pages":t,"#/media":n,"#/menus":a,"#/context-menus":m,"#/theme":r,"#/collections":d,"#/forms":g,"#/views":y,"#/actions":p,"#/blocks":b,"#/components":f,"#/users":v,"#/plugins":w,"#/system/notifications":k}}async function oe(){try{const e=await c.settings.get();return e?.adminBrand?.title||e?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:s}){const t=$(e).get(0);if(!t)return;let n,a=null,m=null,r=null,d=null,g=null;try{const i=await c.menus.get("admin-sidebar");n=Array.isArray(i?.items)?i.items:null,a=i?.variant||null,m=i?.position||null,r=i?.style||null,d=i?.float||null,g=i?.highlight||null}catch{n=null}(!n||!n.length)&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),n=ee.slice());let y=[];try{y=await H.get("/api/sidebar/registered-items")||[]}catch{}n=Y(n,Z);const{toolsFolder:p,managePluginsItem:b,parented:f}=V(y);n=te(n,f),n=J(n,[p,b]);const v=await ne(s);v.length&&se(n,v),n=q(n,s),n=Q(n);const[w,k]=await Promise.all([re(y).catch(()=>({})),oe()]),E=a?` dm-admin-sidebar--${h(a)}`:"";function o(i,u="px"){if(i==null)return i;const l=String(i).trim();return/^\d+(\.\d+)?$/.test(l)?`${l}${u}`:l}let x="";if(r){const i=[];r.fontFamily&&i.push(`font-family: ${r.fontFamily}, sans-serif`),r.fontSize&&i.push(`font-size: ${o(r.fontSize)}`),r.fontWeight&&i.push(`font-weight: ${r.fontWeight}`),r.letterSpacing&&i.push(`letter-spacing: ${o(r.letterSpacing,"em")}`);const u=[];if(i.length&&u.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${i.join("; ")} }`),r.iconSize){const l=o(r.iconSize);u.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${l} !important; height: ${l} !important; }`)}u.length&&(x=`<style data-admin-sidebar-style>${u.join(" ")}</style>`)}const C=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${h(k)}</div>`,W=n.some(i=>i&&i.type==="spacer"&&i.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",D=`${x}<nav class="dm-admin-sidebar${E}${W}">${C}${n.map(i=>X(i,w)).join("")}</nav>`,B=document.createRange();B.selectNodeContents(t);const _=B.createContextualFragment(D);t.replaceChildren(_),Domma.icons.scan(t);const j=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(j)&&j>=180&&j<=480&&(t.style.width=j+"px"),t.removeAttribute("data-menu-hover"),t.removeAttribute("data-menu-active"),t.style.removeProperty("--dm-menu-highlight"),O(t,{highlight:g});const A=document.querySelector(".dashboard-wrapper");if(["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(i=>{t.style[i]=""}),t.classList.remove("dm-menu-floating"),A&&(A.style.paddingLeft="",A.style.paddingRight=""),m==="floating"){K(t,{position:"floating",float:d||{}});const u=d&&d.anchor||"TL",l=Number.isFinite(Number(d?.offsetX))?Number(d.offsetX):16,P=t.getBoundingClientRect().width||j||260;if(A){const z=Math.round(P+l*2)+"px";u==="R"||u==="TR"||u==="BR"?A.style.paddingRight=z:A.style.paddingLeft=z}}const L=document.createElement("div");L.className="dm-admin-sidebar-handle",L.setAttribute("aria-label","Resize sidebar"),t.appendChild(L),t.querySelectorAll("details").forEach(i=>{i.addEventListener("toggle",function(){const u=this.getAttribute("data-state-key");u&&S.set(u,this.open);const l=this.querySelector(":scope > .sidebar-children");if(l)if(this.open){const P=l.scrollHeight;l.style.maxHeight="0",requestAnimationFrame(()=>{l.style.maxHeight=P+"px",l.addEventListener("transitionend",function z(){l.style.maxHeight="none",l.removeEventListener("transitionend",z)})})}else{const P=l.scrollHeight;l.style.maxHeight=P+"px",requestAnimationFrame(()=>{l.style.maxHeight="0"})}})});let F=!1,R=0,I=0;L.addEventListener("mousedown",i=>{F=!0,R=i.clientX,I=t.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",i.preventDefault()}),document.addEventListener("mousemove",i=>{if(!F)return;const u=Math.max(180,Math.min(480,I+(i.clientX-R)));t.style.width=u+"px"}),document.addEventListener("mouseup",()=>{F&&(F=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(t.style.width,10)))});function N(){const i=location.hash||"#/";$(t).find(".sidebar-link").removeClass("active"),$(t).find(`.sidebar-link[data-url="${i}"]`).addClass("active")}N(),M.subscribe("router:afterChange",N)}
4
+ </details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${d}${c}>${n} <span class="sidebar-text">${t}</span>${ge(e,s)}${b}</a>`}async function be(e=[]){const s=y=>y.then(x=>Array.isArray(x)?x.length:Array.isArray(x?.entries)?x.entries.length:0).catch(()=>0),[n,t,i,o,a,r,c,u,d,b,h,v,w,k]=await Promise.all([s(f.pages.list()),s(f.media.list()),s(f.menus.list()),s(f.contextMenus.list()),f.theme.get().then(y=>(y?.overrides||[]).length).catch(()=>0),s(f.collections.list()),s(f.forms.list()),s(f.views.list()),s(f.actions.list()),s(f.blocks.list()),s(f.components.list()),s(f.users.list()),s(f.plugins.list()),f.system?.notifications?.unreadCount?.().then(y=>y?.count??0).catch(()=>0)??Promise.resolve(0)]);C=ve(e);const B={};await Promise.all(C.map(async y=>{const x=await ee(y);x&&(B[y.url]=x)}));const E={"#/pages":n,"#/media":t,"#/menus":i,"#/context-menus":o,"#/theme":a,"#/collections":r,"#/forms":c,"#/views":u,"#/actions":d,"#/blocks":b,"#/components":h,"#/users":v,"#/plugins":w,"#/system/notifications":k},j={};for(const[y,x]of Object.entries(E)){const[U,R]=pe[y]??["item","items"],A=T(x);A&&(j[y]={...A,label:`${A.count} ${A.count===1?U:R}`})}return{...j,...B}}let C=[];const ye="dm:sidebar-count",xe=6e4;function ve(e=[]){const s=[],n=t=>{if(t){t.url&&typeof t.countUrl=="string"&&t.countUrl&&s.push({url:t.url,countUrl:t.countUrl,text:t.text||"",countLabel:t.countLabel||""});for(const i of[...t.children||[],...t.items||[]])n(i)}};for(const t of e??[])n(t?.item??t);return s}async function ee(e){try{const s=new Date,n=`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(s.getDate()).padStart(2,"0")}`,t=e.countUrl+(e.countUrl.includes("?")?"&":"?")+`today=${n}`;return T(await H.get(t),{fallbackLabel:e.countLabel||e.text})}catch{return null}}function te(e,s,n){const t=[...e.querySelectorAll("a.sidebar-link")].find(o=>o.dataset.url===s);if(!t)return;const i=t.querySelector(".sidebar-badge[data-count-for]");if(!n||!(n.count>0))i?.remove();else{const o=document.createRange();o.selectNodeContents(t);const a=o.createContextualFragment(J(s,n)).firstElementChild;i?i.replaceWith(a):t.querySelector(".sidebar-text")?.after(a)}O.set(s,n),Y(e)}const O=new Map;async function _(e,s=null){const n=s?C.filter(t=>t.url===s):C;await Promise.all(n.map(async t=>{const i=await ee(t),o=O.get(t.url);JSON.stringify(o??null)!==JSON.stringify(i)&&te(e,t.url,i)}))}function $e(e){e.dataset.liveCounts||(e.dataset.liveCounts="1",window.addEventListener(ye,s=>{const n=s.detail||{};if(n.url)if(n.count!==void 0){const t=C.find(i=>i.url===n.url);te(e,n.url,T(n,{fallbackLabel:t?.countLabel||t?.text||""}))}else _(e,n.url)}),setInterval(()=>{!document.hidden&&document.body.contains(e)&&_(e)},xe),document.addEventListener("visibilitychange",()=>{document.hidden||_(e)}),Se(e))}function Ce(e,s,n){const t=(a,r,c)=>{const u=document.createElement(a);return r&&(u.className=r),c!=null&&(u.textContent=String(c)),u};if(!n){e.textContent=s;return}e.appendChild(t("strong","dm-badge-pop-title",n.title||s)),(n.rows?.length||!n.items?.length)&&e.appendChild(t("p","dm-badge-pop-sub",s));const i=Array.isArray(n.rows)?n.rows.slice(0,6):[];if(i.length){const a=t("dl","dm-badge-pop-rows");for(const r of i)a.appendChild(t("dt",null,r.label)),a.appendChild(t("dd",r.tone==="danger"?"is-danger":null,r.value));e.appendChild(a)}const o=Array.isArray(n.items)?n.items.slice(0,6):[];if(o.length){const a=t("ul","dm-badge-pop-items");for(const r of o){const c=t("li",r.tone==="danger"?"is-danger":null);c.appendChild(t("span","dm-badge-pop-text",r.text)),r.meta&&c.appendChild(t("span","dm-badge-pop-meta",r.meta)),a.appendChild(c)}e.appendChild(a)}else n.empty&&e.appendChild(t("p","dm-badge-pop-empty",n.empty))}function Se(e){let s=null,n=null;const t=()=>{clearTimeout(n),s?.remove(),s=null},i=o=>{t();const a=o?.dataset.badgeLabel;a&&(n=setTimeout(()=>{s=document.createElement("div"),s.className="dm-help-pop dm-badge-pop",s.setAttribute("role","tooltip"),Ce(s,a,N.get(o.dataset.countFor)),document.body.appendChild(s);const r=o.getBoundingClientRect(),c=s.getBoundingClientRect(),u=Math.min(r.right+10,innerWidth-c.width-8);s.style.left=`${Math.max(8,u)}px`,s.style.top=`${Math.max(8,Math.min(r.top+r.height/2-c.height/2,innerHeight-c.height-8))}px`},150))};e.addEventListener("mouseover",o=>{const a=o.target.closest?.(".sidebar-badge[data-badge-label]");a&&i(a)}),e.addEventListener("mouseout",o=>{o.target.closest?.(".sidebar-badge[data-badge-label]")&&t()}),e.addEventListener("focusin",o=>{const a=o.target.closest?.("a, summary")?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");a?i(a):t()}),e.addEventListener("focusout",t),window.addEventListener("scroll",t,!0),window.addEventListener("hashchange",t)}async function we(){try{const e=await f.settings.get();return e?.adminBrand?.title||e?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:s}){const n=$(e).get(0);if(!n)return;let t,i=null,o=null,a=null,r=null,c=null;try{const l=await f.menus.get("admin-sidebar");t=Array.isArray(l?.items)?l.items:null,i=l?.variant||null,o=l?.position||null,a=l?.style||null,r=l?.float||null,c=l?.highlight||null}catch{t=null}(!t||!t.length)&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),t=de.slice());let u=[];try{u=await H.get("/api/sidebar/registered-items")||[]}catch{}t=oe(t,ce);const{toolsFolder:d,managePluginsItem:b,parented:h}=ae(u);t=ue(t,h),t=re(t,[d,b]);const v=await me(s);v.length&&fe(t,v),t=G(t,s),t=le(t);const[w,k]=await Promise.all([be(u).catch(()=>({})),we()]),B=i?` dm-admin-sidebar--${g(i)}`:"";function E(l,p="px"){if(l==null)return l;const m=String(l).trim();return/^\d+(\.\d+)?$/.test(m)?`${m}${p}`:m}let j="";if(a){const l=[];a.fontFamily&&l.push(`font-family: ${a.fontFamily}, sans-serif`),a.fontSize&&l.push(`font-size: ${E(a.fontSize)}`),a.fontWeight&&l.push(`font-weight: ${a.fontWeight}`),a.letterSpacing&&l.push(`letter-spacing: ${E(a.letterSpacing,"em")}`);const p=[];if(l.length&&p.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${l.join("; ")} }`),a.iconSize){const m=E(a.iconSize);p.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${m} !important; height: ${m} !important; }`)}p.length&&(j=`<style data-admin-sidebar-style>${p.join(" ")}</style>`)}const y=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${g(k)}</div>`,x=t.some(l=>l&&l.type==="spacer"&&l.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",U=`${j}<nav class="dm-admin-sidebar${B}${x}">${y}${t.map(l=>Z(l,w)).join("")}</nav>`,R=document.createRange();R.selectNodeContents(n);const A=R.createContextualFragment(U);n.replaceChildren(A),Domma.icons.scan(n);for(const l of C)O.set(l.url,w[l.url]??null);Y(n),$e(n);const P=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(P)&&P>=180&&P<=480&&(n.style.width=P+"px"),n.removeAttribute("data-menu-hover"),n.removeAttribute("data-menu-active"),n.style.removeProperty("--dm-menu-highlight"),se(n,{highlight:c});const L=document.querySelector(".dashboard-wrapper");if(["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(l=>{n.style[l]=""}),n.classList.remove("dm-menu-floating"),L&&(L.style.paddingLeft="",L.style.paddingRight=""),o==="floating"){ne(n,{position:"floating",float:r||{}});const p=r&&r.anchor||"TL",m=Number.isFinite(Number(r?.offsetX))?Number(r.offsetX):16,F=n.getBoundingClientRect().width||P||260;if(L){const z=Math.round(F+m*2)+"px";p==="R"||p==="TR"||p==="BR"?L.style.paddingRight=z:L.style.paddingLeft=z}}const q=document.createElement("div");q.className="dm-admin-sidebar-handle",q.setAttribute("aria-label","Resize sidebar"),n.appendChild(q),n.querySelectorAll("details").forEach(l=>{l.addEventListener("toggle",function(){const p=this.getAttribute("data-state-key");p&&S.set(p,this.open);const m=this.querySelector(":scope > .sidebar-children");if(m)if(this.open){const F=m.scrollHeight;m.style.maxHeight="0",requestAnimationFrame(()=>{m.style.maxHeight=F+"px",m.addEventListener("transitionend",function z(){m.style.maxHeight="none",m.removeEventListener("transitionend",z)})})}else{const F=m.scrollHeight;m.style.maxHeight=F+"px",requestAnimationFrame(()=>{m.style.maxHeight="0"})}})});let I=!1,D=0,W=0;q.addEventListener("mousedown",l=>{I=!0,D=l.clientX,W=n.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",l.preventDefault()}),document.addEventListener("mousemove",l=>{if(!I)return;const p=Math.max(180,Math.min(480,W+(l.clientX-D)));n.style.width=p+"px"}),document.addEventListener("mouseup",()=>{I&&(I=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(n.style.width,10)))});function X(){const l=location.hash||"#/";$(n).find(".sidebar-link").removeClass("active"),$(n).find(`.sidebar-link[data-url="${l}"]`).addClass("active")}X(),M.subscribe("router:afterChange",X)}
@@ -0,0 +1 @@
1
+ export const STATUSES=["not-started","in-progress","completed"],PRIORITIES=["highest","high","medium","low","lowest"],FILTERS=["open","all","not-started","in-progress","completed","overdue"],SORTS=["created","priority","due","status"],DEFAULT_SETTINGS=Object.freeze({defaultPriority:"medium",defaultFilter:"open",sort:"created",relativeDue:!0,confirmDelete:!0});const c=(r,e,t)=>e.includes(r)?r:t;export function normaliseSettings(r){const e=r&&typeof r=="object"?r:{},t=DEFAULT_SETTINGS;return{defaultPriority:c(e.defaultPriority,PRIORITIES,t.defaultPriority),defaultFilter:c(e.defaultFilter,FILTERS,t.defaultFilter),sort:c(e.sort,SORTS,t.sort),relativeDue:typeof e.relativeDue=="boolean"?e.relativeDue:t.relativeDue,confirmDelete:typeof e.confirmDelete=="boolean"?e.confirmDelete:t.confirmDelete}}export function startOfDay(r=new Date){const e=new Date(r);return e.setHours(0,0,0,0),e.getTime()}export function dueDay(r){if(!r)return null;const e=/^(\d{4})-(\d{2})-(\d{2})/.exec(String(r));return e?new Date(Number(e[1]),Number(e[2])-1,Number(e[3])).getTime():null}export function daysUntil(r,e=new Date){const t=dueDay(r);return t===null?null:Math.round((t-startOfDay(e))/864e5)}export function isOverdue(r,e=new Date){const t=daysUntil(r.dueAt,e);return t!==null&&t<0&&r.status!=="completed"}export function dueLabel(r,{relative:e=!0,now:t=new Date,format:i}={}){const s=daysUntil(r,t);return s===null?"":e?s===0?"Today":s===1?"Tomorrow":s===-1?"Yesterday":s>0?`In ${s} days`:`${-s} days ago`:i?i(dueDay(r)):String(r).slice(0,10)}export function isoDay(r,e=new Date){const t=new Date(startOfDay(e)+r*864e5+432e5),i=s=>String(s).padStart(2,"0");return`${t.getFullYear()}-${i(t.getMonth()+1)}-${i(t.getDate())}`}export function arrangeTodos(r,{filter:e="open",query:t="",sort:i="created",now:s=new Date}={}){const l=String(t).trim().toLowerCase(),p=(r||[]).filter(n=>e==="open"&&n.status==="completed"||STATUSES.includes(e)&&n.status!==e||e==="overdue"&&!isOverdue(n,s)?!1:!l||String(n.text??"").toLowerCase().includes(l)),u=(n,o)=>{const f=n.indexOf(o);return f===-1?n.length:f},a=(n,o)=>String(n.createdAt??"").localeCompare(String(o.createdAt??"")),y={created:a,priority:(n,o)=>u(PRIORITIES,n.priority)-u(PRIORITIES,o.priority)||a(n,o),due:(n,o)=>(dueDay(n.dueAt)??1/0)-(dueDay(o.dueAt)??1/0)||u(PRIORITIES,n.priority)-u(PRIORITIES,o.priority),status:(n,o)=>u(STATUSES,n.status)-u(STATUSES,o.status)||a(n,o)}[i]??a,d=n=>n.status==="completed"?1:0;return[...p].sort((n,o)=>d(n)-d(o)||y(n,o))}export function countTodos(r,e=new Date){const t={all:0,open:0,"not-started":0,"in-progress":0,completed:0,overdue:0};for(const i of r||[])t.all++,i.status!=="completed"&&t.open++,t[i.status]!==void 0&&t[i.status]++,isOverdue(i,e)&&t.overdue++;return t}
@@ -0,0 +1 @@
1
+ export function onceClosed(t,e=()=>{}){const n=t.close.bind(t);let o=!1;t.close=(...c)=>{const r=n(...c);return o||(o=!0,e(),setTimeout(()=>{try{t.destroy?.()}catch{}t.element?.remove()},400)),r}}export function openMenuAt(t){const e=t.getBoundingClientRect();t.dispatchEvent(new MouseEvent("contextmenu",{bubbles:!0,cancelable:!0,clientX:e.left,clientY:e.bottom,view:window}))}export function settingsStore(t,e){return{read(){try{return e(S.get(t))}catch{return e(null)}},write(n){try{S.set(t,n)}catch{}}}}export function viewLifetime(t){let e=[];const n=()=>{const c=e;e=[],c.forEach(r=>{try{r?.()}catch{}})},o=()=>{t.test(location.hash)||n()};return{add(c){e.push(c)},reset(){n(),window.addEventListener("hashchange",o),e.push(()=>window.removeEventListener("hashchange",o))}}}export function sidebarCount(t,e={}){try{window.dispatchEvent(new CustomEvent("dm:sidebar-count",{detail:{url:t,...e}}))}catch{}}
@@ -0,0 +1,138 @@
1
+ <!-- No heading: the banner above (admin/js/lib/plugin-chrome.js) is the title.
2
+ Everything that changes is bound (M.applyBindings) - see admin/js/views/analytics.js, which also
3
+ inserts the list-row templates: the router renders this file through _.render, eating any {{…}}. -->
4
+ <div class="an-tool">
5
+
6
+ <div class="an-bar">
7
+ <select class="form-input an-range" aria-label="Range" data-model="range.value">
8
+ <option value="today">Today</option>
9
+ <option value="7d">Last 7 days</option>
10
+ <option value="30d">Last 30 days</option>
11
+ <option value="90d">Last 90 days</option>
12
+ <option value="all">All time</option>
13
+ </select>
14
+ <span class="an-live" data-bind-class="liveClass.value" data-bind-title="liveTitle.value">
15
+ <span class="an-live-dot"></span><span data-bind-text="liveText.value"></span>
16
+ </span>
17
+ <span data-help-title="Active now"
18
+ data-help="Visitors with a page view in the last five minutes, counted by browser session. It refreshes itself every 30 seconds while this screen is open (switch that off in settings)."></span>
19
+ <div class="an-bar-end">
20
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="refresh" title="Refresh">
21
+ <span data-icon="refresh-cw" data-icon-size="14"></span>
22
+ </button>
23
+ <button type="button" class="btn btn-sm btn-ghost an-more" aria-label="More actions" title="Export, reset">⋮</button>
24
+ </div>
25
+ </div>
26
+
27
+ <div class="an-kpis">
28
+ <div class="an-kpi"><span class="an-kpi-value" data-bind-text="kpi.value.hits">-</span>
29
+ <span class="an-kpi-label">Page views <span data-help="Every page view in the chosen range. Admin pages, the API, and visitors whose browser asks not to be tracked are never counted."></span></span></div>
30
+ <div class="an-kpi"><span class="an-kpi-value" data-bind-text="kpi.value.pages">-</span>
31
+ <span class="an-kpi-label">Pages viewed</span></div>
32
+ <div class="an-kpi"><span class="an-kpi-value" data-bind-text="kpi.value.sessions">-</span>
33
+ <span class="an-kpi-label">Sessions <span data-help="Browser sessions seen in the range (up to 30 days back). One visitor in one browser tab session counts once, however many pages they read."></span></span></div>
34
+ <div class="an-kpi"><span class="an-kpi-value" data-bind-text="kpi.value.bounce">-</span>
35
+ <span class="an-kpi-label">Bounce rate <span data-help="The share of sessions that viewed a single page and left."></span></span></div>
36
+ <div class="an-kpi"><span class="an-kpi-value" data-bind-text="kpi.value.lifetime">-</span>
37
+ <span class="an-kpi-label">All-time views</span></div>
38
+ </div>
39
+
40
+ <div class="an-chart-wrap">
41
+ <canvas class="an-chart" aria-label="Page views over time"></canvas>
42
+ <p class="an-note" data-if="isAllTime.value">The chart shows the last 30 days; the figures above are all time.</p>
43
+ </div>
44
+
45
+ <div class="an-tabs" role="tablist">
46
+ <button type="button" role="tab" data-on-click="showPages" data-bind-class="section.value === 'pages' && 'is-active'">Top pages</button>
47
+ <button type="button" role="tab" data-on-click="showJourneys" data-bind-class="section.value === 'journeys' && 'is-active'">Journeys</button>
48
+ <button type="button" role="tab" data-on-click="showSpikes" data-bind-class="section.value === 'spikes' && 'is-active'">
49
+ Spikes <span class="an-count" data-if="spikes.value.length" data-bind-text="spikes.value.length"></span></button>
50
+ <span data-help-title="Right-click a page"
51
+ data-help="Every page in these lists has a menu - right-click it, or use its ⋮ - to open it on the site, copy its address, or narrow the list to it."></span>
52
+ <label class="an-filter" data-if="section.value === 'pages'">
53
+ <span data-icon="search" data-icon-size="13"></span>
54
+ <input type="search" class="form-input" placeholder="Filter pages…" aria-label="Filter pages" data-model="filter.value">
55
+ </label>
56
+ </div>
57
+
58
+ <section class="an-panel" data-if="section.value === 'pages'">
59
+ <div class="an-list" data-each="pageRows.value key=key"></div>
60
+ <p class="an-note" data-bind-text="pagesNote.value"></p>
61
+ </section>
62
+
63
+ <section class="an-panel an-journeys" data-if="section.value === 'journeys'">
64
+ <div><h3>Entry pages <span data-help="Where sessions started."></span></h3>
65
+ <div class="an-list" data-each="entryRows.value key=key"></div></div>
66
+ <div><h3>Exit pages <span data-help="The last page a session viewed."></span></h3>
67
+ <div class="an-list" data-each="exitRows.value key=key"></div></div>
68
+ <div class="an-wide"><h3>Common paths <span data-help="The most frequent step from one page straight to another."></span></h3>
69
+ <div class="an-list" data-each="pathRows.value key=key"></div></div>
70
+ <p class="an-note" data-if="!sessionsCount.value">No sessions recorded in this range yet.</p>
71
+ </section>
72
+
73
+ <section class="an-panel" data-if="section.value === 'spikes'">
74
+ <div class="an-list" data-each="spikes.value key=key"></div>
75
+ <p class="an-note" data-if="!spikes.value.length">Nothing unusual today. A spike is a page running well above its last seven days at this hour.</p>
76
+ </section>
77
+ </div>
78
+
79
+ <style>
80
+ .an-tool { display: flex; flex-direction: column; gap: .9rem; }
81
+ .an-bar { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
82
+ .an-range { width: auto; height: 2rem; padding-top: 0; padding-bottom: 0; }
83
+ .an-live { display: inline-flex; align-items: center; gap: .4rem; font-size: var(--dm-text-xs); color: var(--dm-text-muted);
84
+ padding: .15rem .6rem; border-radius: 999px; background: var(--dm-surface-hover, rgba(127,127,127,.12)); }
85
+ .an-live-dot { width: .5rem; height: .5rem; border-radius: 50%; background: var(--dm-text-muted); }
86
+ .an-live.is-on .an-live-dot { background: var(--dm-success, #16a34a); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-success, #16a34a) 25%, transparent); }
87
+ .an-bar-end { display: flex; gap: .3rem; margin-left: auto; }
88
+ .an-more { font-size: var(--dm-text-lg); line-height: 1; padding: .2rem .5rem; }
89
+
90
+ .an-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: .6rem; }
91
+ .an-kpi { display: flex; flex-direction: column; gap: .15rem; padding: .6rem .8rem;
92
+ border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); }
93
+ .an-kpi-value { font-size: var(--dm-text-xl); font-weight: 600; font-variant-numeric: tabular-nums; }
94
+ .an-kpi-label { font-size: var(--dm-text-xs); color: var(--dm-text-muted); display: inline-flex; align-items: center; }
95
+
96
+ .an-chart-wrap { position: relative; height: 220px; padding-bottom: 1.2rem; }
97
+ .an-chart-wrap .an-note { position: absolute; bottom: 0; left: 0; margin: 0; }
98
+
99
+ .an-tabs { display: flex; align-items: center; gap: .25rem; border-bottom: 1px solid var(--dm-border); flex-wrap: wrap; }
100
+ .an-tabs [role="tab"] { border: 0; background: none; cursor: pointer; font: inherit; font-size: var(--dm-text-sm);
101
+ padding: .45rem .7rem; color: var(--dm-text-muted); border-bottom: 2px solid transparent; margin-bottom: -1px; }
102
+ .an-tabs [role="tab"].is-active { color: var(--dm-text); border-bottom-color: var(--dm-primary); }
103
+ .an-count { font-size: var(--dm-text-xs); padding: 0 .4rem; border-radius: 999px; background: var(--dm-warning, #d97706); color: #fff; }
104
+ .an-filter { position: relative; margin: 0 0 .3rem auto; }
105
+ .an-filter > svg, .an-filter > [data-icon] { position: absolute; left: .55rem; top: 50%; transform: translateY(-50%); color: var(--dm-text-muted); pointer-events: none; }
106
+ .an-filter .form-input { height: 1.85rem; padding-left: 1.75rem; width: 14rem; }
107
+
108
+ .an-panel { display: flex; flex-direction: column; gap: .5rem; }
109
+ .an-journeys { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
110
+ .an-journeys .an-wide, .an-journeys .an-note { grid-column: 1 / -1; }
111
+ .an-journeys h3 { margin: 0 0 .35rem; font-size: var(--dm-text-xs); font-weight: 600; color: var(--dm-text-muted);
112
+ text-transform: uppercase; letter-spacing: .04em; display: flex; align-items: center; }
113
+ @media (max-width: 760px) { .an-journeys { grid-template-columns: 1fr; } }
114
+
115
+ .an-list { display: flex; flex-direction: column; }
116
+ .an-row { position: relative; display: flex; align-items: center; gap: .6rem; padding: .3rem .4rem .3rem .6rem;
117
+ font-size: var(--dm-text-sm); border-radius: var(--dm-radius-sm); isolation: isolate; }
118
+ .an-row:hover { background: var(--dm-surface-hover, rgba(127,127,127,.08)); }
119
+ .an-bar-fill { position: absolute; inset: 2px auto 2px 0; z-index: -1; border-radius: var(--dm-radius-sm);
120
+ background: color-mix(in srgb, var(--dm-primary) 14%, transparent); }
121
+ .an-rank { flex: 0 0 1.6rem; color: var(--dm-text-muted); font-size: var(--dm-text-xs); text-align: right; }
122
+ .an-url { flex: 1; min-width: 0; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-xs);
123
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
124
+ .an-num { flex: 0 0 auto; font-variant-numeric: tabular-nums; }
125
+ .an-share { flex: 0 0 3.2rem; text-align: right; font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
126
+ .an-kebab { flex: 0 0 auto; border: 0; background: none; cursor: pointer; color: var(--dm-text-muted);
127
+ font-size: var(--dm-text-lg); line-height: 1; padding: 0 .3rem; border-radius: var(--dm-radius-sm); }
128
+ .an-kebab:hover { color: var(--dm-primary); }
129
+ .an-note { margin: .25rem 0; font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
130
+ .an-note:empty { display: none; }
131
+
132
+ .an-panel-form { display: flex; flex-direction: column; gap: 1rem; padding: .25rem 0 .5rem; }
133
+ .an-panel-form .form-label { display: flex; align-items: center; font-weight: 500; margin-bottom: .3rem; }
134
+ .an-setting-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
135
+ .an-panel-meta { font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
136
+ .an-panel-actions { display: flex; align-items: center; gap: .5rem; padding-top: .75rem; border-top: 1px solid var(--dm-border); }
137
+ .an-panel-actions .spacer { flex: 1; }
138
+ </style>
@@ -0,0 +1,156 @@
1
+ <!-- No heading: the banner above (admin/js/lib/plugin-chrome.js) is the title.
2
+ Everything that changes is bound (M.applyBindings) - see admin/js/views/contacts.js, which also
3
+ inserts the row and chip templates: the router renders this file through _.render, eating any {{…}}.
4
+ The editor, settings and groups panels are slideovers built in the view. -->
5
+ <div class="ct-tool">
6
+
7
+ <div class="ct-bar">
8
+ <label class="ct-search">
9
+ <span data-icon="search" data-icon-size="14"></span>
10
+ <input type="search" class="form-input" placeholder="Search contacts…" aria-label="Search contacts"
11
+ data-model="query.value" data-focus="searchFocus.value">
12
+ </label>
13
+ <span class="ct-count" data-bind-text="countText.value"></span>
14
+ <span data-help-title="Working with contacts"
15
+ data-help="Search matches names, emails, phones, companies, notes and groups. Right-click a contact (or its ⋮) to edit, email, call, copy, star, file it in a group, duplicate or delete it. Click a row to edit. Press / to search, N for a new contact."></span>
16
+ <span class="ct-spacer"></span>
17
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="create">
18
+ <span data-icon="plus" data-icon-size="14"></span> New contact
19
+ </button>
20
+ <button type="button" class="btn btn-sm btn-ghost ct-more" aria-label="More actions" title="Export, import, groups">⋮</button>
21
+ </div>
22
+
23
+ <!-- Everyone / starred / each group, with counts: the filter and the stats in one row. -->
24
+ <div class="ct-chips-row">
25
+ <div class="ct-chips" data-each="chips.value key=key"></div>
26
+ <span data-help-title="Filter by group"
27
+ data-help="The numbers are how many contacts are in each. Click one to show only those; click All to show everyone. ⋮ > Manage groups renames or deletes a group."></span>
28
+ </div>
29
+
30
+ <div class="ct-bulk" data-if="hasSelection.value">
31
+ <strong data-bind-text="selectionText.value"></strong>
32
+ <button type="button" class="btn btn-sm btn-ghost ct-bulk-group">
33
+ <span data-icon="folder-plus" data-icon-size="13"></span> Add to group ▾
34
+ </button>
35
+ <button type="button" class="btn btn-sm btn-ghost text-danger" data-on-click="bulkDelete">
36
+ <span data-icon="trash" data-icon-size="13"></span> Delete
37
+ </button>
38
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="clearSelection">Clear</button>
39
+ </div>
40
+
41
+ <p class="ct-state" data-if="loading.value">Loading contacts…</p>
42
+ <p class="ct-state" data-if="isEmpty.value" data-bind-text="emptyText.value"></p>
43
+
44
+ <div class="ct-list">
45
+ <div class="ct-head" data-if="hasRows.value">
46
+ <input type="checkbox" class="ct-select-all" aria-label="Select every contact shown" title="Select all shown"
47
+ data-bind-checked="allSelected.value">
48
+ <span></span>
49
+ <span class="ct-h-name">Name</span>
50
+ <span class="ct-col ct-company">Company</span>
51
+ <span class="ct-col ct-email">Email</span>
52
+ <span class="ct-col ct-phone">Phone</span>
53
+ <span class="ct-col ct-groups">Groups
54
+ <span data-help="Groups this contact is filed in. Change them from the contact's menu (Groups) or in the editor."></span></span>
55
+ <span></span>
56
+ </div>
57
+ <div class="ct-rows" data-each="rows.value key=id"></div>
58
+ </div>
59
+
60
+ <input type="file" class="ct-import-file" accept=".json,application/json" hidden>
61
+ </div>
62
+
63
+ <style>
64
+ .ct-tool { display: flex; flex-direction: column; gap: .75rem; }
65
+ .ct-bar { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
66
+ .ct-search { position: relative; flex: 1 1 200px; max-width: 320px; margin: 0; }
67
+ .ct-search > svg, .ct-search > [data-icon] { position: absolute; left: .6rem; top: 50%; transform: translateY(-50%);
68
+ color: var(--dm-text-muted); pointer-events: none; }
69
+ .ct-search .form-input { height: 2rem; padding-left: 1.9rem; }
70
+ .ct-count { font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
71
+ .ct-spacer { flex: 1; }
72
+ .ct-more { font-size: var(--dm-text-lg); line-height: 1; padding: .2rem .5rem; }
73
+
74
+ .ct-chips-row { display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
75
+ .ct-chips { display: flex; gap: .35rem; flex-wrap: wrap; }
76
+ .ct-chip { display: inline-flex; align-items: center; gap: .35rem; padding: .15rem .65rem; border-radius: 999px; cursor: pointer;
77
+ border: 1px solid var(--dm-border); background: none; color: var(--dm-text-muted); font: inherit; font-size: var(--dm-text-xs); }
78
+ .ct-chip:hover { border-color: var(--dm-primary); color: var(--dm-text); }
79
+ .ct-chip.is-active { background: var(--dm-primary); border-color: var(--dm-primary); color: var(--dm-primary-text, #fff); }
80
+ .ct-chip b { font-weight: 600; font-variant-numeric: tabular-nums; }
81
+
82
+ .ct-bulk { display: flex; align-items: center; gap: .5rem; padding: .4rem .75rem; font-size: var(--dm-text-sm);
83
+ border-radius: var(--dm-radius-md); background: color-mix(in srgb, var(--dm-primary) 10%, transparent); }
84
+ .ct-state { margin: 1.5rem 0; color: var(--dm-text-muted); text-align: center; }
85
+
86
+ .ct-list { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
87
+ .ct-list:has(.ct-rows:empty) { border: 0; }
88
+ .ct-head, .ct-row {
89
+ display: grid; align-items: center; gap: .75rem; padding: .5rem .75rem;
90
+ /* Set by the view from the column settings. */
91
+ grid-template-columns: var(--ct-cols, 1.1rem 1.5rem minmax(10rem, 1.4fr) minmax(0, 1fr) minmax(0, 1.2fr) minmax(0, .9fr) minmax(0, 1fr) 1.75rem);
92
+ }
93
+ .ct-head { font-size: var(--dm-text-xs); text-transform: uppercase; letter-spacing: .04em; color: var(--dm-text-muted);
94
+ border-bottom: 1px solid var(--dm-border); padding-top: .4rem; padding-bottom: .4rem; }
95
+ .ct-head > span { display: inline-flex; align-items: center; min-width: 0; }
96
+ .ct-row { border-bottom: 1px solid var(--dm-border); font-size: var(--dm-text-sm); cursor: pointer; }
97
+ .ct-row:last-child { border-bottom: 0; }
98
+ .ct-row:hover, .ct-row:focus-visible { background: var(--dm-surface-hover, rgba(127,127,127,.08)); outline: none; }
99
+ .ct-row.is-selected { background: color-mix(in srgb, var(--dm-primary) 8%, transparent); }
100
+ .ct-fav { border: 0; background: none; cursor: pointer; padding: 0; font-size: var(--dm-text-base); line-height: 1; color: var(--dm-text-muted); opacity: .35; }
101
+ .ct-fav:hover { opacity: .8; }
102
+ .ct-fav.is-on { color: var(--dm-warning, #d97706); opacity: 1; }
103
+ .ct-name { display: flex; align-items: center; gap: .6rem; min-width: 0; }
104
+ .ct-avatar { flex: 0 0 auto; width: 1.9rem; height: 1.9rem; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
105
+ font-size: var(--dm-text-xs); font-weight: 600; background: color-mix(in srgb, var(--dm-primary) 18%, transparent); color: var(--dm-primary); }
106
+ .ct-name-text { display: flex; flex-direction: column; min-width: 0; }
107
+ .ct-name-text strong, .ct-name-text small, .ct-col { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
108
+ .ct-name-text small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
109
+ .ct-name-text small:empty { display: none; }
110
+ .ct-col { min-width: 0; color: var(--dm-text-muted); }
111
+ .ct-row .ct-email, .ct-row .ct-phone { color: var(--dm-text); }
112
+ .ct-groups-text { font-size: var(--dm-text-xs); }
113
+ .ct-kebab { border: 0; background: none; cursor: pointer; color: var(--dm-text-muted); font-size: var(--dm-text-base); line-height: 1;
114
+ padding: 0 .3rem; border-radius: var(--dm-radius-sm); }
115
+ .ct-kebab:hover { color: var(--dm-primary); background: var(--dm-surface-hover, rgba(127,127,127,.15)); }
116
+
117
+ /* Density and hidden columns, from settings - classes on the list, so rows never re-render for them. */
118
+ .ct-list.is-compact .ct-row { padding-top: .25rem; padding-bottom: .25rem; }
119
+ .ct-list.is-compact .ct-avatar { width: 1.4rem; height: 1.4rem; font-size: var(--dm-text-xs); }
120
+ .ct-list.is-compact .ct-name-text small { display: none; }
121
+ .ct-list.hide-company .ct-company, .ct-list.hide-email .ct-email,
122
+ .ct-list.hide-phone .ct-phone, .ct-list.hide-groups .ct-groups { display: none; }
123
+ @media (max-width: 820px) {
124
+ .ct-head { display: none; }
125
+ .ct-row { grid-template-columns: 1.1rem 1.5rem 1fr 1.75rem; }
126
+ .ct-row .ct-col { display: none; }
127
+ }
128
+
129
+ /* Slideover panels: editor, settings, groups. */
130
+ .ct-panel { display: flex; flex-direction: column; gap: 1rem; padding: .25rem 0 .5rem; }
131
+ .ct-panel .form-label { display: flex; align-items: center; font-weight: 500; margin-bottom: .3rem; }
132
+ .ct-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: .75rem 1rem; }
133
+ .ct-grid .ct-wide { grid-column: 1 / -1; }
134
+ .ct-section { border-top: 1px solid var(--dm-border); padding-top: .75rem; }
135
+ .ct-section > summary { cursor: pointer; display: flex; align-items: center; gap: .5rem; font-weight: 600; list-style: none; }
136
+ .ct-section > summary::-webkit-details-marker { display: none; }
137
+ .ct-section > summary::before { content: '▸'; color: var(--dm-text-muted); transition: transform .15s ease; }
138
+ .ct-section[open] > summary::before { transform: rotate(90deg); }
139
+ .ct-section-sum { font-weight: 400; font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
140
+ .ct-section-body { display: flex; flex-direction: column; gap: 1rem; padding-top: .75rem; }
141
+ .ct-subhead { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: .4rem; }
142
+ .ct-subhead strong { display: inline-flex; align-items: center; }
143
+ .ct-pick { display: flex; flex-wrap: wrap; gap: .35rem; }
144
+ .ct-pick .ct-chip { font-size: var(--dm-text-xs); }
145
+ .ct-setting-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
146
+ .ct-panel-meta { font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
147
+ .ct-panel-actions { display: flex; align-items: center; gap: .5rem; padding-top: .75rem; border-top: 1px solid var(--dm-border); }
148
+ .ct-panel-actions .spacer { flex: 1; }
149
+ .ct-group-row { display: flex; align-items: center; gap: .5rem; padding: .4rem .5rem; border-bottom: 1px solid var(--dm-border); font-size: var(--dm-text-sm); }
150
+ .ct-group-row:last-child { border-bottom: 0; }
151
+ .ct-group-row span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
152
+ .ct-group-row small { color: var(--dm-text-muted); }
153
+ .ct-groups-list { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); }
154
+ .ct-add-group { display: flex; gap: .5rem; }
155
+ .ct-add-group .form-input { flex: 1; }
156
+ </style>