domma-cms 0.75.0 → 0.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CLAUDE.md +1 -1
  2. package/admin/js/api.js +1 -1
  3. package/admin/js/app.js +4 -4
  4. package/admin/js/lib/action-editor-arrange.js +1 -0
  5. package/admin/js/lib/api-endpoint-editor-arrange.js +2 -0
  6. package/admin/js/lib/api-tokens-arrange.js +3 -0
  7. package/admin/js/lib/block-editor-arrange.js +2 -0
  8. package/admin/js/lib/collection-editor-arrange.js +3 -0
  9. package/admin/js/lib/collection-entries-arrange.js +1 -0
  10. package/admin/js/lib/component-editor-arrange.js +27 -0
  11. package/admin/js/lib/context-menu-editor-arrange.js +1 -0
  12. package/admin/js/lib/dashboard-arrange.js +1 -0
  13. package/admin/js/lib/effects-arrange.js +3 -0
  14. package/admin/js/lib/form-editor-arrange.js +1 -0
  15. package/admin/js/lib/form-submissions-arrange.js +2 -0
  16. package/admin/js/lib/layouts-arrange.js +1 -0
  17. package/admin/js/lib/menu-editor-arrange.js +2 -0
  18. package/admin/js/lib/my-profile-arrange.js +1 -0
  19. package/admin/js/lib/notifications-arrange.js +1 -0
  20. package/admin/js/lib/page-editor-arrange.js +1 -0
  21. package/admin/js/lib/plugin-code-arrange.js +1 -0
  22. package/admin/js/lib/plugins-arrange.js +2 -0
  23. package/admin/js/lib/project-page-arrange.js +1 -0
  24. package/admin/js/lib/role-editor-arrange.js +3 -0
  25. package/admin/js/lib/roles-arrange.js +1 -0
  26. package/admin/js/lib/search-settings-arrange.js +1 -0
  27. package/admin/js/lib/sidebar-renderer.js +2 -2
  28. package/admin/js/lib/site-settings-arrange.js +1 -0
  29. package/admin/js/lib/user-editor-arrange.js +1 -0
  30. package/admin/js/lib/users-arrange.js +1 -0
  31. package/admin/js/lib/view-editor-arrange.js +14 -0
  32. package/admin/js/templates/action-editor.html +223 -147
  33. package/admin/js/templates/api-endpoint-editor.html +172 -117
  34. package/admin/js/templates/api-tokens.html +83 -10
  35. package/admin/js/templates/block-editor.html +194 -203
  36. package/admin/js/templates/collection-editor.html +191 -113
  37. package/admin/js/templates/collection-entries.html +99 -36
  38. package/admin/js/templates/component-editor.html +128 -126
  39. package/admin/js/templates/context-menu-editor.html +208 -148
  40. package/admin/js/templates/dashboard.html +184 -22
  41. package/admin/js/templates/effects.html +155 -72
  42. package/admin/js/templates/form-editor.html +41 -26
  43. package/admin/js/templates/form-submissions.html +78 -26
  44. package/admin/js/templates/layouts.html +126 -43
  45. package/admin/js/templates/menu-editor.html +57 -17
  46. package/admin/js/templates/menu-locations.html +54 -11
  47. package/admin/js/templates/my-profile.html +100 -12
  48. package/admin/js/templates/notifications.html +70 -10
  49. package/admin/js/templates/page-editor.html +47 -26
  50. package/admin/js/templates/plugin-code.html +91 -73
  51. package/admin/js/templates/plugins.html +100 -28
  52. package/admin/js/templates/project-detail.html +83 -145
  53. package/admin/js/templates/project-settings.html +88 -53
  54. package/admin/js/templates/role-editor.html +158 -83
  55. package/admin/js/templates/roles.html +78 -7
  56. package/admin/js/templates/search.html +193 -216
  57. package/admin/js/templates/settings.html +404 -472
  58. package/admin/js/templates/user-editor.html +150 -14
  59. package/admin/js/templates/users.html +69 -9
  60. package/admin/js/templates/view-editor.html +203 -180
  61. package/admin/js/templates/view-preview.html +45 -45
  62. package/admin/js/views/action-editor.js +7 -1
  63. package/admin/js/views/api-endpoint-editor.js +2 -3
  64. package/admin/js/views/api-tokens.js +62 -8
  65. package/admin/js/views/block-editor.js +14 -8
  66. package/admin/js/views/collection-editor.js +64 -5
  67. package/admin/js/views/collection-entries.js +50 -1
  68. package/admin/js/views/component-editor.js +2 -28
  69. package/admin/js/views/context-menu-editor.js +54 -55
  70. package/admin/js/views/dashboard.js +7 -1
  71. package/admin/js/views/effects.js +31 -1
  72. package/admin/js/views/form-editor.js +7 -7
  73. package/admin/js/views/form-submissions.js +11 -1
  74. package/admin/js/views/index.js +1 -1
  75. package/admin/js/views/layouts.js +81 -1
  76. package/admin/js/views/login.js +2 -2
  77. package/admin/js/views/menu-editor.js +21 -43
  78. package/admin/js/views/menu-locations.js +16 -1
  79. package/admin/js/views/my-profile.js +1 -1
  80. package/admin/js/views/notifications.js +18 -1
  81. package/admin/js/views/page-editor.js +26 -26
  82. package/admin/js/views/plugin-code.js +23 -4
  83. package/admin/js/views/plugins.js +97 -39
  84. package/admin/js/views/project-detail.js +9 -1
  85. package/admin/js/views/project-settings.js +1 -1
  86. package/admin/js/views/role-editor.js +11 -3
  87. package/admin/js/views/roles.js +49 -4
  88. package/admin/js/views/search.js +7 -1
  89. package/admin/js/views/settings.js +13 -3
  90. package/admin/js/views/user-editor.js +1 -1
  91. package/admin/js/views/users.js +13 -4
  92. package/admin/js/views/view-editor.js +1 -1
  93. package/admin/js/views/view-preview.js +1 -1
  94. package/package.json +1 -1
  95. package/plugins/_lib/admin/mail/compose-window.js +97 -40
  96. package/plugins/_lib/admin/mail/contacts.js +4 -1
  97. package/plugins/_lib/admin/mail/diagnostics-section.js +4 -1
  98. package/plugins/_lib/admin/mail/folder-tree.js +1 -1
  99. package/plugins/_lib/admin/mail/image-senders-section.js +69 -90
  100. package/plugins/_lib/admin/mail/mail.css +1 -1
  101. package/plugins/_lib/admin/mail/mail.html +1 -1
  102. package/plugins/_lib/admin/mail/reader-view.js +303 -44
  103. package/plugins/_lib/admin/mail/when-closed.js +26 -0
  104. package/plugins/_lib/mail/readRoutes.js +43 -1
  105. package/plugins/_lib/mail/unreadBadge.js +42 -0
  106. package/plugins/free-tier.lock.json +5 -5
  107. package/plugins/mail-reader/CLAUDE.md +1 -0
  108. package/plugins/mail-reader/plugin.js +3 -1
  109. package/plugins/mail-reader/plugin.json +5 -4
  110. package/server/routes/api/api-tokens.js +56 -0
  111. package/server/routes/api/auth.js +7 -1
  112. package/server/routes/api/collections.js +13 -1
  113. package/server/routes/api/dashboard.js +32 -4
  114. package/server/routes/api/effects.js +68 -13
  115. package/server/routes/api/forms.js +104 -13
  116. package/server/routes/api/layouts.js +93 -2
  117. package/server/routes/api/notifications.js +140 -14
  118. package/server/routes/api/plugins.js +135 -4
  119. package/server/routes/api/roles.js +111 -0
  120. package/server/routes/api/search.js +38 -4
  121. package/server/routes/api/settings.js +52 -0
  122. package/server/routes/api/users.js +65 -3
  123. package/server/routes/api/views.js +55 -1
  124. package/server/server.js +12 -0
  125. package/server/services/components.js +9 -0
  126. package/server/services/plugins.js +7 -0
  127. package/server/services/roles.js +3 -0
  128. package/server/services/search.js +58 -0
  129. package/server/services/users.js +16 -2
  130. package/server/services/views.js +14 -0
  131. package/admin/js/templates/dashboard/activity-feed.html +0 -3
  132. package/admin/js/templates/dashboard/cache.html +0 -32
  133. package/admin/js/templates/dashboard/health-detail.html +0 -2
  134. package/admin/js/templates/dashboard/journeys.html +0 -17
  135. package/admin/js/templates/dashboard/kpi-strip.html +0 -34
  136. package/admin/js/templates/dashboard/spike-feed.html +0 -3
  137. package/admin/js/templates/dashboard/top-pages.html +0 -3
  138. package/admin/js/templates/dashboard/traffic-chart.html +0 -3
  139. package/admin/js/templates/plugin-marketplace.html +0 -29
  140. package/admin/js/views/block-editor-enhance.js +0 -1
  141. package/admin/js/views/dashboard/lib/escape.js +0 -1
  142. package/admin/js/views/dashboard/widgets/activity-feed.js +0 -1
  143. package/admin/js/views/dashboard/widgets/cache.js +0 -1
  144. package/admin/js/views/dashboard/widgets/health-detail.js +0 -1
  145. package/admin/js/views/dashboard/widgets/journeys.js +0 -1
  146. package/admin/js/views/dashboard/widgets/kpi-strip.js +0 -1
  147. package/admin/js/views/dashboard/widgets/spike-feed.js +0 -6
  148. package/admin/js/views/dashboard/widgets/top-pages.js +0 -1
  149. package/admin/js/views/dashboard/widgets/traffic-chart.js +0 -1
  150. package/admin/js/views/plugin-marketplace.js +0 -1
@@ -1,55 +1,54 @@
1
- import{api as v}from"../api.js";import{makeIconInput as B}from"/admin/js/lib/shortcode-modal.js";import{makePageInput as L}from"/admin/js/lib/page-picker.js";const C=[{label:"This page",actions:{navigate:"Go to a URL",reload:"Reload the page",back:"Back",forward:"Forward",print:"Print","copy-link":"Copy link to this view","site-search":"Open site search"}},{label:"Clipboard and selection",actions:{copy:"Copy the selection",cut:"Cut",paste:"Paste","select-all":"Select all"}},{label:"Collection displays",actions:{filter:"Filter the display",search:"Search this display",sort:"Sort the display",export:"Export the display","mark-as-spam":"Mark as spam"}},{label:"This site",actions:{"open-modal":"Open a block in a modal","run-action":"Run an Action"}}],q=Object.assign({},...C.map(c=>c.actions)),I={back:"greys out when there is no history behind the page",forward:"greys out unless you have gone back",copy:"greys out with nothing selected",cut:"greys out outside an editable field, or with nothing selected",paste:"greys out outside an editable field","site-search":"greys out on a site where search is unavailable",search:"greys out unless the click lands in a collection display",filter:"greys out unless the click lands in a collection display",sort:"greys out unless the click lands in a collection display",export:"greys out unless the click lands in a collection display"},M={"mark-as-spam":"needs a right-click on an entry inside a collection display"},O={navigate:{key:"url",label:"URL",placeholder:"/shop"},"run-action":{key:"slug",label:"Action slug",placeholder:"archive-entry"},"open-modal":{key:"block",label:"Block name",placeholder:"contact-card"},export:{key:"format",label:"Format",placeholder:"csv"},sort:{key:"field",label:"Field",placeholder:"price"},filter:{key:"field",label:"Field",placeholder:"category"},"mark-as-spam":{key:"field",label:"Flag field (optional)",placeholder:"spam"}};function d(c){return String(c??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function R(c){return String(c||"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,64)}let g=null;function b(){if(g){try{g.destroy()}catch{}g=null}const c=document.getElementById("ctx-preview");if(c&&E.contextMenu&&E.contextMenu.registry)for(const m of E.contextMenu.registry(c))try{m.destroy()}catch{}}function j(c){return String(c||"").split(",").map(m=>m.trim()).filter(Boolean)}export const contextMenuEditorView={templateUrl:"/admin/js/templates/context-menu-editor.html",async onMount(c){E.tabs&&E.tabs(c.find("#ctx-tabs").get(0)),b();const m=location.hash.match(/^#\/context-menus\/edit\/([^?#]+)/),y=m?decodeURIComponent(m[1]):null,x=!y;let a=[];if(!x)try{const t=await v.contextMenus.get(y);document.getElementById("ctx-title").textContent=`Edit: ${t.name||t.slug}`,$("#ctx-name").val(t.name||""),$("#ctx-slug").val(t.slug).prop("disabled",!0),$("#ctx-selector").val(t.target?.selector||""),$("#ctx-match").val(t.target?.match||""),$("#ctx-exclude").val(t.target?.exclude||""),$("#ctx-inherit").val(String(t.inherit===!1?"false":t.inherit||"append")),$("#ctx-enabled").prop("checked",t.enabled!==!1),$("#ctx-accent").val(t.theme?.accent||""),$("#ctx-transition").val(t.theme?.transition||""),$("#ctx-density").val(t.theme?.density||""),$("#ctx-radius").val(t.theme?.radius||""),$("#ctx-shadow").val(t.theme?.shadow||""),$("#ctx-opacity").val(t.theme?.opacity??""),$("#ctx-pages").val((t.binding?.pages||[]).join(", ")),$("#ctx-projects").val((t.binding?.projects||[]).join(", ")),a=Array.isArray(t.items)?t.items:[]}catch(t){E.toast(`Could not load "${y}": ${t.message||t}`,{type:"error"});return}let f=!x;$("#ctx-slug").on("input",()=>{f=!0}),$("#ctx-name").on("input",function(){f||$("#ctx-slug").val(R(this.value))});function u(){const t=document.getElementById("ctx-items"),s=document.getElementById("ctx-items-empty");s.style.display=a.length?"none":"",t.replaceChildren(),a.forEach((e,i)=>{const n=e.type||"item",o=O[e.action?.type],r=document.createElement("div");r.className="card mb-2",r.innerHTML=`
2
- <div class="card-body">
3
- <div class="row">
4
- <div class="col-4">
5
- <label class="form-label">Type</label>
6
- <select class="form-select ctx-item-type" data-i="${i}">
7
- <option value="item"${n==="item"?" selected":""}>Item</option>
8
- <option value="divider"${n==="divider"?" selected":""}>Divider</option>
9
- <option value="header"${n==="header"?" selected":""}>Heading</option>
10
- </select>
11
- </div>
12
- ${n==="divider"?"":`
13
- <div class="col-8">
14
- <label class="form-label">Text</label>
15
- <input class="form-input ctx-item-text" data-i="${i}" type="text" value="${d(e.text)}">
16
- </div>`}
17
- </div>
18
- ${n==="item"?`
19
- <div class="row mt-2">
20
- <div class="col-4">
21
- <label class="form-label">Icon</label>
22
- <div class="ctx-icon-slot" data-i="${i}"></div>
23
- </div>
24
- <div class="col-4">
25
- <label class="form-label">Does</label>
26
- <select class="form-select ctx-item-action" data-i="${i}">
27
- ${C.map(l=>`
28
- <optgroup label="${d(l.label)}">
29
- ${Object.entries(l.actions).map(([S,T])=>`<option value="${S}"${e.action?.type===S?" selected":""}>${d(T)}</option>`).join("")}
30
- </optgroup>`).join("")}
31
- </select>
32
- ${I[e.action?.type]?`<small class="text-muted">${d(I[e.action.type])}</small>`:""}
33
- ${M[e.action?.type]?`<small class="text-muted">${d(M[e.action.type])}</small>`:""}
34
- </div>
35
- ${o?`
36
- <div class="col-4">
37
- <label class="form-label">${d(o.label)}</label>
38
- ${o.key==="url"?`<div class="ctx-url-slot" data-i="${i}"></div>`:`<input class="form-input ctx-item-arg" data-i="${i}" data-key="${o.key}"
39
- type="text" value="${d(e.action?.[o.key])}" placeholder="${d(o.placeholder)}">`}
40
- </div>`:""}
41
- </div>
42
- <div class="row mt-2">
43
- <div class="col-6">
44
- <label class="form-check">
45
- <input type="checkbox" class="ctx-item-danger" data-i="${i}"${e.danger?" checked":""}>
46
- Destructive (red)
47
- </label>
48
- </div>
49
- </div>`:""}
50
- <div class="mt-2 text-right">
51
- <button class="btn btn-sm btn-ghost ctx-item-up" data-i="${i}" ${i===0?"disabled":""}>Up</button>
52
- <button class="btn btn-sm btn-ghost ctx-item-down" data-i="${i}" ${i===a.length-1?"disabled":""}>Down</button>
53
- <button class="btn btn-sm btn-ghost ctx-item-remove" data-i="${i}">Remove</button>
54
- </div>
55
- </div>`;const w=r.querySelector(".ctx-icon-slot");if(w){const l=B("edit",e.icon||"");l.input.className+=" ctx-item-icon",l.input.dataset.i=String(i),w.appendChild(l.el)}const N=r.querySelector(".ctx-url-slot");if(N){const l=L(o.placeholder,e.action?.url||"");l.input.className+=" ctx-item-arg",l.input.dataset.i=String(i),l.input.dataset.key="url",N.appendChild(l.el)}t.appendChild(r)}),Domma.icons.scan("#ctx-items"),p()}c.on("change",".ctx-item-type",function(){const t=Number(this.dataset.i);a[t].type=this.value,this.value!=="item"?delete a[t].action:a[t].action||(a[t].action={type:"navigate"}),u()}),c.on("input",".ctx-item-text",function(){a[Number(this.dataset.i)].text=this.value,p()}),c.on("input",".ctx-item-icon",function(){a[Number(this.dataset.i)].icon=this.value,p()}),c.on("change",".ctx-item-action",function(){const t=Number(this.dataset.i);a[t].action={type:this.value},u()}),c.on("input",".ctx-item-arg",function(){const t=Number(this.dataset.i);a[t].action=a[t].action||{},a[t].action[this.dataset.key]=this.value,p()}),c.on("change",".ctx-item-danger",function(){a[Number(this.dataset.i)].danger=this.checked,p()}),c.on("click",".ctx-item-remove",function(){a.splice(Number(this.dataset.i),1),u()}),c.on("click",".ctx-item-up",function(){const t=Number(this.dataset.i);[a[t-1],a[t]]=[a[t],a[t-1]],u()}),c.on("click",".ctx-item-down",function(){const t=Number(this.dataset.i);[a[t+1],a[t]]=[a[t],a[t+1]],u()}),$("#ctx-add-item").on("click",()=>{a.push({type:"item",text:"New item",action:{type:"navigate",url:""}}),u()});function k(){const t={},s=$("#ctx-accent").val(),e=$("#ctx-transition").val(),i=$("#ctx-density").val(),n=$("#ctx-radius").val(),o=$("#ctx-shadow").val(),r=$("#ctx-opacity").val();return s&&(t.accent=s),e&&(t.transition=e),i&&(t.density=i),n&&(t.radius=n),o&&(t.shadow=o),r!==""&&r!=null&&(t.opacity=Number(r)),t}function A(){const t=$("#ctx-inherit").val(),s=j($("#ctx-pages").val()),e=j($("#ctx-projects").val()),i={slug:$("#ctx-slug").val().trim(),name:$("#ctx-name").val().trim(),enabled:$("#ctx-enabled").is(":checked"),target:{selector:$("#ctx-selector").val().trim(),match:$("#ctx-match").val().trim(),exclude:$("#ctx-exclude").val().trim()},inherit:t==="false"?!1:t,items:a},n=k();return Object.keys(n).length&&(i.theme=n),(s.length||e.length)&&(i.binding={},s.length&&(i.binding.pages=s),e.length&&(i.binding.projects=e)),i}E.tooltip&&E.tooltip(".ctx-help",{trigger:"click",position:"right"});function p(){b();const t=a.map(e=>{const i=e.type||"item";return i==="divider"?{type:"divider"}:i==="header"?{type:"header",header:e.text||""}:{label:e.text||"(no text)",icon:e.icon||void 0,danger:!!e.danger}});if(!t.some(e=>e.label))return;const s=k();g=E.contextMenu("#ctx-preview",{cursor:"glyph",items:t,inherit:!1,accent:s.accent,transition:s.transition,density:s.density,radius:s.radius,shadow:s.shadow,opacity:s.opacity})}c.on("change","#ctx-accent, #ctx-transition, #ctx-density, #ctx-radius, #ctx-shadow",p),c.on("input","#ctx-opacity",p);function h(t){const s=document.getElementById("ctx-errors");if(!t){s.style.display="none";return}s.style.display="",s.innerHTML=String(t).split("; ").map(e=>`<div>${d(e)}</div>`).join(""),s.scrollIntoView({behavior:"smooth",block:"nearest"})}$("#ctx-save").on("click",async function(){h(null);const t=A();if(!t.slug)return h("Slug is required");if(!t.name)return h("Name is required");if(!t.target.selector)return h("Container selector is required");this.disabled=!0;try{x?(await v.contextMenus.create(t),E.toast("Context menu created.",{type:"success"}),location.hash="#/context-menus"):(await v.contextMenus.update(y,t),E.toast("Context menu saved.",{type:"success"}))}catch(s){h(s.message||String(s))}finally{this.disabled=!1}}),u(),Domma.icons.scan(c.get(0))},onUnmount(){E.contextMenu&&E.contextMenu.closeAll&&E.contextMenu.closeAll(),b()}};
1
+ import{api as ue}from"../api.js";import{addBannerAction as qe}from"../lib/plugin-chrome.js";import{helpPopovers as Ne}from"../lib/help-popover.js";import{onceClosed as it,openMenuAt as Re}from"../lib/tool-kit.js";import{makeIconInput as rt}from"/admin/js/lib/shortcode-modal.js";import{makePageInput as ct}from"/admin/js/lib/page-picker.js";import{ACCENTS as lt,ACTION_ARG as B,ACTION_GROUPS as dt,EDITABLE_TYPES as ut,NEEDS_TARGET as mt,SELF_DISABLING as pt,checkMenu as vt,duplicateItem as ft,formFromMenu as Fe,insertItem as ht,itemForm as bt,itemFromForm as He,itemRow as yt,moveItem as gt,newItem as xt,previewItems as kt,previewable as Pe,recordFromForm as wt,removeItem as Q,slugify as Et,snapshot as me,splitList as X,themeFromForm as Mt}from"../lib/context-menu-editor-arrange.js";const St=/^\/context-menus\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,F=c=>`context-menu-editor.draft.${c||"new"}`,At=`
2
+ <div class="cmx-row" data-index="{{index}}" tabindex="0" data-bind-class="cls">
3
+ <span class="cmx-icon" aria-hidden="true" data-name="{{icon}}"></span>
4
+ <span class="cmx-type">{{typeLabel}}</span>
5
+ <span class="cmx-main"><strong>{{text}}</strong><small data-bind-hidden="!does">{{does}}</small><small data-bind-hidden="!note">{{note}}</small><small class="cmx-bad" data-bind-hidden="!problem">{{problem}}</small></span>
6
+ <span class="cmx-chip is-danger" data-bind-hidden="!danger">Destructive</span>
7
+ <span class="cmx-move">
8
+ <button type="button" class="cmx-up" data-bind-disabled="first" aria-label="Move up" title="Move up (Alt+\u2191)">\u2191</button>
9
+ <button type="button" class="cmx-down" data-bind-disabled="last" aria-label="Move down" title="Move down (Alt+\u2193)">\u2193</button>
10
+ </span>
11
+ <button type="button" class="cmx-kebab" aria-label="Actions for {{text}}" title="Actions">\u22EE</button>
12
+ </div>`,It='<span class="cmx-chip" data-bind-class="cls">{{text}}</span>',Lt="<small>{{text}}</small>";function C(c){return String(c??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function Tt(c){const u=document.createElement("template");return u.innerHTML=c.trim(),u.content.firstElementChild}let Z=null;function pe(){if(Z){try{Z.destroy()}catch{}Z=null}const c=document.getElementById("ctx-preview");if(c&&E.contextMenu&&E.contextMenu.registry)for(const u of E.contextMenu.registry(c))try{u.destroy()}catch{}}function ve(c,u){const l=c.querySelector(".dm-plugin-banner-name");if(!l)return;const T=l.querySelector(".dm-plugin-banner-sep");if(T){for(;T.nextSibling;)T.nextSibling.remove();l.appendChild(document.createTextNode(u))}}let k=null,Ke=!1;const ee=c=>String(c||"").split("?")[0].replace(/\/$/,"");function Ct(){Ke||!window.R?.use||(Ke=!0,R.use((c,u,l)=>!(St.test(u?.path||"")&&ee(c?.path)!==ee(u?.path))||!k?.dirty()?l():E.confirm("This context menu has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(w=>{w?l():history.replaceState(null,"",`#${u.path}`)})),M.subscribe("router:afterChange",({to:c}={})=>{if(k&&ee(c?.path)!==k.path){const u=k;k=null,u.dispose()}}))}export const contextMenuEditorView={templateUrl:"/admin/js/templates/context-menu-editor.html",async onMount(c){if(Ct(),k){const e=k;k=null,e.dispose()}pe();const u=[],l=e=>(u.push(e),e);let T=!1;const w=c.get(0),m=w.querySelector(".cmx")||w,_=()=>!T&&m.isConnected,fe=location.hash.match(/^#\/context-menus\/edit\/([^?#]+)/),p=fe?decodeURIComponent(fe[1]):null,h=!p,Be=ee(location.hash.replace(/^#/,""));let g=null;if(!h){const e=E.loader(w,{type:"dots"});try{g=await ue.contextMenus.get(p)}catch(n){e.destroy(),E.toast(`Could not load "${p}": ${n.message||n}`,{type:"error"});return}if(e.destroy(),!m.isConnected)return}const O=Fe(g),r={};for(const[e,n]of Object.entries(O))r[e]=M.observable(n);const U=()=>Object.fromEntries(Object.keys(r).map(e=>[e,r[e].peek()])),he=()=>Object.fromEntries(Object.keys(r).map(e=>[e,r[e].value])),s=M.observable(Array.isArray(g?.items)?g.items:[]),z=M.observable(me(O,s.peek())),G=M.computed(()=>me(he(),s.value)!==z.value),V=M.observable(!1),J=M.observable(!1),H=M.observable([]),$=M.computed(()=>vt(he(),s.value,{isNew:h})),_e=M.computed(()=>{const e=$.value.errors;if(J.value)return e;const n={};for(const t of Object.keys(e))t!=="items"&&String(r[t]?.value??"").trim()&&(n[t]=e[t]);return n}),be=M.computed(()=>{const e=J.value?$.value.errors:{};return{binding:!!(e.name||e.slug||e.selector),items:!!e.items||$.value.itemErrors.some(Boolean),appearance:!!(e.opacity||e.accent)}});let te=!h;l(M.effect(()=>{const e=r.name.value;te||(r.slug.value=Et(e))}));const ne=M.observable([]);l(M.effect(()=>{const e=s.value,n=$.value.itemErrors;ne.value=e.map((t,i)=>({...yt(t,i,e.length,n[i]||""),key:`${i}:${JSON.stringify(t)}`}))}));const x=m.querySelector("#ctx-items");l(M.effect(()=>{ne.value,requestAnimationFrame(()=>{x?.isConnected&&(x.querySelectorAll(".cmx-icon[data-name]").forEach(e=>{const n=e.dataset.name;if(!n||e.dataset.drawn===n)return;e.dataset.drawn=n;const t=document.createElement("span");t.setAttribute("data-icon",n),t.setAttribute("data-icon-size","14"),e.replaceChildren(t)}),I.scan(x))})}));const Ue=M.computed(()=>{const e=[];e.push(r.enabled.value?{text:"On",cls:"is-ok"}:{text:"Off",cls:"is-warn"});const n=X(r.pages.value).length,t=X(r.projects.value).length;e.push({text:n||t?`${n+t} scope rule${n+t===1?"":"s"}`:"Site-wide",cls:""});const i=s.value.filter(o=>!["divider","header"].includes(o?.type||"item")).length;return e.push({text:`${i} item${i===1?"":"s"} to click`,cls:i?"":"is-danger"}),r.selector.value.trim()&&e.push({text:r.selector.value.trim(),cls:""}),e});let ae=null;function ze(){if(!_())return;pe();const e=s.peek();if(!Pe(e))return;const n=Mt(U());Z=E.contextMenu("#ctx-preview",{cursor:"glyph",items:kt(e),inherit:!1,accent:n.accent,transition:n.transition,density:n.density,radius:n.radius,shadow:n.shadow,opacity:n.opacity})}l(M.effect(()=>{s.value,r.accent.value,r.transition.value,r.density.value,r.radius.value,r.shadow.value,r.opacity.value,clearTimeout(ae),ae=setTimeout(ze,120)})),l(()=>{clearTimeout(ae),pe()});const Ge=M.computed(()=>Pe(s.value)?"Right-click here to try the menu":"Add an item to try the menu here"),oe=e=>ye(e),A=e=>{s.value=e};function P(e){requestAnimationFrame(()=>x?.querySelector(`.cmx-row[data-index="${e}"]`)?.focus())}function j(e,n){n<0||n>=s.peek().length||(A(gt(s.peek(),e,n)),P(n))}function q(e,n=s.peek().length){const t=xt(e);A(ht(s.peek(),n,t)),e!=="divider"&&ye(n,{isAdd:!0})}function Ve(e){const n=s.peek(),t=n[e];if(!t)return[];const i=t.type||"item";return[{header:i==="divider"?"Divider":t.text||"(no text)"},{label:i==="divider"?"Change\u2026":"Edit\u2026",icon:"edit",action:()=>oe(e)},{type:"divider"},{label:"Move up",icon:"arrow-up",disabled:e===0,action:()=>j(e,e-1)},{label:"Move down",icon:"arrow-down",disabled:e===n.length-1,action:()=>j(e,e+1)},{label:"Duplicate",icon:"copy",action:()=>{A(ft(s.peek(),e)),P(e+1)}},{label:"Add below",icon:"plus",submenu:[{label:"Item",action:()=>q("item",e+1)},{label:"Heading",action:()=>q("header",e+1)},{label:"Divider",action:()=>q("divider",e+1)}]},{type:"divider"},{label:"Remove",icon:"trash",danger:!0,action:()=>{A(Q(s.peek(),e)),P(Math.max(0,e-1))}}]}let v=null;function ye(e,{isAdd:n=!1}={}){const t=s.peek()[e];if(!t)return;if(v)try{v.so.close(!0)}catch{}const i=bt(t),o={};for(const[a,d]of Object.entries(i))o[a]=M.observable(d);const W=()=>Object.fromEntries(Object.keys(o).map(a=>[a,o[a].peek()])),Ze=JSON.stringify(i),ie=()=>JSON.stringify(W())!==Ze,Ie=!ut.includes(i.type),re=Array.isArray(t.submenu)&&t.submenu.length,Le={...o,locked:Ie,notLocked:!Ie,hasSub:re,subText:re?`Opens a submenu of ${t.submenu.length} - kept as it is (edit it in the file).`:"",isItem:M.computed(()=>o.type.value!=="divider"&&o.type.value!=="header"),hasText:M.computed(()=>o.type.value!=="divider"),showAction:M.computed(()=>o.type.value!=="divider"&&o.type.value!=="header"&&!re),argLabel:M.computed(()=>B[o.action.value]?.label||""),note:M.computed(()=>pt[o.action.value]||mt[o.action.value]||""),problem:M.computed(()=>{const a=He(Object.fromEntries(Object.keys(o).map(d=>[d,o[d].value])),t);return a.type==="divider"?"":String(a.text||"").trim()?B[a.action?.type]?.required&&!String(a.action?.[B[a.action.type].key]||"").trim()?`Needs a ${B[a.action.type].label.toLowerCase()}.`:"":"Needs text."})},y=E.slideover({title:n?"New item":i.type==="divider"?"Divider":`Edit ${C(i.text||"item")}`,size:"sm",position:"right"}),et=dt.map(a=>`<optgroup label="${C(a.label)}">${Object.entries(a.actions).map(([d,Y])=>`<option value="${C(d)}">${C(Y)}</option>`).join("")}</optgroup>`).join(""),L=Tt(`
13
+ <div class="cmx-form">
14
+ <div data-bind-hidden="locked">
15
+ <label class="form-label" for="cmx-i-type">Type
16
+ <span data-help="An Item does something when clicked, a Heading labels a group, a Divider draws a line."></span></label>
17
+ <select id="cmx-i-type" class="form-select" data-model="type.value">
18
+ <option value="item">Item</option><option value="header">Heading</option><option value="divider">Divider</option>
19
+ </select>
20
+ </div>
21
+ <p class="cmx-note" data-bind-hidden="notLocked">A ${C(i.type)} item - its text, icon and action can be changed here; its type is kept.</p>
22
+ <div data-bind-hidden="!hasText.value">
23
+ <label class="form-label" for="cmx-i-text">Text</label>
24
+ <input id="cmx-i-text" type="text" class="form-input" maxlength="80" data-model="text.value">
25
+ </div>
26
+ <div data-bind-hidden="!isItem.value">
27
+ <label class="form-label" for="cmx-i-icon">Icon <small class="cmx-hint-inline">(optional)</small></label>
28
+ <div class="cmx-icon-mount"></div>
29
+ </div>
30
+ <p class="cmx-note" data-bind-hidden="!hasSub">${C(Le.subText)}</p>
31
+ <div data-bind-hidden="!showAction.value">
32
+ <label class="form-label" for="cmx-i-action">Does
33
+ <span data-help-title="What it does" data-help="A fixed set of actions, on purpose: nothing typed here runs as script on your pages. Some grey themselves out when they do not apply to the click - the note under the choice says when."></span></label>
34
+ <select id="cmx-i-action" class="form-select" data-model="action.value">${et}</select>
35
+ <small class="cmx-hint" data-bind-hidden="!note.value" data-bind-text="note.value"></small>
36
+ </div>
37
+ <div data-bind-hidden="!argLabel.value || !showAction.value">
38
+ <label class="form-label" for="cmx-i-arg" data-bind-text="argLabel.value">Argument</label>
39
+ <div class="cmx-arg-mount"></div>
40
+ </div>
41
+ <div class="cmx-setting" data-bind-hidden="!isItem.value">
42
+ <label class="form-label" for="cmx-i-danger">Destructive (red)
43
+ <span data-help="Drawn in red, for things like Delete - a warning, not a guard."></span></label>
44
+ <input id="cmx-i-danger" type="checkbox" class="form-switch" data-model="danger.value">
45
+ </div>
46
+ <small class="cmx-err" data-bind-hidden="!problem.value" data-bind-text="problem.value"></small>
47
+ <div class="cmx-actions">
48
+ <button type="button" class="btn btn-sm btn-ghost text-danger" data-on-click="remove"><span data-icon="trash" data-icon-size="13"></span> Remove</button>
49
+ <span class="cmx-spacer"></span>
50
+ <small class="cmx-hint-inline">Ctrl+Enter</small>
51
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
52
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="done"><span data-icon="check" data-icon-size="13"></span> Done</button>
53
+ </div>
54
+ </div>`),N=rt("edit",i.icon);N.input.id="cmx-i-icon",N.input.classList.add("form-input");const Te=()=>{o.icon.value=N.input.value};N.input.addEventListener("input",Te),L.querySelector(".cmx-icon-mount").appendChild(N.el);const ce=L.querySelector(".cmx-arg-mount");let f=null;const le=()=>{o.arg.value=f.value};function Ce(){f?.removeEventListener("input",le),ce.replaceChildren();const a=B[o.action.peek()];if(!a){f=null;return}if(a.key==="url"){const d=ct(a.placeholder,o.arg.peek());f=d.input,ce.appendChild(d.el)}else f=document.createElement("input"),f.type="text",f.className="form-input",f.placeholder=a.placeholder,f.value=o.arg.peek(),ce.appendChild(f);f.id="cmx-i-arg",f.addEventListener("input",le)}let Oe=o.action.peek();const tt=M.effect(()=>{const a=o.action.value;a!==Oe&&(Oe=a,o.arg.value="",Ce())});Ce();const $e=()=>{const a=He(W(),t),d=[...s.peek()],Y=d.indexOf(t);Y!==-1&&(d[Y]=a,A(d))};let K=!1,de=!1;const De={done:()=>{$e(),K=!0,y.close(),P(e)},cancel:()=>y.close(),remove:()=>{const a=s.peek().indexOf(t);a!==-1&&A(Q(s.peek(),a)),K=!0,y.close()}};y.element.appendChild(L);const nt=Ne(L),at=M.applyBindings(Le,L,{methods:De}),ot=a=>{(a.ctrlKey||a.metaKey)&&a.key==="Enter"&&(a.preventDefault(),De.done())};L.addEventListener("keydown",ot);const je={so:y,dirty:ie,apply:$e};v=je,it(y,()=>{tt(),at.dispose(),nt(),N.input.removeEventListener("input",Te),f?.removeEventListener("input",le),v===je&&(v=null)});const st=y.close.bind(y);y.close=async a=>{if(!K&&a!==!0&&ie()){if(de)return;de=!0;const d=await E.confirm("Discard the changes to this item?");if(de=!1,!d)return}if(!K&&n&&a!==!0&&!ie()){const d=s.peek().indexOf(t);d!==-1&&A(Q(s.peek(),d))}return K=!0,st()},I.scan(L),y.open(),requestAnimationFrame(()=>L.querySelector(i.type==="divider"?"#cmx-i-type":"#cmx-i-text")?.focus())}l(()=>{if(v){const e=v;e.dirty()&&e.apply();try{e.so.close(!0)}catch{}}});async function se(){if(V.peek())return;J.value=!0,H.value=[];const e=$.peek();if(!e.ok){const t=Object.keys(e.errors).length;E.toast(`Not saved yet - ${t===1?"one thing needs":`${t} things need`} fixing (marked in red).`,{type:"warning"});const i=be.peek(),o=i.binding?0:i.items?1:i.appearance?2:-1;o>=0&&Ye?.activate?.(o);return}V.value=!0;const n=wt(U(),s.peek());try{if(h)await ue.contextMenus.create(n);else{const t=await ue.contextMenus.update(p,n);t?.items&&(s.value=t.items)}if(!_())return;z.value=me(U(),s.peek());try{S.remove?S.remove(F(p)):S.set(F(p),null)}catch{}window.dispatchEvent(new CustomEvent("dm:sidebar-count",{detail:{url:"#/context-menus"}})),h?(E.toast("Context menu created.",{type:"success"}),location.hash="#/context-menus"):(E.toast("Context menu saved.",{type:"success"}),ve(w,r.name.peek()||p))}catch(t){H.value=String(t.message||t).split("; ").map((i,o)=>({key:String(o),text:i})),m.querySelector(".cmx-alert")?.scrollIntoView({behavior:"smooth",block:"nearest"})}finally{V.value=!1}}function Je(){E.confirm("Throw away your unsaved changes to this menu?").then(e=>{if(!e||!_())return;const n=Fe(g);for(const[t,i]of Object.entries(n))r[t].value=i;s.value=Array.isArray(g?.items)?g.items:[],H.value=[]})}m.querySelector("#ctx-items").innerHTML=At,m.querySelector(".cmx-chips").innerHTML=It,m.querySelector(".cmx-server-errors").innerHTML=Lt;const ge=m.querySelector("#ctx-accent");if(O.accent&&!lt.includes(O.accent)&&ge){const e=document.createElement("option");e.value=O.accent,e.textContent=`Custom (${O.accent})`,ge.appendChild(e)}const We={...r,items:s,rows:ne,chips:Ue,dirty:G,saving:V,err:_e,tabBad:be,serverErrors:H,isNew:h,notNew:!h,previewText:Ge,serverError:M.computed(()=>H.value.length>0),saveText:M.computed(()=>h?"Create menu":"Save"),problemText:M.computed(()=>J.value&&!$.value.ok?" - some need fixing before they can be saved":""),scopeText:M.computed(()=>{const e=X(r.pages.value),n=X(r.projects.value);return!e.length&&!n.length?"Site-wide: every page carries this menu.":`Only ${[e.length&&`pages matching ${e.join(", ")}`,n.length&&`pages in ${n.join(", ")}`].filter(Boolean).join(", or ")}.`})},Ye=E.tabs(m.querySelector("#ctx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Qe=M.applyBindings(We,m,{methods:{save:()=>se(),discard:()=>Je(),addItem:()=>q("item"),addHeader:()=>q("header"),addDivider:()=>q("divider")}});l(()=>Qe.dispose()),l(Ne(m,{position:"bottom"}));const xe=m.querySelector("#ctx-slug"),ke=()=>{te=!0};xe?.addEventListener("input",ke),l(()=>xe?.removeEventListener("input",ke));const Xe=E.contextMenu(x,{cursor:"glyph",match:".cmx-row",inherit:!1,density:"compact",items:e=>Ve(Number(e.dataset.index))});l(()=>Xe?.destroy?.());const we=e=>{const n=e.target.closest(".cmx-row");if(!n)return;const t=Number(n.dataset.index);if(e.target.closest(".cmx-kebab")){Re(e.target.closest(".cmx-kebab"));return}if(e.target.closest(".cmx-up")){j(t,t-1);return}if(e.target.closest(".cmx-down")){j(t,t+1);return}oe(t)},Ee=e=>{const n=e.target.closest(".cmx-row");if(!n||e.target!==n)return;const t=Number(n.dataset.index);e.key==="F10"&&e.shiftKey?(e.preventDefault(),Re(n)):e.key==="Enter"?(e.preventDefault(),oe(t)):e.altKey&&e.key==="ArrowUp"?(e.preventDefault(),j(t,t-1)):e.altKey&&e.key==="ArrowDown"?(e.preventDefault(),j(t,t+1)):e.key==="Delete"&&(e.preventDefault(),A(Q(s.peek(),t)),P(Math.max(0,t-1)))};x.addEventListener("click",we),x.addEventListener("keydown",Ee),l(()=>{x.removeEventListener("click",we),x.removeEventListener("keydown",Ee)}),h?ve(w,"New menu"):ve(w,g?.name||p),qe(c,{id:"ctx-editor-back",icon:"arrow-left",title:"All context menus",onClick:()=>{location.hash="#/context-menus"}}),qe(c,{id:"ctx-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>se()});const Me=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||v||(e.preventDefault(),e.stopImmediatePropagation(),se())};document.addEventListener("keydown",Me,!0),l(()=>document.removeEventListener("keydown",Me,!0));const Se=()=>{if(G.peek())try{S.set(F(p),{form:U(),items:s.peek(),base:z.peek(),at:new Date().toISOString()})}catch{}},Ae=e=>{v?.dirty()&&v.apply(),G.peek()&&(Se(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Ae),l(()=>window.removeEventListener("beforeunload",Ae)),k={path:Be,dirty:()=>G.peek()||!!v?.dirty(),dispose(){if(T=!0,v){const e=v;e.dirty()&&e.apply();try{e.so.close(!0)}catch{}}Se(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(m);let b=null;try{b=S.get(F(p))}catch{}if(b?.form){try{S.remove?S.remove(F(p)):S.set(F(p),null)}catch{}const e=b.base&&b.base!==z.peek(),n=b.at?` (${D(b.at).format("D MMM HH:mm")})`:"",t=h?"the new context menu you started":`"${C(g?.name||p)}"`,i=e?`You left unsaved changes to ${t}${n}, but it has been saved since. Restore your version anyway?`:`You left unsaved changes to ${t}${n}. Restore them?`;if(await E.confirm(i)&&_()){for(const[o,W]of Object.entries(b.form))r[o]&&o!==(h?"":"slug")&&(r[o].value=W);h&&b.form.slug&&(te=!0),Array.isArray(b.items)&&(s.value=b.items)}}}};
@@ -1 +1,7 @@
1
- import{api as n}from"../api.js";import l from"./dashboard/widgets/kpi-strip.js";import c from"./dashboard/widgets/traffic-chart.js";import m from"./dashboard/widgets/spike-feed.js";import u from"./dashboard/widgets/top-pages.js";import y from"./dashboard/widgets/journeys.js";import b from"./dashboard/widgets/health-detail.js";import g from"./dashboard/widgets/activity-feed.js";import w,{refreshCacheWidget as k}from"./dashboard/widgets/cache.js";const o=[{slot:"#dash-kpi-strip",template:"/admin/js/templates/dashboard/kpi-strip.html",render:l},{slot:"#dash-traffic-chart",template:"/admin/js/templates/dashboard/traffic-chart.html",render:c},{slot:"#dash-spike-feed",template:"/admin/js/templates/dashboard/spike-feed.html",render:m},{slot:"#dash-top-pages",template:"/admin/js/templates/dashboard/top-pages.html",render:u},{slot:"#dash-journeys",template:"/admin/js/templates/dashboard/journeys.html",render:y},{slot:"#dash-health-detail",template:"/admin/js/templates/dashboard/health-detail.html",render:b},{slot:"#dash-activity-feed",template:"/admin/js/templates/dashboard/activity-feed.html",render:g},{slot:"#dash-cache",template:"/admin/js/templates/dashboard/cache.html",render:w}],j=30*1e3;async function v(t){const a=await fetch(t,{credentials:"same-origin"});if(!a.ok)throw new Error(`Failed to load ${t}: ${a.status}`);return a.text()}function S(t,a){t.length&&(t.get(0).innerHTML=a)}function C(t,a){const s=t.find("#dash-warnings");if(!a||a.length===0){s.attr("hidden","hidden");return}s.removeAttr("hidden"),s.get(0).textContent="Some sections could not load: "+a.join(" \xB7 ")}function h(t){t.find("#dash-updated").text("Updated "+D().format("HH:mm:ss"))}function f(t){if(!t||!Array.isArray(t.activity))return t;const a=D().subtract(7,"days").valueOf();return t._submissionsWeek=t.activity.filter(s=>s.type==="form"&&D(s.at).valueOf()>=a).length,t}let i=null;export const dashboardView={templateUrl:"/admin/js/templates/dashboard.html",async onMount(t){const a=E.loader(t.get(0),{type:"dots"}),s=await Promise.all(o.map(e=>v(e.template)));o.forEach((e,d)=>{S(t.find(e.slot),s[d])});let r;try{r=await n.dashboard.summary(),r=f(r)}catch(e){a.destroy();const d=t.find("#dash-warnings");d.removeAttr("hidden"),d.get(0).textContent="Could not load dashboard: "+e.message;return}for(const e of o){const d=t.find(e.slot);try{await e.render(d,r)}catch(p){console.warn("[dashboard]",e.slot,"render failed:",p)}}C(t,r.warnings),h(t),Domma.icons.scan(),a.destroy(),i&&clearInterval(i),i=setInterval(async()=>{if(!document.body.contains(t.get(0))){clearInterval(i),i=null;return}try{const e=await n.dashboard.summaryLite();e.traffic&&(r.traffic={...r.traffic,...e.traffic}),e.realtime&&(r.realtime=e.realtime),e.spikes&&(r.spikes=e.spikes),e.health&&(r.health={...r.health,status:e.health.status}),f(r),await l(t.find("#dash-kpi-strip"),r),await m(t.find("#dash-spike-feed"),r),k(t.find("#dash-cache")).catch(()=>{}),h(t)}catch(e){console.warn("[dashboard] lite refresh failed:",e)}},j)}};
1
+ import{api as T,apiRequest as m}from"../api.js";import{addBannerAction as ae}from"../lib/plugin-chrome.js";import{helpPopovers as we}from"../lib/help-popover.js";import{openMenuAt as se,viewLifetime as xe}from"../lib/tool-kit.js";import{BADGE_SOURCES as ke,activityRows as Ce,attentionFrom as Me,canAny as j,chartSeries as $e,isSitePath as L,kpisFrom as Ee}from"../lib/dashboard-arrange.js";const oe=/^(#\/?)?(\?.*)?$/,Se=3e4,Pe="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js",Ae=`
2
+ <div class="dbx-row" data-id="{{id}}" data-href="{{href}}" data-url="{{url}}" tabindex="0" data-bind-class="rowCls">
3
+ <span class="dbx-main"><strong>{{text}}</strong><small data-bind-hidden="!meta">{{meta}}</small></span>
4
+ <span class="dbx-value" data-bind-hidden="!value">{{value}}</span>
5
+ <span class="dbx-chip" data-bind-hidden="!chip" data-bind-class="chipCls">{{chip}}</span>
6
+ <button type="button" class="dbx-kebab" aria-label="Actions" title="Actions" data-bind-hidden="!menu">\u22EE</button>
7
+ </div>`;let b=null;function Re(){return window.Chart?Promise.resolve(!0):(b||(b=new Promise(i=>{const n=document.createElement("script");n.src=Pe,n.async=!0,n.onload=()=>i(!0),n.onerror=()=>{b=null,i(!1)},document.head.appendChild(n)})),b)}const c=i=>Number(i||0).toLocaleString("en-GB"),Te=i=>`${Number(i)>=0?"+":""}${Number(i).toFixed(0)}%`,v=(i,n,d,l={})=>({id:`${i}${n}`,text:d,meta:"",href:"",url:d,value:"",chip:"",chipCls:"",rowCls:"",menu:!0,...l}),u=xe(oe);export const dashboardView={templateUrl:"/admin/js/templates/dashboard.html",async onMount(i){u.reset();const n=i.get(0).querySelector(".dbx")||i.get(0),d=()=>oe.test(location.hash)&&n.isConnected,l=M.observable(null),g=M.observable({}),y=M.observable(!1),w=M.observable(""),x=M.observable(null),k=M.observable(Date.now()),p=M.observable(null),C=M.observable(!!window.Chart);let f=[],_=new Set;const ne=M.computed(()=>Ee(l.value,g.value)),re=M.computed(()=>{const e=x.value;if(!e)return"";const a=Math.floor((k.value-e)/6e4);return a<1?"Updated just now":`Updated ${a} min ago`}),O=M.observable([]),B=M.observable([]),F=M.observable([]),N=M.observable([]),D=M.observable([]),U=M.observable([]),q=M.observable([]),G=M.observable([]);u.add(M.effect(()=>{const e=l.value,a=t=>t==="danger"?"is-danger":"is-warn";O.value=Me(g.value,e?.health?.checks||[]).map(t=>({id:t.id,text:`${t.title}: ${t.text}`,meta:t.meta,href:t.href,url:"",value:"",chip:t.tone==="danger"?"Urgent":"Check",chipCls:a(t.tone),rowCls:a(t.tone)+(t.href?"":" is-static"),menu:!!t.href})),B.value=Ce(e?.activity||[],k.value).map(t=>({id:t.id,text:t.text,meta:t.meta,href:t.href,url:t.url,value:"",chip:"",chipCls:"",rowCls:"",menu:!0})),F.value=(e?.topPages||[]).map((t,r)=>v("t",r,t.url,{value:c(t.hits),chip:t.deltaPct==null?"":Te(t.deltaPct),chipCls:Number(t.deltaPct)>=0?"is-ok":"is-danger"})),N.value=(e?.spikes||[]).map((t,r)=>v("s",r,t.url,{value:c(t.hits),chip:`\xD7${Number(t.ratio)||0}`,chipCls:"is-warn"}));const s=e?.journeys;D.value=(s?.entry||[]).map((t,r)=>v("e",r,t.url,{value:c(t.count)})),U.value=(s?.exit||[]).map((t,r)=>v("x",r,t.url,{value:c(t.count)})),q.value=(s?.paths||[]).map((t,r)=>({id:`p${r}`,text:`${t.from} \u2192 ${t.to}`,meta:"",href:"",url:"",value:c(t.count),chip:"",chipCls:"",rowCls:"is-static",menu:!1}));const o={ok:["OK","is-ok"],warn:["Look","is-warn"],fail:["Failing","is-danger"]};G.value=(e?.health?.checks||[]).map((t,r)=>({id:`c${r}`,text:String(t.label||t.id),meta:Array.isArray(t.detail)?t.detail.slice(0,5).join(", "):"",href:"",url:"",value:String(t.value??""),chip:(o[t.level]||o.warn)[0],chipCls:(o[t.level]||o.warn)[1],rowCls:"is-static",menu:!1}))}));const ie=M.computed(()=>!!l.value?.journeys?.totalSessions),le=M.computed(()=>{const e=l.value?.journeys;return e?.totalSessions?`Bounce rate ${(Number(e.bounceRate)*100).toFixed(1)}%`:""}),$=M.computed(()=>$e(l.value?.traffic?.series)),S=M.computed(()=>C.value&&!$.value.empty),ce=M.computed(()=>y.value?l.value?C.value?"No page views recorded yet.":"The chart could not load (Chart.js).":"Traffic needs an administrator.":"Loading\u2026"),de=M.computed(()=>!!p.value?.enabled),ue=M.computed(()=>{const e=p.value||{},a=(e.hits||0)+(e.misses||0),s=o=>{if(!o)return"never";const t=Math.max(0,Math.floor((Date.now()-new Date(o).getTime())/1e3));return t<60?`${t}s ago`:t<3600?`${Math.floor(t/60)} min ago`:t<86400?`${Math.floor(t/3600)} h ago`:`${Math.floor(t/86400)} days ago`};return{hitRate:e.hitRate==null?"-":`${(e.hitRate*100).toFixed(1)}%`,hitsMisses:a?`${c(e.hits)} hits \xB7 ${c(e.misses)} misses`:"no visits yet",size:e.maxItems?`${c(e.size)} / ${c(e.maxItems)}`:c(e.size),driver:e.driver?`driver: ${e.driver}`:"",writes:c(e.writes),invalidations:`${c(e.invalidations)} invalidations`,cleared:s(e.lastClearedAt)}});let P=0;async function H(){const e=ke.filter(s=>j(f,s.permission)),a=await Promise.all(e.map(s=>m(s.countUrl,{method:"GET"}).catch(()=>null)));d()&&(g.value=Object.fromEntries(e.map((s,o)=>[s.key,a[o]]).filter(([,s])=>s)))}async function K(){const e=++P;try{f=(await m("/auth/permissions",{method:"GET"}).catch(()=>({})))?.permissions||[];const[s,o,t]=await Promise.all([T.dashboard.summary().catch(r=>({__error:r})),m("/cache/status",{method:"GET"}).catch(()=>null),j(f,"pages")?T.pages.list().catch(()=>[]):Promise.resolve([]),H()]);if(e!==P||!d())return;_=new Set((Array.isArray(t)?t:[]).map(r=>r.urlPath).filter(Boolean)),s?.__error?(l.value=null,w.value=/403|forbidden/i.test(String(s.__error?.message))?"":`The dashboard could not load: ${s.__error.message}`):(l.value=s,w.value=s?.warnings?.length?`Some parts could not load: ${s.warnings.join(" \xB7 ")}`:""),p.value=o,x.value=Date.now()}finally{e===P&&(y.value=!0)}}let he=0;async function pe(){if(!(!d()||!l.peek())){k.value=Date.now();try{const e=await T.dashboard.summaryLite();if(!d())return;const a={...l.peek()};e.traffic&&(a.traffic={...a.traffic,...e.traffic}),e.realtime&&(a.realtime=e.realtime),e.spikes&&(a.spikes=e.spikes),e.health&&(a.health={...a.health,status:e.health.status}),l.value=a,x.value=Date.now()}catch{}++he%2===0&&H(),m("/cache/status",{method:"GET"}).then(e=>{d()&&(p.value=e)}).catch(()=>{})}}const me=setInterval(pe,Se);u.add(()=>clearInterval(me));let h=null;const A=n.querySelector(".dbx-canvas"),fe=()=>{const e=getComputedStyle(n),a=(s,o)=>e.getPropertyValue(s).trim()||o;return{line:a("--dm-primary","#5b8cff"),text:a("--dm-text-muted","#94a3b8"),grid:a("--dm-border","rgba(127,127,127,.2)")}},be=e=>/^#[0-9a-f]{6}$/i.test(e)?`${e}26`:"transparent";function V(){if(!S.peek()||!A?.isConnected||!A.offsetParent)return;const{labels:e,values:a}=$.peek(),{line:s,text:o,grid:t}=fe();if(h){h.data.labels=e,h.data.datasets[0].data=a,h.update("none");return}h=new window.Chart(A,{type:"line",data:{labels:e,datasets:[{data:a,borderColor:s,backgroundColor:be(s),fill:!0,tension:.3,pointRadius:0,borderWidth:2}]},options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{display:!1}},scales:{x:{ticks:{maxTicksLimit:8,color:o},grid:{color:t}},y:{beginAtZero:!0,ticks:{precision:0,color:o},grid:{color:t}}}}})}u.add(M.effect(()=>{$.value,S.value,requestAnimationFrame(V)})),u.add(()=>{try{h?.destroy()}catch{}h=null});const z=()=>[...document.body.classList].filter(e=>/^dm-(theme|custom)-/.test(e)).sort().join(" ");let J=z();const W=new MutationObserver(()=>{const e=z();if(e!==J){J=e;try{h?.destroy()}catch{}h=null,requestAnimationFrame(V)}});W.observe(document.body,{attributes:!0,attributeFilter:["class"]}),u.add(()=>W.disconnect());const R=e=>new URL(e,location.origin).href,Z=()=>j(f,"pages"),Q=e=>L(e)&&_.has(e.split(/[?#]/)[0].replace(/\/$/,"")||"/");async function X(e,a){try{await navigator.clipboard.writeText(e),E.toast(`${a} copied.`,{type:"success"})}catch{E.toast("Could not copy - select it and copy it by hand.",{type:"error"})}}function Y(e){const{href:a,url:s}=e.dataset;if(a){location.hash=a;return}if(s&&Z()&&Q(s)){location.hash=`#/pages/edit${s.split(/[?#]/)[0]}`;return}s&&L(s)&&window.open(R(s),"_blank","noopener")}function ve(e){const{href:a,url:s}=e.dataset,o=[{header:(e.querySelector(".dbx-main strong")?.textContent||"").slice(0,60)}];return a&&o.push({label:"Open",icon:"arrow-right",action:()=>{location.hash=a}}),s&&L(s)?(o.push({label:"Open on the site",icon:"external-link",action:()=>window.open(R(s),"_blank","noopener")}),o.push({label:"Copy address",icon:"copy",action:()=>X(R(s),"Address")}),Z()&&o.push({label:"Edit the page",icon:"edit",disabled:!Q(s),action:()=>{location.hash=`#/pages/edit${s.split(/[?#]/)[0]}`}})):s&&o.push({label:"Copy address",icon:"copy",action:()=>X(s,"Address")}),o.length>1?o:[]}for(const e of[".dbx-attention",".dbx-activity",".dbx-top-pages",".dbx-spikes",".dbx-entry",".dbx-exit",".dbx-paths",".dbx-checks"]){const a=n.querySelector(e);a&&(a.innerHTML=Ae)}const ge=M.applyBindings({summary:l,loaded:y,warning:w,asOf:re,kpi:ne,attention:O,activity:B,topPages:F,spikes:N,entry:D,exit:U,paths:q,checks:G,hasJourneys:ie,bounce:le,hasChart:S,chartNote:ce,cache:p,cacheOn:de,cacheView:ue},n,{methods:{async toggleCache(e){const a=!!e?.target?.checked;try{p.value=await m("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:a})}),E.toast(`The cache is ${a?"on":"off"}.`,{type:"success"})}catch(s){e?.target&&(e.target.checked=!a),E.toast(s.message||"Could not change the cache.",{type:"error"})}},async clearCache(){if(await E.confirm("Clear the whole response cache? Pages render afresh until it fills again."))try{p.value=await m("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}}}});u.add(()=>ge.dispose()),u.add(we(n,{position:"bottom"}));const ye=E.contextMenu(n,{cursor:"glyph",match:".dbx-row",inherit:!1,density:"compact",items:e=>ve(e)});u.add(()=>ye?.destroy?.());const ee=e=>{const a=e.target.closest(".dbx-row");if(!a||!n.contains(a))return;const s=e.target.closest(".dbx-kebab");if(s){se(s);return}Y(a)},te=e=>{const a=e.target.closest(".dbx-row");!a||!n.contains(a)||(e.key==="F10"&&e.shiftKey?(e.preventDefault(),se(a)):e.key==="Enter"&&e.target===a&&Y(a))};n.addEventListener("click",ee),n.addEventListener("keydown",te),u.add(()=>{n.removeEventListener("click",ee),n.removeEventListener("keydown",te)}),ae(i,{id:"dashboard-refresh",icon:"refresh-cw",title:"Refresh",onClick:()=>K()}),ae(i,{id:"dashboard-new-page",icon:"plus",title:"New page",onClick:()=>{location.hash="#/pages/new"}}),I.scan(n),Re().then(e=>{d()&&(C.value=e)}),await K(),d()&&I.scan(n)}};
@@ -1 +1,31 @@
1
- import{apiRequest as a}from"/admin/js/api.js";export const effectsView={templateUrl:"/admin/js/templates/effects.html",async onMount(t){let e={};const f=E.loader(t.get(0),{type:"dots"});try{e=await a("/effects/settings")}catch{E.toast("Could not load settings.",{type:"error"})}finally{f.destroy()}t.find("#field-respect-motion").prop("checked",e.respectMotion!==!1),t.find("#field-default-duration").val(e.defaultDuration??600),t.find("#field-default-animation").val(e.defaultAnimation||"fade"),t.find("#field-default-threshold").val(e.defaultThreshold??.1),t.find("#save-settings-btn").off("click").on("click",async()=>{const s={respectMotion:t.find("#field-respect-motion").prop("checked"),defaultDuration:parseInt(t.find("#field-default-duration").val(),10)||600,defaultAnimation:t.find("#field-default-animation").val(),defaultThreshold:parseFloat(t.find("#field-default-threshold").val())||.1};try{await a("/effects/settings",{method:"PUT",body:JSON.stringify(s)}),E.toast("Settings saved.",{type:"success"})}catch{E.toast("Failed to save settings.",{type:"error"})}}),E.tabs(t.find("#effects-tabs").get(0)),t.find(".effects-tab-btn").on("click",function(){const s=this.getAttribute("data-fx-tab");t.find(".effects-tab-btn").removeClass("active"),$(this).addClass("active"),t.find(".effects-tab-panel").hide(),t.find(`#tab-${s}`).show()}),t.find(".effects-tab-panel").hide(),t.find("#tab-entrance").show(),Domma.icons.scan()}};
1
+ import{apiRequest as ft}from"../api.js";import{addBannerAction as At}from"../lib/plugin-chrome.js";import{helpPopovers as pt}from"../lib/help-popover.js";import{onceClosed as jt,openMenuAt as mt}from"../lib/tool-kit.js";import{EFFECTS as D,getEffect as bt}from"../lib/effect-defs.js";import{arrangeEffects as yt,attrRow as $t,categoriesOf as Ft,changedFromDefaults as Rt,checkSettings as A,DEFAULT_SETTINGS as j,effectRow as ht,exampleShortcode as w,formFromSettings as p,settingsSnapshot as $}from"../lib/effects-arrange.js";const V=/^\/effects(\?.*)?$/,m="effects.draft",vt="effects.tab",It=`
2
+ <div class="fxx-row" data-name="{{name}}" tabindex="0">
3
+ <code class="fxx-tag">{{tag}}</code>
4
+ <span class="fxx-main"><strong>{{label}}</strong><small>{{description}}</small><small>{{meta}}</small></span>
5
+ <span class="fxx-chip is-info">{{category}}</span>
6
+ <span class="fxx-chip is-warn" data-bind-hidden="!fullPage" title="Runs full-page on the published site - the editor cannot preview it">Full page</span>
7
+ <button type="button" class="fxx-kebab" aria-label="Actions for {{label}}" title="Actions">\u22EE</button>
8
+ </div>`,Kt=`<button type="button" data-cat="{{key}}" data-bind-class="on && 'is-on'">{{label}} <b>{{count}}</b></button>`,Nt=s=>String(s??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function Bt(s){const r=document.createElement("template");return r.innerHTML=s.trim(),r.content.firstElementChild}async function k(s,r){try{await navigator.clipboard.writeText(s),E.toast(`${r} copied.`,{type:"success"})}catch{E.toast(`Could not reach the clipboard - select the ${r.toLowerCase()} and copy it.`,{type:"warning"})}}let f=null,gt=!1;function Pt(){gt||!window.R?.use||(gt=!0,R.use((s,r,o)=>!(V.test(r?.path||"")&&!V.test(s?.path||""))||!f?.dirty()?o():E.confirm("You have unsaved effect settings. Leave anyway? They are kept as a draft for your next visit.").then(i=>{i?o():history.replaceState(null,"","#/effects")})),M.subscribe("router:afterChange",({to:s}={})=>{if(f&&!V.test(s?.path||"")){const r=f;f=null,r.dispose()}}))}export const effectsView={templateUrl:"/admin/js/templates/effects.html",async onMount(s){if(Pt(),f){const t=f;f=null,t.dispose()}const r=[],o=t=>(r.push(t),t);let F=!1;const i=s.get(0).querySelector(".fxx")||s.get(0),C=()=>!F&&i.isConnected,b=M.observable({...j}),y=M.observable($(p(j))),xt=p(j),d=Object.fromEntries(Object.entries(xt).map(([t,e])=>[t,M.observable(e)])),K=()=>Object.fromEntries(Object.keys(d).map(t=>[t,d[t].peek()])),T=t=>{for(const[e,a]of Object.entries(t))d[e]&&(d[e].value=a)},N=M.observable(!1),J=M.observable(!1),L=M.computed(()=>A(Object.fromEntries(Object.keys(d).map(t=>[t,d[t].value])))),h=M.computed(()=>J.value&&$(Object.fromEntries(Object.keys(d).map(t=>[t,d[t].value])))!==y.value),B=t=>M.computed(()=>L.value.errors[t]||""),P=M.observable("all"),_=M.observable(""),Q=M.observable("entrance"),X=M.observable([]);o(M.effect(()=>{X.value=yt(D,{category:P.value,query:_.value}).map(ht)}));const Z=M.observable([]);o(M.effect(()=>{const t=P.value,e=_.value,a=n=>yt(D,{category:n,query:e}).length;Z.value=[{key:"all",label:"All",count:a("all"),on:t==="all"},...Ft(D).map(n=>({key:n,label:n,count:a(n),on:t===n}))]}));const tt={...d,saved:b,dirty:h,rows:X,cats:Z,query:_,guide:Q,effectCount:D.length,durErr:B("defaultDuration"),thrErr:B("defaultThreshold"),animErr:B("defaultAnimation"),cannotSave:M.computed(()=>N.value||!h.value||Object.keys(L.value.errors).length>0),motionText:M.computed(()=>b.value.respectMotion?"Respects reduced motion":"Ignores reduced motion"),defaultsText:M.computed(()=>{const t=Rt(b.value).length;return t?`${t} changed from Domma's defaults`:"Domma's defaults"})};async function Et(){try{const t=await ft("/effects/settings");if(!C())return;const e=p(t);b.value=A(e).payload,y.value=$(e),T(e)}catch(t){E.toast(`Could not load the effect settings: ${t.message||t}`,{type:"error"})}finally{J.value=!0}}async function H(){if(tt.cannotSave.peek()){Object.keys(L.peek().errors).length&&E.toast("Fix the highlighted settings first.",{type:"warning"});return}N.value=!0;const{payload:t}=L.peek();try{const a=(await ft("/effects/settings",{method:"PUT",body:JSON.stringify(t)}))?.settings||t;b.value=A(p(a)).payload,y.value=$(K());try{S.remove?S.remove(m):S.set(m,null)}catch{}E.toast("Effect settings saved - the site uses them on its next page load.",{type:"success"})}catch(e){E.toast(`Not saved: ${e.message||e}`,{type:"error"})}finally{N.value=!1}}async function St(){(!h.peek()||await E.confirm("Throw away the changes you have not saved?"))&&T(p(b.peek()))}function wt(){T(p(j)),E.toast("Domma's defaults are filled in - Save to use them.",{type:"info"})}const U=new Set;function W(t){const e=bt(t);if(!e)return;const a=ht(e),n=E.slideover({title:Nt(`${a.label} - ${a.tag}`),size:"md",position:"right"}),ct=(e.attrs||[]).map($t),l=Bt(`
9
+ <div class="fxx-form">
10
+ <p class="fxx-note" data-part="desc"></p>
11
+ <div class="fxx-chips" data-part="chips"></div>
12
+ <div>
13
+ <div class="fxx-sub">The shortcode
14
+ <span data-help="The bare tag runs with every setting at its default; the full form spells them all out, ready to edit."></span></div>
15
+ <pre data-part="bare"></pre>
16
+ </div>
17
+ <div data-part="full-wrap">
18
+ <div class="fxx-sub">With every setting</div>
19
+ <pre data-part="full"></pre>
20
+ </div>
21
+ <div data-part="attrs-wrap">
22
+ <div class="fxx-sub">Settings</div>
23
+ <table class="fxx-attrs"><thead><tr><th>Attribute</th><th>Takes</th><th>Default</th></tr></thead><tbody data-part="attrs"></tbody></table>
24
+ </div>
25
+ <div class="fxx-actions">
26
+ <span class="fxx-spacer"></span>
27
+ <button type="button" class="btn btn-sm btn-ghost" data-act="close">Close</button>
28
+ <button type="button" class="btn btn-sm btn-outline" data-act="copy-full">Copy full</button>
29
+ <button type="button" class="btn btn-sm btn-primary" data-act="copy"><span data-icon="copy" data-icon-size="13"></span> Copy shortcode</button>
30
+ </div>
31
+ </div>`);l.querySelector('[data-part="desc"]').textContent=e.description||"";const qt=l.querySelector('[data-part="chips"]'),O=(c,u="")=>{const x=document.createElement("span");x.className=`fxx-chip ${u}`,x.textContent=c,qt.appendChild(x)};O(e.category,"is-info"),O(e.selfClosing?"Self-closing":"Wraps content"),e.previewable===!1&&O("Full page","is-warn"),(e.hosts||[]).length&&O(`Also as an attribute of ${e.hosts.map(c=>`[${c}]`).join(" ")}`);const z=w(e),G=w(e,{full:!0});l.querySelector('[data-part="bare"]').textContent=z,l.querySelector('[data-part="full"]').textContent=G,l.querySelector('[data-part="full-wrap"]').hidden=G===z;const Ot=l.querySelector('[data-part="attrs"]');ct.length||(l.querySelector('[data-part="attrs-wrap"]').hidden=!0);for(const c of ct){const u=document.createElement("tr"),x=document.createElement("td"),lt=document.createElement("code");lt.textContent=c.name;const it=document.createElement("small");it.textContent=c.label+(c.hint?` - ${c.hint}`:""),x.append(lt,it);const dt=document.createElement("td");dt.textContent=c.takes;const ut=document.createElement("td");ut.textContent=c.def,u.append(x,dt,ut),Ot.appendChild(u)}l.addEventListener("click",c=>{const u=c.target.closest("[data-act]")?.dataset.act;u==="close"?n.close():u==="copy"?k(z,"Shortcode"):u==="copy-full"&&k(G,"Shortcode")}),n.element.appendChild(l);const Dt=pt(l);I.scan(l),U.add(n),jt(n,()=>{Dt(),U.delete(n)}),n.open()}function kt(t){const e=bt(t);if(!e)return[];const a=w(e,{full:!0});return[{header:`${e.label} [${e.name}]`},{label:"Details\u2026",icon:"eye",action:()=>W(t)},{type:"divider"},{label:"Copy shortcode",icon:"copy",action:()=>k(w(e),"Shortcode")},{label:"Copy with every setting",icon:"copy",disabled:a===w(e),action:()=>k(a,"Shortcode")},{label:"Copy tag name",icon:"code",action:()=>k(e.name,"Tag name")}]}const v=i.querySelector(".fxx-list");v.innerHTML=It;const Y=i.querySelector(".fxx-cats");Y.innerHTML=Kt;const q=i.querySelector(".fxx-tabs"),Ct=E.tabs(q,{tabSelector:".tab-item",panelSelector:".tab-panel"}),Mt=M.applyBindings(tt,i,{methods:{save:()=>H(),discard:()=>St(),resetDefaults:()=>wt(),showGuide:t=>{Q.value=t}}});o(()=>Mt.dispose()),o(pt(i,{position:"bottom"}));const et=t=>{const e=t.target.closest("[data-cat]");e&&(P.value=e.dataset.cat)};Y.addEventListener("click",et),o(()=>Y.removeEventListener("click",et));try{const t=Number(S.get(vt));Number.isInteger(t)&&t>0&&t<3&&Ct.activate?.(t)}catch{}const Tt=[...q.querySelectorAll(":scope > .tab-list > .tab-item")],at=t=>{const e=Tt.indexOf(t.target.closest(".tab-item"));if(e>=0)try{S.set(vt,e)}catch{}};q.querySelector(".tab-list").addEventListener("click",at),o(()=>q.querySelector(".tab-list")?.removeEventListener("click",at));const Lt=E.contextMenu(v,{cursor:"glyph",match:".fxx-row",inherit:!1,density:"compact",items:t=>kt(t.dataset.name)});o(()=>Lt?.destroy?.());const st=t=>{const e=t.target.closest(".fxx-row");if(!e)return;const a=t.target.closest(".fxx-kebab");if(a){mt(a);return}W(e.dataset.name)},ot=t=>{const e=t.target.closest(".fxx-row");e&&(t.key==="F10"&&t.shiftKey?(t.preventDefault(),mt(e)):t.key==="Enter"&&t.target===e&&W(e.dataset.name))};v.addEventListener("click",st),v.addEventListener("keydown",ot),o(()=>{v.removeEventListener("click",st),v.removeEventListener("keydown",ot)});const nt=t=>{(t.ctrlKey||t.metaKey)&&t.key==="s"&&C()&&(t.preventDefault(),H())};document.addEventListener("keydown",nt),o(()=>document.removeEventListener("keydown",nt)),At(s,{id:"effects-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>H()});const rt=t=>{if(h.peek()){try{S.set(m,{form:K(),base:y.peek(),at:new Date().toISOString()})}catch{}t.preventDefault(),t.returnValue=""}};if(window.addEventListener("beforeunload",rt),o(()=>window.removeEventListener("beforeunload",rt)),f={dirty:()=>h.peek(),dispose(){if(F=!0,h.peek())try{S.set(m,{form:K(),base:y.peek(),at:new Date().toISOString()})}catch{}[...U].forEach(t=>{try{t.close()}catch{}}),r.splice(0).forEach(t=>{try{t()}catch{}})}},I.scan(i),await Et(),!C())return;let g=null;try{g=S.get(m)}catch{}if(g?.form){try{S.remove?S.remove(m):S.set(m,null)}catch{}const e=`You left unsaved effect settings.${g.base&&g.base!==y.peek()?" The saved settings have changed since - restoring puts yours over them.":""} Restore them?`;await E.confirm(e)&&C()&&T(p({...A(g.form).payload,...g.form}))}}};