domma-cms 0.92.1 → 0.94.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 (177) hide show
  1. package/CLAUDE.md +5 -3
  2. package/admin/css/admin.css +1 -1
  3. package/admin/js/app.js +2 -2
  4. package/admin/js/lib/action-editor-arrange.js +1 -1
  5. package/admin/js/lib/api-tokens-arrange.js +2 -2
  6. package/admin/js/lib/block-editor-arrange.js +1 -1
  7. package/admin/js/lib/blocks-arrange.js +1 -1
  8. package/admin/js/lib/collection-entries-arrange.js +1 -1
  9. package/admin/js/lib/components-arrange.js +1 -1
  10. package/admin/js/lib/dashboard-arrange.js +1 -1
  11. package/admin/js/lib/dates.js +1 -0
  12. package/admin/js/lib/forms-arrange.js +1 -1
  13. package/admin/js/lib/media-arrange.js +1 -1
  14. package/admin/js/lib/notifications-arrange.js +1 -1
  15. package/admin/js/lib/pages-arrange.js +1 -1
  16. package/admin/js/lib/related.js +1 -1
  17. package/admin/js/lib/timeline-builder.js +2 -2
  18. package/admin/js/templates/action-editor.html +6 -5
  19. package/admin/js/templates/actions-list.html +1 -1
  20. package/admin/js/templates/contacts.html +1 -1
  21. package/admin/js/templates/docs/api-actions.html +86 -60
  22. package/admin/js/templates/docs/api-authentication.html +159 -123
  23. package/admin/js/templates/docs/api-builder.html +197 -0
  24. package/admin/js/templates/docs/api-collections.html +199 -259
  25. package/admin/js/templates/docs/api-external.html +225 -0
  26. package/admin/js/templates/docs/api-forms.html +268 -0
  27. package/admin/js/templates/docs/api-layouts.html +70 -45
  28. package/admin/js/templates/docs/api-media.html +57 -80
  29. package/admin/js/templates/docs/api-navigation.html +66 -22
  30. package/admin/js/templates/docs/api-pages.html +109 -129
  31. package/admin/js/templates/docs/api-plugins.html +123 -61
  32. package/admin/js/templates/docs/api-scaffold.html +185 -0
  33. package/admin/js/templates/docs/api-settings.html +72 -64
  34. package/admin/js/templates/docs/api-users.html +74 -107
  35. package/admin/js/templates/docs/api-views.html +68 -54
  36. package/admin/js/templates/docs/components-howto.html +20 -17
  37. package/admin/js/templates/docs/components-reference.html +13 -16
  38. package/admin/js/templates/docs/components-rules.html +7 -6
  39. package/admin/js/templates/docs/components-walkthrough.html +19 -19
  40. package/admin/js/templates/docs/tutorial-crud.html +71 -40
  41. package/admin/js/templates/docs/tutorial-forms.html +51 -35
  42. package/admin/js/templates/docs/tutorial-plugin.html +132 -56
  43. package/admin/js/templates/docs/usage-actions.html +61 -15
  44. package/admin/js/templates/docs/usage-collections.html +108 -0
  45. package/admin/js/templates/docs/usage-cta-shortcode.html +14 -3
  46. package/admin/js/templates/docs/usage-dconfig.html +0 -3
  47. package/admin/js/templates/docs/usage-editions.html +213 -0
  48. package/admin/js/templates/docs/usage-media.html +22 -6
  49. package/admin/js/templates/docs/usage-navigation.html +74 -18
  50. package/admin/js/templates/docs/usage-pages.html +60 -20
  51. package/admin/js/templates/docs/usage-plugins.html +89 -17
  52. package/admin/js/templates/docs/usage-shortcodes.html +123 -70
  53. package/admin/js/templates/docs/usage-site-settings.html +50 -18
  54. package/admin/js/templates/docs/usage-tools.html +73 -0
  55. package/admin/js/templates/docs/usage-users-roles.html +99 -20
  56. package/admin/js/templates/docs/usage-views.html +36 -19
  57. package/admin/js/templates/documentation.html +153 -32
  58. package/admin/js/templates/page-editor.html +0 -5
  59. package/admin/js/templates/plugin-guide.html +15 -0
  60. package/admin/js/templates/plugin-guides.html +21 -0
  61. package/admin/js/templates/pro-docs.html +53 -234
  62. package/admin/js/templates/tutorials.html +5 -4
  63. package/admin/js/views/actions-list.js +3 -3
  64. package/admin/js/views/analytics.js +5 -5
  65. package/admin/js/views/api-endpoint-editor.js +2 -2
  66. package/admin/js/views/block-editor.js +4 -4
  67. package/admin/js/views/blocks.js +4 -4
  68. package/admin/js/views/collection-editor.js +4 -4
  69. package/admin/js/views/collection-entries.js +7 -7
  70. package/admin/js/views/component-editor.js +2 -2
  71. package/admin/js/views/contacts.js +22 -20
  72. package/admin/js/views/context-menu-editor.js +5 -5
  73. package/admin/js/views/doc-pages.js +1 -1
  74. package/admin/js/views/form-editor.js +4 -4
  75. package/admin/js/views/form-submissions.js +2 -2
  76. package/admin/js/views/index.js +1 -1
  77. package/admin/js/views/media.js +3 -3
  78. package/admin/js/views/menu-editor.js +13 -13
  79. package/admin/js/views/menu-locations.js +2 -2
  80. package/admin/js/views/my-profile.js +1 -1
  81. package/admin/js/views/page-editor.js +8 -8
  82. package/admin/js/views/plugin-guides.js +5 -0
  83. package/admin/js/views/project-detail.js +2 -2
  84. package/admin/js/views/project-settings.js +1 -1
  85. package/admin/js/views/role-editor.js +4 -4
  86. package/admin/js/views/search.js +2 -2
  87. package/admin/js/views/seo.js +17 -17
  88. package/admin/js/views/settings.js +3 -3
  89. package/admin/js/views/theme.js +3 -3
  90. package/admin/js/views/user-editor.js +1 -1
  91. package/admin/js/views/users.js +2 -2
  92. package/admin/js/views/view-editor.js +1 -1
  93. package/bin/cli.js +13 -13
  94. package/bin/lib/node-version.js +29 -0
  95. package/package.json +1 -1
  96. package/plugins/_lib/admin/mail/compose-window.js +3 -2
  97. package/plugins/_lib/admin/mail/reader-view.js +7 -6
  98. package/plugins/_lib/admin/mail/scheduling.js +4 -2
  99. package/plugins/_lib/admin/mail/templates.js +4 -4
  100. package/plugins/_lib/admin/ui/dates.js +85 -0
  101. package/plugins/blog/CLAUDE.md +31 -22
  102. package/plugins/blog/admin/views/blog.js +3 -2
  103. package/plugins/blog/admin/views/comments.js +2 -1
  104. package/plugins/blog/admin/views/post-editor.js +4 -4
  105. package/plugins/blog/blocks/blog-card-row.html +1 -1
  106. package/plugins/blog/blocks/blog-card.html +2 -2
  107. package/plugins/blog/blocks/blog-post-classic.html +2 -2
  108. package/plugins/blog/blocks/blog-post-essay.html +2 -2
  109. package/plugins/blog/blocks/blog-post-feature.html +2 -2
  110. package/plugins/blog/blocks/blog-post-minimal.html +2 -2
  111. package/plugins/blog/blocks/blog-post-sidebar.html +2 -2
  112. package/plugins/blog/blocks/blog-post-split.html +2 -2
  113. package/plugins/blog/docs/guide.md +205 -0
  114. package/plugins/blog/lib/layouts.js +3 -3
  115. package/plugins/blog/lib/page.js +2 -1
  116. package/plugins/blog/plugin.js +3 -3
  117. package/plugins/blog/plugin.json +4 -4
  118. package/plugins/blog/tests/layouts.test.js +6 -0
  119. package/plugins/feedback/CLAUDE.md +22 -3
  120. package/plugins/feedback/admin/lib/kit.js +6 -7
  121. package/plugins/feedback/admin/views/feedback.js +79 -10
  122. package/plugins/feedback/admin/views/send.js +28 -6
  123. package/plugins/feedback/docs/guide.md +95 -0
  124. package/plugins/feedback/lib/receiver.js +9 -2
  125. package/plugins/feedback/lib/sender.js +3 -2
  126. package/plugins/feedback/plugin.js +54 -6
  127. package/plugins/feedback/plugin.json +4 -4
  128. package/plugins/feedback/tests/api.test.js +74 -2
  129. package/plugins/free-tier.lock.json +49 -44
  130. package/plugins/mail-reader/CLAUDE.md +33 -18
  131. package/plugins/mail-reader/docs/guide.md +147 -0
  132. package/plugins/mail-reader/plugin.json +1 -1
  133. package/plugins/security/CLAUDE.md +4 -1
  134. package/plugins/security/admin/views/security.js +5 -5
  135. package/plugins/security/docs/guide.md +170 -0
  136. package/plugins/security/plugin.js +2 -1
  137. package/plugins/security/plugin.json +2 -1
  138. package/plugins/shopping-cart/CLAUDE.md +7 -1
  139. package/plugins/shopping-cart/admin/lib/kit.js +5 -2
  140. package/plugins/shopping-cart/admin/views/orders.js +4 -4
  141. package/plugins/shopping-cart/admin/views/overview.js +2 -2
  142. package/plugins/shopping-cart/docs/guide.md +191 -0
  143. package/plugins/shopping-cart/lib/render.js +2 -1
  144. package/plugins/shopping-cart/plugin.json +3 -3
  145. package/public/js/collection-browser.js +2 -2
  146. package/public/js/site.js +1 -1
  147. package/scripts/gen-instance-secret.js +3 -1
  148. package/scripts/setup.js +3 -1
  149. package/server/middleware/auth.js +2 -1
  150. package/server/routes/api/actions.js +47 -27
  151. package/server/routes/api/blocks.js +2 -1
  152. package/server/routes/api/collections.js +16 -52
  153. package/server/routes/api/contacts.js +66 -3
  154. package/server/routes/api/documentation.js +42 -0
  155. package/server/routes/api/notifications.js +3 -2
  156. package/server/routes/api/users.js +10 -6
  157. package/server/server.js +16 -1
  158. package/server/services/actions.js +110 -34
  159. package/server/services/adapterRegistry.js +169 -16
  160. package/server/services/adapters/FileAdapter.js +25 -0
  161. package/server/services/adapters/MongoAdapter.js +23 -0
  162. package/server/services/collections.js +104 -1
  163. package/server/services/connectionManager.js +12 -0
  164. package/server/services/dates.js +81 -0
  165. package/server/services/docs.js +13 -2
  166. package/server/services/markdown.js +75 -26
  167. package/server/services/notification-sources.js +6 -5
  168. package/server/services/passwordReset.js +2 -1
  169. package/server/services/permissionRegistry.js +3 -2
  170. package/server/services/pluginGuides.js +255 -0
  171. package/server/services/pluginInstaller.js +54 -13
  172. package/server/services/plugins.js +29 -1
  173. package/server/services/presetCollections.js +31 -5
  174. package/server/services/renderer.js +2 -2
  175. package/server/services/sidebarBadges.js +3 -1
  176. package/server/services/tools.js +4 -2
  177. package/server/templates/page.html +2 -2
@@ -1 +1 @@
1
- export const OP_LABELS={eq:"=",ne:"\u2260",contains:"contains",gt:">",lt:"<"},MAX_COLUMNS=5,PAGE_SIZE=500,SHOW_STEP=100;export function matchesFilter(t,e){const r=t?.data?.[e.field],n=String(r??"");switch(e.op){case"eq":return n===String(e.value);case"ne":return n!==String(e.value);case"contains":return n.toLowerCase().includes(String(e.value).toLowerCase());case"gt":case"lt":{const i=o=>String(o).trim()!==""&&Number.isFinite(Number(o))?Number(o):NaN,s=i(n),c=i(e.value);if(!Number.isNaN(s)&&!Number.isNaN(c))return e.op==="gt"?s>c:s<c;const a=Date.parse(n),u=Date.parse(e.value);return!Number.isNaN(a)&&!Number.isNaN(u)?e.op==="gt"?a>u:a<u:e.op==="gt"?n>String(e.value):n<String(e.value)}default:return!0}}export function valueText(t){return t==null?"":typeof t=="boolean"?t?"Yes":"No":Array.isArray(t)?t.map(e=>e&&typeof e=="object"?JSON.stringify(e):String(e)).join(", "):typeof t=="object"?JSON.stringify(t):String(t)}export function cellText(t,e=60){const r=valueText(t).replace(/\s+/g," ").trim();return r.length>e?`${r.slice(0,e-1)}\u2026`:r}export function arrangeEntries(t,{filters:e=[],query:r="",sort:n={key:"created",dir:"desc"}}={}){const i=String(r||"").toLowerCase().trim();let s=(t||[]).filter(o=>e.every(p=>matchesFilter(o,p)));i&&(s=s.filter(o=>Object.values(o?.data||{}).some(p=>valueText(p).toLowerCase().includes(i))));const c=n?.dir==="asc"?1:-1,a=n?.key||"created",u=o=>a==="created"?String(o?.meta?.createdAt||""):o?.data?.[a];return[...s].sort((o,p)=>{const l=u(o),m=u(p),g=typeof l=="number"?l:parseFloat(l),b=typeof m=="number"?m:parseFloat(m);return a!=="created"&&!Number.isNaN(g)&&!Number.isNaN(b)&&String(g)===String(l??"").trim()&&String(b)===String(m??"").trim()?(g-b)*c:valueText(l).localeCompare(valueText(m),void 0,{numeric:!0,sensitivity:"base"})*c})}export function filterChoices(t,e){const r=valueText(e);if(r==="")return[];const n=r.length>28?`${r.slice(0,27)}\u2026`:r,i=[{op:"eq",value:r,label:`= \u201C${n}\u201D`},{op:"ne",value:r,label:`\u2260 \u201C${n}\u201D`}];return["number","date","time","datetime"].includes(t?.type)&&i.push({op:"gt",value:r,label:`> \u201C${n}\u201D`},{op:"lt",value:r,label:`< \u201C${n}\u201D`}),i}export function filterText(t){const e=String(t.value);return`${t.label} ${OP_LABELS[t.op]||t.op} \u201C${e.length>24?`${e.slice(0,23)}\u2026`:e}\u201D`}export function typedValue(t,e){if(t?.type==="number"&&typeof e=="string"&&e.trim()!==""){const r=Number(e);if(Number.isFinite(r))return r}return e}export function isSecretField(t){return t?.type==="password"||/pass(word)?|secret|token|api[-_]?key|private|credential/i.test(String(t?.name||""))}export function draftable(t,e=[]){const r=new Set(e.filter(isSecretField).map(n=>n.name));return Object.fromEntries(Object.entries(t||{}).filter(([n])=>!r.has(n)))}export function snapshot(t){const e=Object.keys(t||{}).sort();return JSON.stringify(e.map(r=>[r,typeof t[r]=="boolean"?t[r]:valueText(t[r])]))}export const hasForm=t=>!["roles","user-profiles"].includes(t),canClear=t=>t!=="roles";export function countText(t,e,r){const n=c=>c.toLocaleString("en-GB"),i=c=>`entr${c===1?"y":"ies"}`,s=t===e?`${n(e)} ${i(e)}`:`Showing ${n(t)} of ${n(e)}`;return r>e?`${s} \xB7 ${n(e)} of ${n(r)} loaded`:s}
1
+ import{fmtDate as b,fmtDateTime as d}from"./dates.js";export const OP_LABELS={eq:"=",ne:"\u2260",contains:"contains",gt:">",lt:"<"},MAX_COLUMNS=5,PAGE_SIZE=500,SHOW_STEP=100;export function matchesFilter(e,t){const r=e?.data?.[t.field],n=String(r??"");switch(t.op){case"eq":return n===String(t.value);case"ne":return n!==String(t.value);case"contains":return n.toLowerCase().includes(String(t.value).toLowerCase());case"gt":case"lt":{const i=s=>String(s).trim()!==""&&Number.isFinite(Number(s))?Number(s):NaN,o=i(n),c=i(t.value);if(!Number.isNaN(o)&&!Number.isNaN(c))return t.op==="gt"?o>c:o<c;const a=Date.parse(n),u=Date.parse(t.value);return!Number.isNaN(a)&&!Number.isNaN(u)?t.op==="gt"?a>u:a<u:t.op==="gt"?n>String(t.value):n<String(t.value)}default:return!0}}export function valueText(e){return e==null?"":typeof e=="boolean"?e?"Yes":"No":Array.isArray(e)?e.map(t=>t&&typeof t=="object"?JSON.stringify(t):String(t)).join(", "):typeof e=="object"?JSON.stringify(e):String(e)}export function shownValue(e,t){return t==null||t===""?t:e?.type==="date"?b(t):e?.type==="datetime"?d(t):t}export function cellText(e,t=60){const r=valueText(e).replace(/\s+/g," ").trim();return r.length>t?`${r.slice(0,t-1)}\u2026`:r}export function arrangeEntries(e,{filters:t=[],query:r="",sort:n={key:"created",dir:"desc"}}={}){const i=String(r||"").toLowerCase().trim();let o=(e||[]).filter(s=>t.every(p=>matchesFilter(s,p)));i&&(o=o.filter(s=>Object.values(s?.data||{}).some(p=>valueText(p).toLowerCase().includes(i))));const c=n?.dir==="asc"?1:-1,a=n?.key||"created",u=s=>a==="created"?String(s?.meta?.createdAt||""):s?.data?.[a];return[...o].sort((s,p)=>{const l=u(s),m=u(p),f=typeof l=="number"?l:parseFloat(l),g=typeof m=="number"?m:parseFloat(m);return a!=="created"&&!Number.isNaN(f)&&!Number.isNaN(g)&&String(f)===String(l??"").trim()&&String(g)===String(m??"").trim()?(f-g)*c:valueText(l).localeCompare(valueText(m),void 0,{numeric:!0,sensitivity:"base"})*c})}export function filterChoices(e,t){const r=valueText(t);if(r==="")return[];const n=valueText(shownValue(e,t)),i=n.length>28?`${n.slice(0,27)}\u2026`:n,o=[{op:"eq",value:r,label:`= \u201C${i}\u201D`},{op:"ne",value:r,label:`\u2260 \u201C${i}\u201D`}];return["number","date","time","datetime"].includes(e?.type)&&o.push({op:"gt",value:r,label:`> \u201C${i}\u201D`},{op:"lt",value:r,label:`< \u201C${i}\u201D`}),o}export function filterText(e){const t=String(e.value),r=/^\d{4}-\d{2}-\d{2}$/.test(t)?b(t):/^\d{4}-\d{2}-\d{2}T\d/.test(t)?d(t):t;return`${e.label} ${OP_LABELS[e.op]||e.op} \u201C${r.length>24?`${r.slice(0,23)}\u2026`:r}\u201D`}export function typedValue(e,t){if(e?.type==="number"&&typeof t=="string"&&t.trim()!==""){const r=Number(t);if(Number.isFinite(r))return r}return t}export function isSecretField(e){return e?.type==="password"||/pass(word)?|secret|token|api[-_]?key|private|credential/i.test(String(e?.name||""))}export function draftable(e,t=[]){const r=new Set(t.filter(isSecretField).map(n=>n.name));return Object.fromEntries(Object.entries(e||{}).filter(([n])=>!r.has(n)))}export function snapshot(e){const t=Object.keys(e||{}).sort();return JSON.stringify(t.map(r=>[r,typeof e[r]=="boolean"?e[r]:valueText(e[r])]))}export const hasForm=e=>!["roles","user-profiles"].includes(e),canClear=e=>e!=="roles";export function countText(e,t,r){const n=c=>c.toLocaleString("en-GB"),i=c=>`entr${c===1?"y":"ies"}`,o=e===t?`${n(t)} ${i(t)}`:`Showing ${n(e)} of ${n(t)}`;return r>t?`${o} \xB7 ${n(t)} of ${n(r)} loaded`:o}
@@ -1 +1 @@
1
- export const NAME_RE=/^[a-z][a-z0-9-]*$/,FILTERS=["all","mine","builtin","plugin"],SORTS=["name","updated"];export function kindOf(t){return t?.origin==="plugin"?"plugin":t?.bundled?"builtin":"mine"}export const KIND_LABEL={mine:"Yours",builtin:"Built in",plugin:"Plugin"};const s=(t,n,e=`${n}s`)=>`${t} ${t===1?n:e}`;export function formatSize(t){const n=Number(t)||0;return n<1024?`${n} B`:n<1024*1024?`${(n/1024).toFixed(1)} KB`:`${(n/1024/1024).toFixed(1)} MB`}export function ago(t,n=Date.now()){const e=new Date(t??"").getTime();if(!t||!Number.isFinite(e))return"";const o=Math.max(0,Math.round((n-e)/1e3));return o<60?"just now":o<3600?`${Math.round(o/60)} min ago`:o<86400?`${s(Math.round(o/3600),"hour")} ago`:o<86400*14?`${s(Math.round(o/86400),"day")} ago`:new Date(e).toISOString().slice(0,10)}export function propsSummary(t){const n=Object.keys(t&&typeof t=="object"?t:{});if(!n.length)return{count:0,text:"no props"};const e=n.slice(0,4).join(", ");return{count:n.length,text:n.length>4?`${e} +${n.length-4}`:e}}export const usageTag=t=>`<dm-${t}></dm-${t}>`;export function arrangeComponents(t,{filter:n="all",query:e="",sort:o="name"}={}){const u=String(e).trim().toLowerCase();return(t||[]).filter(r=>n==="all"||kindOf(r)===n).filter(r=>!u||[r.name,...Object.keys(r.props||{}),r.meta?.project].some(i=>String(i||"").toLowerCase().includes(u))).sort(o==="updated"?(r,i)=>String(i.updatedAt||"").localeCompare(String(r.updatedAt||""))||r.name.localeCompare(i.name):(r,i)=>r.name.localeCompare(i.name))}export function kindCounts(t){const n={all:0,mine:0,builtin:0,plugin:0};for(const e of t||[])n.all++,n[kindOf(e)]++;return n}export function componentRow(t,n=Date.now()){const e=kindOf(t),{count:o,text:u}=propsSummary(t.props),a=ago(t.updatedAt,n);return{name:t.name,file:`${t.name}.dmc`,tag:`<dm-${t.name}>`,meta:[o?`${s(o,"prop")}: ${u}`:"no props",formatSize(t.size),a&&`updated ${a}`].filter(Boolean).join(" \xB7 "),kind:e,kindLabel:KIND_LABEL[e],kindCls:e==="mine"?"":`is-${e}`,showKind:e!=="mine",project:t.meta?.project||"",locked:e==="plugin"}}export function checkName(t,n=[]){const e=String(t??"").trim();return e?NAME_RE.test(e)?n.includes(e)?`"${e}" is already a component.`:"":"A letter first, then lowercase letters, digits and hyphens.":"A component needs a name."}export function copyName(t,n=[]){const e=`${t}-copy`;if(!n.includes(e))return e;for(let o=2;;o++)if(!n.includes(`${e}-${o}`))return`${e}-${o}`}export function checkBundle(t){return!t||typeof t!="object"||Array.isArray(t)?"Not a component bundle.":!t.name||typeof t.name!="string"?"The bundle has no name.":typeof t.source!="string"?"The bundle has no source.":""}
1
+ import{fmtDate as p}from"./dates.js";export const NAME_RE=/^[a-z][a-z0-9-]*$/,FILTERS=["all","mine","builtin","plugin"],SORTS=["name","updated"];export function kindOf(t){return t?.origin==="plugin"?"plugin":t?.bundled?"builtin":"mine"}export const KIND_LABEL={mine:"Yours",builtin:"Built in",plugin:"Plugin"};const s=(t,n,e=`${n}s`)=>`${t} ${t===1?n:e}`;export function formatSize(t){const n=Number(t)||0;return n<1024?`${n} B`:n<1024*1024?`${(n/1024).toFixed(1)} KB`:`${(n/1024/1024).toFixed(1)} MB`}export function ago(t,n=Date.now()){const e=new Date(t??"").getTime();if(!t||!Number.isFinite(e))return"";const o=Math.max(0,Math.round((n-e)/1e3));return o<60?"just now":o<3600?`${Math.round(o/60)} min ago`:o<86400?`${s(Math.round(o/3600),"hour")} ago`:o<86400*14?`${s(Math.round(o/86400),"day")} ago`:p(e)}export function propsSummary(t){const n=Object.keys(t&&typeof t=="object"?t:{});if(!n.length)return{count:0,text:"no props"};const e=n.slice(0,4).join(", ");return{count:n.length,text:n.length>4?`${e} +${n.length-4}`:e}}export const usageTag=t=>`<dm-${t}></dm-${t}>`;export function arrangeComponents(t,{filter:n="all",query:e="",sort:o="name"}={}){const u=String(e).trim().toLowerCase();return(t||[]).filter(r=>n==="all"||kindOf(r)===n).filter(r=>!u||[r.name,...Object.keys(r.props||{}),r.meta?.project].some(i=>String(i||"").toLowerCase().includes(u))).sort(o==="updated"?(r,i)=>String(i.updatedAt||"").localeCompare(String(r.updatedAt||""))||r.name.localeCompare(i.name):(r,i)=>r.name.localeCompare(i.name))}export function kindCounts(t){const n={all:0,mine:0,builtin:0,plugin:0};for(const e of t||[])n.all++,n[kindOf(e)]++;return n}export function componentRow(t,n=Date.now()){const e=kindOf(t),{count:o,text:u}=propsSummary(t.props),a=ago(t.updatedAt,n);return{name:t.name,file:`${t.name}.dmc`,tag:`<dm-${t.name}>`,meta:[o?`${s(o,"prop")}: ${u}`:"no props",formatSize(t.size),a&&`updated ${a}`].filter(Boolean).join(" \xB7 "),kind:e,kindLabel:KIND_LABEL[e],kindCls:e==="mine"?"":`is-${e}`,showKind:e!=="mine",project:t.meta?.project||"",locked:e==="plugin"}}export function checkName(t,n=[]){const e=String(t??"").trim();return e?NAME_RE.test(e)?n.includes(e)?`"${e}" is already a component.`:"":"A letter first, then lowercase letters, digits and hyphens.":"A component needs a name."}export function copyName(t,n=[]){const e=`${t}-copy`;if(!n.includes(e))return e;for(let o=2;;o++)if(!n.includes(`${e}-${o}`))return`${e}-${o}`}export function checkBundle(t){return!t||typeof t!="object"||Array.isArray(t)?"Not a component bundle.":!t.name||typeof t.name!="string"?"The bundle has no name.":typeof t.source!="string"?"The bundle has no source.":""}
@@ -1 +1 @@
1
- export const BADGE_SOURCES=[{key:"pages",title:"Pages",url:"#/pages",countUrl:"/pages/_count",permission:"pages"},{key:"forms",title:"Forms",url:"#/forms",countUrl:"/forms/_count",permission:"collections"},{key:"notifications",title:"Notifications",url:"#/system/notifications",countUrl:"/system/notifications/_count",permission:"notifications"},{key:"settings",title:"Site Settings",url:"#/settings",countUrl:"/settings/_count",permission:"settings"},{key:"plugins",title:"Marketplace",url:"#/plugins",countUrl:"/plugins/_count",permission:"plugins"},{key:"views",title:"Views",url:"#/views",countUrl:"/views/_count",permission:"views"},{key:"layouts",title:"Layouts",url:"#/layouts",countUrl:"/layouts/_count",permission:"layouts"},{key:"apiTokens",title:"API Tokens",url:"#/api-tokens",countUrl:"/api-tokens/_count",permission:"api-tokens"},{key:"theme",title:"Theme",url:"#/theme",countUrl:"/theme/count",permission:"theme"}];const c=new Set(["notifications","settings","layouts","apiTokens"]);export function canAny(e=[],o){return o?e.some(t=>t===o||String(t).startsWith(`${o}.`)):!0}export function deltaText(e){if(e==null||!Number.isFinite(Number(e)))return"";const o=Number(e);return`${o>=0?"\u25B2":"\u25BC"} ${Math.abs(o).toFixed(0)}% vs yesterday`}export function rowValue(e,o){const t=(e?.details?.rows||[]).find(i=>i.label===o);return t?t.value:null}const a=e=>Number(e||0).toLocaleString("en-GB");export function kpisFrom(e,o={}){const t=o.pages,i=o.forms,n=rowValue(t,"Drafts"),s=rowValue(i,"Last 7 days"),r=e?.health?.status||null;return{hits:e?a(e.traffic?.today):"\u2014",hitsDelta:e?deltaText(e.traffic?.deltaPct):"",hitsUp:Number(e?.traffic?.deltaPct)>=0,active:e?a(e.realtime?.activeSessions):"\u2014",pages:t?a(t.count):"\u2014",pagesSub:t?Number(n)?`${a(n)} draft${Number(n)===1?"":"s"}`:"no drafts":"",submissions:i?a(s):"\u2014",submissionsSub:i?`${a(rowValue(i,"Last 24 hours"))} in the last 24 hours`:"",health:r?{ok:"Healthy",warn:"Needs a look",fail:"Failing"}[r]||r:"\u2014",healthTone:{ok:"ok",warn:"warn",fail:"danger"}[r]||""}}export function attentionFrom(e={},o=[]){const t={danger:0,attention:1,info:2},i=[];for(const n of BADGE_SOURCES){const s=e[n.key];if(!s||!(Number(s.count)>0&&(s.tone==="danger"||s.tone==="attention"||c.has(n.key))))continue;const l=(s.details?.items||[]).slice(0,2).map(u=>u.text).filter(Boolean);i.push({id:`badge:${n.key}`,title:n.title,text:String(s.label||""),meta:l.join(" \xB7 "),tone:s.tone==="danger"?"danger":"warn",rank:t[s.tone]??2,href:n.url})}for(const n of o||[])!n||n.level==="ok"||i.push({id:`health:${n.id||n.label}`,title:"Site health",text:`${n.label||n.id}: ${n.value??""}`.trim(),meta:Array.isArray(n.detail)?n.detail.slice(0,3).join(", "):"",tone:n.level==="fail"?"danger":"warn",rank:n.level==="fail"?0:1,href:""});return i.sort((n,s)=>n.rank-s.rank||n.title.localeCompare(s.title))}export function ago(e,o=Date.now()){const t=new Date(e).getTime();if(!Number.isFinite(t))return"";const i=Math.max(0,Math.round((o-t)/1e3));return i<60?"just now":i<3600?`${Math.round(i/60)} min ago`:i<86400?`${Math.round(i/3600)} hour${Math.round(i/3600)===1?"":"s"} ago`:i<86400*14?`${Math.round(i/86400)} day${Math.round(i/86400)===1?"":"s"} ago`:new Date(t).toISOString().slice(0,10)}export function activityRows(e=[],o=Date.now()){return(e||[]).map((t,i)=>{const n=ago(t.at,o);if(t.type==="entry"||t.type==="form"){const r=t.collection||t.form||"";return{id:`a${i}`,kind:"entry",icon:"database",text:`New entry in ${t.title||r}`,meta:n,href:r?`#/collections/${encodeURIComponent(r)}/entries`:"",url:"",slug:r}}const s=t.type==="publish"?"published":"edited";return{id:`a${i}`,kind:"page",icon:t.type==="publish"?"upload":"edit",text:`${t.actor||"Someone"} ${s} ${t.target||"/"}`,meta:n,href:`#/pages/edit${t.target||"/"}`,url:t.target||"/",slug:""}})}export function chartSeries(e=[]){const o=Array.isArray(e)?e:[];return{labels:o.map(t=>String(t.day||"").slice(5)),values:o.map(t=>Number(t.hits)||0),empty:!o.some(t=>Number(t.hits)>0)}}export function isSitePath(e){return typeof e=="string"&&/^\/(?!\/)/.test(e)&&!/^\/(admin|api|media|public)(\/|$)/.test(e)}
1
+ import{fmtDate as c}from"./dates.js";export const BADGE_SOURCES=[{key:"pages",title:"Pages",url:"#/pages",countUrl:"/pages/_count",permission:"pages"},{key:"forms",title:"Forms",url:"#/forms",countUrl:"/forms/_count",permission:"collections"},{key:"notifications",title:"Notifications",url:"#/system/notifications",countUrl:"/system/notifications/_count",permission:"notifications"},{key:"settings",title:"Site Settings",url:"#/settings",countUrl:"/settings/_count",permission:"settings"},{key:"plugins",title:"Marketplace",url:"#/plugins",countUrl:"/plugins/_count",permission:"plugins"},{key:"views",title:"Views",url:"#/views",countUrl:"/views/_count",permission:"views"},{key:"layouts",title:"Layouts",url:"#/layouts",countUrl:"/layouts/_count",permission:"layouts"},{key:"apiTokens",title:"API Tokens",url:"#/api-tokens",countUrl:"/api-tokens/_count",permission:"api-tokens"},{key:"theme",title:"Theme",url:"#/theme",countUrl:"/theme/count",permission:"theme"}];const f=new Set(["notifications","settings","layouts","apiTokens"]);export function canAny(e=[],o){return o?e.some(t=>t===o||String(t).startsWith(`${o}.`)):!0}export function deltaText(e){if(e==null||!Number.isFinite(Number(e)))return"";const o=Number(e);return`${o>=0?"\u25B2":"\u25BC"} ${Math.abs(o).toFixed(0)}% vs yesterday`}export function rowValue(e,o){const t=(e?.details?.rows||[]).find(i=>i.label===o);return t?t.value:null}const a=e=>Number(e||0).toLocaleString("en-GB");export function kpisFrom(e,o={}){const t=o.pages,i=o.forms,n=rowValue(t,"Drafts"),s=rowValue(i,"Last 7 days"),r=e?.health?.status||null;return{hits:e?a(e.traffic?.today):"\u2014",hitsDelta:e?deltaText(e.traffic?.deltaPct):"",hitsUp:Number(e?.traffic?.deltaPct)>=0,active:e?a(e.realtime?.activeSessions):"\u2014",pages:t?a(t.count):"\u2014",pagesSub:t?Number(n)?`${a(n)} draft${Number(n)===1?"":"s"}`:"no drafts":"",submissions:i?a(s):"\u2014",submissionsSub:i?`${a(rowValue(i,"Last 24 hours"))} in the last 24 hours`:"",health:r?{ok:"Healthy",warn:"Needs a look",fail:"Failing"}[r]||r:"\u2014",healthTone:{ok:"ok",warn:"warn",fail:"danger"}[r]||""}}export function attentionFrom(e={},o=[]){const t={danger:0,attention:1,info:2},i=[];for(const n of BADGE_SOURCES){const s=e[n.key];if(!s||!(Number(s.count)>0&&(s.tone==="danger"||s.tone==="attention"||f.has(n.key))))continue;const l=(s.details?.items||[]).slice(0,2).map(u=>u.text).filter(Boolean);i.push({id:`badge:${n.key}`,title:n.title,text:String(s.label||""),meta:l.join(" \xB7 "),tone:s.tone==="danger"?"danger":"warn",rank:t[s.tone]??2,href:n.url})}for(const n of o||[])!n||n.level==="ok"||i.push({id:`health:${n.id||n.label}`,title:"Site health",text:`${n.label||n.id}: ${n.value??""}`.trim(),meta:Array.isArray(n.detail)?n.detail.slice(0,3).join(", "):"",tone:n.level==="fail"?"danger":"warn",rank:n.level==="fail"?0:1,href:""});return i.sort((n,s)=>n.rank-s.rank||n.title.localeCompare(s.title))}export function ago(e,o=Date.now()){const t=new Date(e).getTime();if(!Number.isFinite(t))return"";const i=Math.max(0,Math.round((o-t)/1e3));return i<60?"just now":i<3600?`${Math.round(i/60)} min ago`:i<86400?`${Math.round(i/3600)} hour${Math.round(i/3600)===1?"":"s"} ago`:i<86400*14?`${Math.round(i/86400)} day${Math.round(i/86400)===1?"":"s"} ago`:c(t)}export function activityRows(e=[],o=Date.now()){return(e||[]).map((t,i)=>{const n=ago(t.at,o);if(t.type==="entry"||t.type==="form"){const r=t.collection||t.form||"";return{id:`a${i}`,kind:"entry",icon:"database",text:`New entry in ${t.title||r}`,meta:n,href:r?`#/collections/${encodeURIComponent(r)}/entries`:"",url:"",slug:r}}const s=t.type==="publish"?"published":"edited";return{id:`a${i}`,kind:"page",icon:t.type==="publish"?"upload":"edit",text:`${t.actor||"Someone"} ${s} ${t.target||"/"}`,meta:n,href:`#/pages/edit${t.target||"/"}`,url:t.target||"/",slug:""}})}export function chartSeries(e=[]){const o=Array.isArray(e)?e:[];return{labels:o.map(t=>String(t.day||"").slice(5)),values:o.map(t=>Number(t.hits)||0),empty:!o.some(t=>Number(t.hits)>0)}}export function isSitePath(e){return typeof e=="string"&&/^\/(?!\/)/.test(e)&&!/^\/(admin|api|media|public)(\/|$)/.test(e)}
@@ -0,0 +1 @@
1
+ const e=t=>String(t).padStart(2,"0"),o=/^(\d{4})-(\d{2})-(\d{2})$/;export function toDate(t){if(t==null||t==="")return null;if(t instanceof Date)return Number.isNaN(t.getTime())?null:t;if(typeof t=="string"){const r=o.exec(t.trim());if(r)return new Date(Number(r[1]),Number(r[2])-1,Number(r[3]))}const n=new Date(t);return Number.isNaN(n.getTime())?null:n}const i=t=>t==null?"":String(t);export function fmtDate(t){const n=toDate(t);return n?`${e(n.getDate())}/${e(n.getMonth()+1)}/${n.getFullYear()}`:i(t)}export function fmtTime(t){const n=toDate(t);return n?`${e(n.getHours())}:${e(n.getMinutes())}`:i(t)}export function fmtDateTime(t){if(typeof t=="string"&&o.test(t.trim()))return fmtDate(t);const n=toDate(t);return n?`${fmtDate(n)} ${fmtTime(n)}`:i(t)}
@@ -1 +1 @@
1
- export const FILTERS=["all","recent","empty"];const u=new Set(["page-break","spacer"]),a=(t,o,i=`${o}s`)=>`${t} ${t===1?o:i}`;export function ago(t,o=Date.now()){const i=new Date(t).getTime();if(!Number.isFinite(i))return"";const e=Math.max(0,Math.round((o-i)/1e3));return e<60?"just now":e<3600?`${Math.round(e/60)} min ago`:e<86400?`${a(Math.round(e/3600),"hour")} ago`:e<86400*14?`${a(Math.round(e/86400),"day")} ago`:new Date(i).toISOString().slice(0,10)}export function formShape(t){const o=Array.isArray(t?.fields)?t.fields:[],i=o.filter(e=>e?.type==="page-break").length;return{questions:o.filter(e=>e&&!u.has(e.type)).length,steps:i?i+1:0,logic:o.some(e=>e&&(e.logic||Array.isArray(e.triggers)&&e.triggers.length))}}export function actionsOn(t){const o=t?.actions||{};return Object.entries({email:"email",collection:"saved",webhook:"webhook",action:"action"}).filter(([e])=>o[e]?.enabled).map(([,e])=>e)}export function formRow(t,o=Date.now()){const{questions:i,steps:e,logic:n}=formShape(t),s=Number(t.submissionCount)||0,r=Number(t.submissionsThisWeek)||0,l=[t.slug,a(i,"question"),e?`${e}-step wizard`:"",n?"conditional logic":"",t.lastSubmissionAt?`last submission ${ago(t.lastSubmissionAt,o)}`:"no submissions yet"].filter(Boolean).join(" \xB7 ");return{slug:t.slug,title:t.title||t.slug,meta:l,total:s,totalText:String(s),week:r,weekText:r?`${r} new this week`:"",plugin:t.plugin||"",project:t.meta?.project||"",wizard:e>0,rowCls:r?"is-recent":""}}export function arrangeForms(t,{filter:o="all",query:i=""}={}){const e=String(i).trim().toLowerCase();return[...t||[]].filter(n=>o==="recent"?Number(n.submissionsThisWeek)>0:o==="empty"?!Number(n.submissionCount):!0).filter(n=>!e||[n.title,n.slug,n.description,n.plugin,n.meta?.project].some(s=>String(s||"").toLowerCase().includes(e))).sort((n,s)=>String(s.lastSubmissionAt||"").localeCompare(String(n.lastSubmissionAt||""))||String(n.title||n.slug).localeCompare(String(s.title||s.slug)))}export function duplicatePayload(t,o){const{collection:i,...e}=t?.actions||{};return{title:String(o||`${t?.title||t?.slug||"Form"} (Copy)`).trim(),description:t?.description||"",fields:t?.fields||[],settings:t?.settings||{},actions:e}}export const shortcodeFor=t=>`[form name="${t}" /]`;
1
+ import{fmtDate as u}from"./dates.js";export const FILTERS=["all","recent","empty"];const c=new Set(["page-break","spacer"]),a=(t,o,i=`${o}s`)=>`${t} ${t===1?o:i}`;export function ago(t,o=Date.now()){const i=new Date(t).getTime();if(!Number.isFinite(i))return"";const e=Math.max(0,Math.round((o-i)/1e3));return e<60?"just now":e<3600?`${Math.round(e/60)} min ago`:e<86400?`${a(Math.round(e/3600),"hour")} ago`:e<86400*14?`${a(Math.round(e/86400),"day")} ago`:u(i)}export function formShape(t){const o=Array.isArray(t?.fields)?t.fields:[],i=o.filter(e=>e?.type==="page-break").length;return{questions:o.filter(e=>e&&!c.has(e.type)).length,steps:i?i+1:0,logic:o.some(e=>e&&(e.logic||Array.isArray(e.triggers)&&e.triggers.length))}}export function actionsOn(t){const o=t?.actions||{};return Object.entries({email:"email",collection:"saved",webhook:"webhook",action:"action"}).filter(([e])=>o[e]?.enabled).map(([,e])=>e)}export function formRow(t,o=Date.now()){const{questions:i,steps:e,logic:n}=formShape(t),s=Number(t.submissionCount)||0,r=Number(t.submissionsThisWeek)||0,l=[t.slug,a(i,"question"),e?`${e}-step wizard`:"",n?"conditional logic":"",t.lastSubmissionAt?`last submission ${ago(t.lastSubmissionAt,o)}`:"no submissions yet"].filter(Boolean).join(" \xB7 ");return{slug:t.slug,title:t.title||t.slug,meta:l,total:s,totalText:String(s),week:r,weekText:r?`${r} new this week`:"",plugin:t.plugin||"",project:t.meta?.project||"",wizard:e>0,rowCls:r?"is-recent":""}}export function arrangeForms(t,{filter:o="all",query:i=""}={}){const e=String(i).trim().toLowerCase();return[...t||[]].filter(n=>o==="recent"?Number(n.submissionsThisWeek)>0:o==="empty"?!Number(n.submissionCount):!0).filter(n=>!e||[n.title,n.slug,n.description,n.plugin,n.meta?.project].some(s=>String(s||"").toLowerCase().includes(e))).sort((n,s)=>String(s.lastSubmissionAt||"").localeCompare(String(n.lastSubmissionAt||""))||String(n.title||n.slug).localeCompare(String(s.title||s.slug)))}export function duplicatePayload(t,o){const{collection:i,...e}=t?.actions||{};return{title:String(o||`${t?.title||t?.slug||"Form"} (Copy)`).trim(),description:t?.description||"",fields:t?.fields||[],settings:t?.settings||{},actions:e}}export const shortcodeFor=t=>`[form name="${t}" /]`;
@@ -1 +1 @@
1
- export const KINDS=["all","image","video","audio","document","other"],KIND_LABEL={all:"All",image:"Images",video:"Video",audio:"Audio",document:"Documents",other:"Other"},SORTS=["newest","oldest","name","size"];const c={image:["png","jpg","jpeg","gif","webp","svg","avif","ico","bmp","tif","tiff"],video:["mp4","webm","ogv","mov"],audio:["mp3","ogg","oga","wav","weba","m4a"],document:["pdf","txt","csv","doc","docx","xls","xlsx"]},u=new Set(["png","jpg","jpeg","gif","webp","svg","avif","ico","bmp"]),m=new Set(["jpg","jpeg","png","webp","gif","tiff"]);export const extOf=e=>{const t=/\.([a-z0-9]+)$/i.exec(String(e??""));return t?t[1].toLowerCase():""};export function kindOf(e){const t=extOf(e);for(const[o,n]of Object.entries(c))if(n.includes(t))return o;return"other"}export const isPreviewable=e=>u.has(extOf(e)),isEditable=e=>m.has(extOf(e));export function formatBytes(e){const t=Number(e)||0;if(t<1024)return`${t} B`;const o=["KB","MB","GB","TB"];let n=t/1024,r=0;for(;n>=1024&&r<o.length-1;)n/=1024,r++;return`${n.toFixed(1)} ${o[r]}`}export function ago(e,t=Date.now()){const o=new Date(e).getTime();if(!Number.isFinite(o))return"";const n=Math.max(0,Math.round((t-o)/1e3));if(n<60)return"just now";if(n<3600)return`${Math.round(n/60)} min ago`;if(n<86400){const r=Math.round(n/3600);return`${r} hour${r===1?"":"s"} ago`}if(n<86400*14){const r=Math.round(n/86400);return`${r} day${r===1?"":"s"} ago`}return new Date(o).toISOString().slice(0,10)}export function safeName(e){return String(e??"").split(/[\\/]/).pop().replace(/[^a-zA-Z0-9._-]/g,"_")}export function clashes(e,t){const o=new Set(t);return[...new Set(e.map(safeName))].filter(n=>o.has(n))}export function altFromName(e){return String(e??"").replace(/\.[^.]+$/,"").replace(/[-_]+/g," ").replace(/\s+/g," ").trim()}export function markdownFor(e){return isPreviewable(e.name)?`![${altFromName(e.name)}](${e.url})`:`[${e.name}](${e.url})`}export function kindCounts(e){const t=Object.fromEntries(KINDS.map(o=>[o,0]));for(const o of e||[])t.all++,t[kindOf(o.name)]++;return t}export function arrangeMedia(e,{kind:t="all",query:o="",sort:n="newest"}={}){const r=String(o).trim().toLowerCase(),s={newest:(a,i)=>String(i.createdAt).localeCompare(String(a.createdAt)),oldest:(a,i)=>String(a.createdAt).localeCompare(String(i.createdAt)),name:(a,i)=>a.name.localeCompare(i.name,void 0,{numeric:!0,sensitivity:"base"}),size:(a,i)=>(i.size||0)-(a.size||0)}[n]||(()=>0);return[...e||[]].filter(a=>t==="all"||kindOf(a.name)===t).filter(a=>!r||a.name.toLowerCase().includes(r)).sort((a,i)=>s(a,i)||a.name.localeCompare(i.name))}export function mediaTile(e,{now:t=Date.now(),stamp:o=""}={}){const n=kindOf(e.name),r=isPreviewable(e.name);return{name:e.name,url:e.url,thumb:r?`${e.url}${o?`?t=${encodeURIComponent(o)}`:""}`:null,preview:r,icon:{video:"video",audio:"music",document:"file-text"}[n]||(n==="image"?"image":"paperclip"),kind:n,kindLabel:(extOf(e.name)||n).toUpperCase(),meta:[formatBytes(e.size),ago(e.createdAt,t)].filter(Boolean).join(" \xB7 ")}}export function normalisePrefs(e){const t=e&&typeof e=="object"?e:{};return{kind:KINDS.includes(t.kind)?t.kind:"all",sort:SORTS.includes(t.sort)?t.sort:"newest"}}
1
+ import{fmtDate as c}from"./dates.js";export const KINDS=["all","image","video","audio","document","other"],KIND_LABEL={all:"All",image:"Images",video:"Video",audio:"Audio",document:"Documents",other:"Other"},SORTS=["newest","oldest","name","size"];const u={image:["png","jpg","jpeg","gif","webp","svg","avif","ico","bmp","tif","tiff"],video:["mp4","webm","ogv","mov"],audio:["mp3","ogg","oga","wav","weba","m4a"],document:["pdf","txt","csv","doc","docx","xls","xlsx"]},m=new Set(["png","jpg","jpeg","gif","webp","svg","avif","ico","bmp"]),p=new Set(["jpg","jpeg","png","webp","gif","tiff"]);export const extOf=e=>{const t=/\.([a-z0-9]+)$/i.exec(String(e??""));return t?t[1].toLowerCase():""};export function kindOf(e){const t=extOf(e);for(const[o,n]of Object.entries(u))if(n.includes(t))return o;return"other"}export const isPreviewable=e=>m.has(extOf(e)),isEditable=e=>p.has(extOf(e));export function formatBytes(e){const t=Number(e)||0;if(t<1024)return`${t} B`;const o=["KB","MB","GB","TB"];let n=t/1024,r=0;for(;n>=1024&&r<o.length-1;)n/=1024,r++;return`${n.toFixed(1)} ${o[r]}`}export function ago(e,t=Date.now()){const o=new Date(e).getTime();if(!Number.isFinite(o))return"";const n=Math.max(0,Math.round((t-o)/1e3));if(n<60)return"just now";if(n<3600)return`${Math.round(n/60)} min ago`;if(n<86400){const r=Math.round(n/3600);return`${r} hour${r===1?"":"s"} ago`}if(n<86400*14){const r=Math.round(n/86400);return`${r} day${r===1?"":"s"} ago`}return c(o)}export function safeName(e){return String(e??"").split(/[\\/]/).pop().replace(/[^a-zA-Z0-9._-]/g,"_")}export function clashes(e,t){const o=new Set(t);return[...new Set(e.map(safeName))].filter(n=>o.has(n))}export function altFromName(e){return String(e??"").replace(/\.[^.]+$/,"").replace(/[-_]+/g," ").replace(/\s+/g," ").trim()}export function markdownFor(e){return isPreviewable(e.name)?`![${altFromName(e.name)}](${e.url})`:`[${e.name}](${e.url})`}export function kindCounts(e){const t=Object.fromEntries(KINDS.map(o=>[o,0]));for(const o of e||[])t.all++,t[kindOf(o.name)]++;return t}export function arrangeMedia(e,{kind:t="all",query:o="",sort:n="newest"}={}){const r=String(o).trim().toLowerCase(),s={newest:(a,i)=>String(i.createdAt).localeCompare(String(a.createdAt)),oldest:(a,i)=>String(a.createdAt).localeCompare(String(i.createdAt)),name:(a,i)=>a.name.localeCompare(i.name,void 0,{numeric:!0,sensitivity:"base"}),size:(a,i)=>(i.size||0)-(a.size||0)}[n]||(()=>0);return[...e||[]].filter(a=>t==="all"||kindOf(a.name)===t).filter(a=>!r||a.name.toLowerCase().includes(r)).sort((a,i)=>s(a,i)||a.name.localeCompare(i.name))}export function mediaTile(e,{now:t=Date.now(),stamp:o=""}={}){const n=kindOf(e.name),r=isPreviewable(e.name);return{name:e.name,url:e.url,thumb:r?`${e.url}${o?`?t=${encodeURIComponent(o)}`:""}`:null,preview:r,icon:{video:"video",audio:"music",document:"file-text"}[n]||(n==="image"?"image":"paperclip"),kind:n,kindLabel:(extOf(e.name)||n).toUpperCase(),meta:[formatBytes(e.size),ago(e.createdAt,t)].filter(Boolean).join(" \xB7 ")}}export function normalisePrefs(e){const t=e&&typeof e=="object"?e:{};return{kind:KINDS.includes(t.kind)?t.kind:"all",sort:SORTS.includes(t.sort)?t.sort:"newest"}}
@@ -1 +1 @@
1
- export const SEVERITIES=["critical","warning","info","success"],FILTERS=["all","new",...SEVERITIES],SEVERITY_TONE={critical:"danger",warning:"warn",success:"ok",info:"info"};const s=(t,e,r=`${e}s`)=>`${t} ${t===1?e:r}`;export function severityOf(t){const e=t?.data?.severity;return SEVERITIES.includes(e)?e:"info"}export function ago(t,e=Date.now()){const r=new Date(t).getTime();if(!Number.isFinite(r))return"";const n=Math.max(0,Math.round((e-r)/1e3));return n<60?"just now":n<3600?`${Math.round(n/60)} min ago`:n<86400?`${s(Math.round(n/3600),"hour")} ago`:n<86400*14?`${s(Math.round(n/86400),"day")} ago`:new Date(r).toISOString().slice(0,10)}export function arrangeNotifications(t,{filter:e="all",query:r=""}={}){const n=String(r).trim().toLowerCase();return[...t||[]].filter(o=>e==="all"?!0:e==="new"?o.isNew:severityOf(o)===e).filter(o=>!n||[o.data?.title,o.data?.body,o.data?.link].some(i=>String(i||"").toLowerCase().includes(n))).sort((o,i)=>String(i.data?.createdAt||"").localeCompare(String(o.data?.createdAt||"")))}export function filterCounts(t){const e={all:0,new:0,critical:0,warning:0,info:0,success:0};for(const r of t||[])e.all++,r.isNew&&e.new++,e[severityOf(r)]++;return e}export function safeLink(t){const e=String(t||"").trim();return/^https?:\/\/\S+$/i.test(e)||/^#\/\S*$/.test(e)?e:""}export function sourceLabel(t={}){return t.source==="manager"?"from the manager":t.source==="sample"?"sample":t.sourceLabel?String(t.sourceLabel):t.source?String(t.source).startsWith("core:")?"System":String(t.source).split(":")[0]:""}export function notificationRow(t,{now:e=Date.now(),expanded:r=""}={}){const n=t?.data||{},o=severityOf(t),i=String(n.body||""),a=i.replace(/\s+/g," ").trim();return{id:String(t.id),title:String(n.title||"(no title)"),severity:o,severityLabel:o[0].toUpperCase()+o.slice(1),tone:SEVERITY_TONE[o],meta:[n.createdAt?ago(n.createdAt,e):"",sourceLabel(n),/^https?:\/\//i.test(String(n.link||""))?"has a link":"",n.expiresAt&&Date.parse(n.expiresAt)>e?`until ${String(n.expiresAt).slice(0,10)}`:""].filter(Boolean).join(" \xB7 "),preview:a.length>140?`${a.slice(0,139)}\u2026`:a,body:i,link:safeLink(n.link),linkTarget:/^#\//.test(safeLink(n.link))?"_self":"_blank",linkText:/^#\//.test(safeLink(n.link))?"Go there \u2192":"Open link \u2197",isNew:!!t.isNew,unread:!!t.unread,open:r===String(t.id),rowCls:[t.unread?"is-unread":"",r===String(t.id)?"is-open":"",`is-${o}`].filter(Boolean).join(" ")}}const l={boot:"at start-up","15m":"every 15 minutes",hourly:"hourly",daily:"daily at 08:00"};export function sourceRow(t,e=Date.now()){const r=t.every?`${l[t.every]||t.every}${t.atBoot&&t.every!=="boot"?" and at start-up":""}`:"as it happens";return{id:String(t.id),label:String(t.label||t.id),description:String(t.description||""),meta:[t.ownerLabel||t.owner,r,t.lastRun?`last run ${ago(t.lastRun,e)}`:""].filter(Boolean).join(" \xB7 "),enabled:!!t.enabled,audience:t.audience||"super-admin",every:t.every||"",lastError:t.lastError?`Last run failed: ${t.lastError}`:""}}export const DESKTOP_LEVELS=["all","warning","critical"],DEFAULT_SETTINGS=Object.freeze({desktop:!1,desktopLevel:"all",signInToasts:!0});export function normaliseSettings(t){const e=t&&typeof t=="object"?t:{},r=DEFAULT_SETTINGS;return{desktop:typeof e.desktop=="boolean"?e.desktop:r.desktop,desktopLevel:DESKTOP_LEVELS.includes(e.desktopLevel)?e.desktopLevel:r.desktopLevel,signInToasts:typeof e.signInToasts=="boolean"?e.signInToasts:r.signInToasts}}export function meetsLevel(t,e="all"){return e==="critical"?t==="critical":e==="warning"?t==="critical"||t==="warning":!0}export function desktopWorthy(t,{seen:e=[],level:r="all",max:n=3}={}){const o=new Set((e||[]).map(String));return[...t||[]].filter(i=>i&&i.unread&&!o.has(String(i.id))&&meetsLevel(severityOf(i),r)).sort((i,a)=>String(i.data?.createdAt||"").localeCompare(String(a.data?.createdAt||""))).slice(-n)}export function rememberSeen(t,e,r=200){const n=[...(t||[]).map(String)];for(const o of e||[])n.includes(String(o))||n.push(String(o));return n.slice(-r)}export function desktopPayload(t){const e=t?.data||{},r=severityOf(t),n=String(e.body||"").replace(/\s+/g," ").trim();return{title:String(e.title||"Notification").slice(0,120),options:{body:n.length>240?`${n.slice(0,239)}\u2026`:n,tag:`dcms-notification-${t?.id}`,requireInteraction:r==="critical"}}}
1
+ import{fmtDate as s}from"./dates.js";export const SEVERITIES=["critical","warning","info","success"],FILTERS=["all","new",...SEVERITIES],SEVERITY_TONE={critical:"danger",warning:"warn",success:"ok",info:"info"};const l=(t,e,n=`${e}s`)=>`${t} ${t===1?e:n}`;export function severityOf(t){const e=t?.data?.severity;return SEVERITIES.includes(e)?e:"info"}export function ago(t,e=Date.now()){const n=new Date(t).getTime();if(!Number.isFinite(n))return"";const r=Math.max(0,Math.round((e-n)/1e3));return r<60?"just now":r<3600?`${Math.round(r/60)} min ago`:r<86400?`${l(Math.round(r/3600),"hour")} ago`:r<86400*14?`${l(Math.round(r/86400),"day")} ago`:s(n)}export function arrangeNotifications(t,{filter:e="all",query:n=""}={}){const r=String(n).trim().toLowerCase();return[...t||[]].filter(o=>e==="all"?!0:e==="new"?o.isNew:severityOf(o)===e).filter(o=>!r||[o.data?.title,o.data?.body,o.data?.link].some(i=>String(i||"").toLowerCase().includes(r))).sort((o,i)=>String(i.data?.createdAt||"").localeCompare(String(o.data?.createdAt||"")))}export function filterCounts(t){const e={all:0,new:0,critical:0,warning:0,info:0,success:0};for(const n of t||[])e.all++,n.isNew&&e.new++,e[severityOf(n)]++;return e}export function safeLink(t){const e=String(t||"").trim();return/^https?:\/\/\S+$/i.test(e)||/^#\/\S*$/.test(e)?e:""}export function sourceLabel(t={}){return t.source==="manager"?"from the manager":t.source==="sample"?"sample":t.sourceLabel?String(t.sourceLabel):t.source?String(t.source).startsWith("core:")?"System":String(t.source).split(":")[0]:""}export function notificationRow(t,{now:e=Date.now(),expanded:n=""}={}){const r=t?.data||{},o=severityOf(t),i=String(r.body||""),a=i.replace(/\s+/g," ").trim();return{id:String(t.id),title:String(r.title||"(no title)"),severity:o,severityLabel:o[0].toUpperCase()+o.slice(1),tone:SEVERITY_TONE[o],meta:[r.createdAt?ago(r.createdAt,e):"",sourceLabel(r),/^https?:\/\//i.test(String(r.link||""))?"has a link":"",r.expiresAt&&Date.parse(r.expiresAt)>e?`until ${s(r.expiresAt)}`:""].filter(Boolean).join(" \xB7 "),preview:a.length>140?`${a.slice(0,139)}\u2026`:a,body:i,link:safeLink(r.link),linkTarget:/^#\//.test(safeLink(r.link))?"_self":"_blank",linkText:/^#\//.test(safeLink(r.link))?"Go there \u2192":"Open link \u2197",isNew:!!t.isNew,unread:!!t.unread,open:n===String(t.id),rowCls:[t.unread?"is-unread":"",n===String(t.id)?"is-open":"",`is-${o}`].filter(Boolean).join(" ")}}const c={boot:"at start-up","15m":"every 15 minutes",hourly:"hourly",daily:"daily at 08:00"};export function sourceRow(t,e=Date.now()){const n=t.every?`${c[t.every]||t.every}${t.atBoot&&t.every!=="boot"?" and at start-up":""}`:"as it happens";return{id:String(t.id),label:String(t.label||t.id),description:String(t.description||""),meta:[t.ownerLabel||t.owner,n,t.lastRun?`last run ${ago(t.lastRun,e)}`:""].filter(Boolean).join(" \xB7 "),enabled:!!t.enabled,audience:t.audience||"super-admin",every:t.every||"",lastError:t.lastError?`Last run failed: ${t.lastError}`:""}}export const DESKTOP_LEVELS=["all","warning","critical"],DEFAULT_SETTINGS=Object.freeze({desktop:!1,desktopLevel:"all",signInToasts:!0});export function normaliseSettings(t){const e=t&&typeof t=="object"?t:{},n=DEFAULT_SETTINGS;return{desktop:typeof e.desktop=="boolean"?e.desktop:n.desktop,desktopLevel:DESKTOP_LEVELS.includes(e.desktopLevel)?e.desktopLevel:n.desktopLevel,signInToasts:typeof e.signInToasts=="boolean"?e.signInToasts:n.signInToasts}}export function meetsLevel(t,e="all"){return e==="critical"?t==="critical":e==="warning"?t==="critical"||t==="warning":!0}export function desktopWorthy(t,{seen:e=[],level:n="all",max:r=3}={}){const o=new Set((e||[]).map(String));return[...t||[]].filter(i=>i&&i.unread&&!o.has(String(i.id))&&meetsLevel(severityOf(i),n)).sort((i,a)=>String(i.data?.createdAt||"").localeCompare(String(a.data?.createdAt||""))).slice(-r)}export function rememberSeen(t,e,n=200){const r=[...(t||[]).map(String)];for(const o of e||[])r.includes(String(o))||r.push(String(o));return r.slice(-n)}export function desktopPayload(t){const e=t?.data||{},n=severityOf(t),r=String(e.body||"").replace(/\s+/g," ").trim();return{title:String(e.title||"Notification").slice(0,120),options:{body:r.length>240?`${r.slice(0,239)}\u2026`:r,tag:`dcms-notification-${t?.id}`,requireInteraction:n==="critical"}}}
@@ -1 +1 @@
1
- export const STATUSES=["all","published","draft"],SORTS=["order","updated","title","url"],MODES=["list","tree"],DEFAULT_SETTINGS={status:"all",sort:"order",mode:"list"};const l=(t,e,r=`${e}s`)=>`${t} ${t===1?e:r}`;export function normaliseSettings(t){const e=t&&typeof t=="object"?t:{};return{status:STATUSES.includes(e.status)?e.status:DEFAULT_SETTINGS.status,sort:SORTS.includes(e.sort)?e.sort:DEFAULT_SETTINGS.sort,mode:MODES.includes(e.mode)?e.mode:DEFAULT_SETTINGS.mode}}export function ago(t,e=Date.now()){const r=new Date(t).getTime();if(!Number.isFinite(r))return"";const n=Math.max(0,Math.round((e-r)/1e3));return n<60?"just now":n<3600?`${Math.round(n/60)} min ago`:n<86400?`${l(Math.round(n/3600),"hour")} ago`:n<86400*14?`${l(Math.round(n/86400),"day")} ago`:new Date(r).toISOString().slice(0,10)}export function visibilityLabel(t){const e=String(t??"public").trim();return!e||e==="public"?"":e==="private"?"Private":e.startsWith("=")?`Only ${e.slice(1)}`:`${e}+`}const a=t=>`${t.title||""} ${t.urlPath||""} ${(t.tags||[]).join(" ")} ${t.layout||""}`.toLowerCase();export function arrangePages(t,{status:e="all",query:r="",sort:n="order"}={}){const s=String(r).trim().toLowerCase(),i=(t||[]).filter(o=>e==="all"||(e==="draft"?o.status!=="published":o.status==="published")).filter(o=>!s||a(o).includes(s));return n==="updated"?[...i].sort((o,u)=>String(u.updatedAt||"").localeCompare(String(o.updatedAt||""))):n==="title"?[...i].sort((o,u)=>String(o.title||"").localeCompare(String(u.title||""))):n==="url"?[...i].sort((o,u)=>String(o.urlPath).localeCompare(String(u.urlPath))):i}export function statusCounts(t){const e=(t||[]).length,r=(t||[]).filter(n=>n.status==="published").length;return{all:e,published:r,draft:e-r}}export function pageRow(t,e=Date.now()){const r=t.status!=="published",n=Number.isFinite(t.versionCount)?t.versionCount:0,s=t.resolvedProject&&t.resolvedProject!=="core"?t.resolvedProject:"";return{urlPath:t.urlPath,title:t.title||t.urlPath,statusText:r?"Draft":"Published",statusCls:r?"is-warn":"is-ok",visibility:visibilityLabel(t.visibility),project:s,plugin:t.plugin?String(t.plugin):"",bundled:t.bundled===!0,tagsText:(t.tags||[]).filter(Boolean).map(i=>`#${i}`).join(" "),meta:[t.urlPath,t.layout||"default",l(n,"revision"),t.updatedAt?`updated ${ago(t.updatedAt,e)}`:""].filter(Boolean).join(" \xB7 "),revisions:n,rowCls:r?"is-draft":""}}export function treeRows(t){const e=new Set((t||[]).map(r=>r.urlPath));return(t||[]).map(r=>{const n=String(r.urlPath).split("/").filter(Boolean);let s=null;for(let i=n.length-1;i>0&&!s;i--){const o="/"+n.slice(0,i).join("/");e.has(o)&&(s=o)}return{id:r.urlPath,parent_id:s,name:r.title||r.urlPath,icon:r.status==="published"?"check-circle":"file-text"}})}export function checkPrune(t,e){const r=String(t??"").trim();if(!/^\d+$/.test(r))return{error:"A whole number, 0 or more.",keep:0,deletes:0};const n=Number.parseInt(r,10);return{error:"",keep:n,deletes:Math.max(0,(Number(e)||0)-n)}}
1
+ import{fmtDate as a}from"./dates.js";export const STATUSES=["all","published","draft"],SORTS=["order","updated","title","url"],MODES=["list","tree"],DEFAULT_SETTINGS={status:"all",sort:"order",mode:"list"};const l=(t,e,r=`${e}s`)=>`${t} ${t===1?e:r}`;export function normaliseSettings(t){const e=t&&typeof t=="object"?t:{};return{status:STATUSES.includes(e.status)?e.status:DEFAULT_SETTINGS.status,sort:SORTS.includes(e.sort)?e.sort:DEFAULT_SETTINGS.sort,mode:MODES.includes(e.mode)?e.mode:DEFAULT_SETTINGS.mode}}export function ago(t,e=Date.now()){const r=new Date(t).getTime();if(!Number.isFinite(r))return"";const o=Math.max(0,Math.round((e-r)/1e3));return o<60?"just now":o<3600?`${Math.round(o/60)} min ago`:o<86400?`${l(Math.round(o/3600),"hour")} ago`:o<86400*14?`${l(Math.round(o/86400),"day")} ago`:a(r)}export function visibilityLabel(t){const e=String(t??"public").trim();return!e||e==="public"?"":e==="private"?"Private":e.startsWith("=")?`Only ${e.slice(1)}`:`${e}+`}const d=t=>`${t.title||""} ${t.urlPath||""} ${(t.tags||[]).join(" ")} ${t.layout||""}`.toLowerCase();export function arrangePages(t,{status:e="all",query:r="",sort:o="order"}={}){const s=String(r).trim().toLowerCase(),i=(t||[]).filter(n=>e==="all"||(e==="draft"?n.status!=="published":n.status==="published")).filter(n=>!s||d(n).includes(s));return o==="updated"?[...i].sort((n,u)=>String(u.updatedAt||"").localeCompare(String(n.updatedAt||""))):o==="title"?[...i].sort((n,u)=>String(n.title||"").localeCompare(String(u.title||""))):o==="url"?[...i].sort((n,u)=>String(n.urlPath).localeCompare(String(u.urlPath))):i}export function statusCounts(t){const e=(t||[]).length,r=(t||[]).filter(o=>o.status==="published").length;return{all:e,published:r,draft:e-r}}export function pageRow(t,e=Date.now()){const r=t.status!=="published",o=Number.isFinite(t.versionCount)?t.versionCount:0,s=t.resolvedProject&&t.resolvedProject!=="core"?t.resolvedProject:"";return{urlPath:t.urlPath,title:t.title||t.urlPath,statusText:r?"Draft":"Published",statusCls:r?"is-warn":"is-ok",visibility:visibilityLabel(t.visibility),project:s,plugin:t.plugin?String(t.plugin):"",bundled:t.bundled===!0,tagsText:(t.tags||[]).filter(Boolean).map(i=>`#${i}`).join(" "),meta:[t.urlPath,t.layout||"default",l(o,"revision"),t.updatedAt?`updated ${ago(t.updatedAt,e)}`:""].filter(Boolean).join(" \xB7 "),revisions:o,rowCls:r?"is-draft":""}}export function treeRows(t){const e=new Set((t||[]).map(r=>r.urlPath));return(t||[]).map(r=>{const o=String(r.urlPath).split("/").filter(Boolean);let s=null;for(let i=o.length-1;i>0&&!s;i--){const n="/"+o.slice(0,i).join("/");e.has(n)&&(s=n)}return{id:r.urlPath,parent_id:s,name:r.title||r.urlPath,icon:r.status==="published"?"check-circle":"file-text"}})}export function checkPrune(t,e){const r=String(t??"").trim();if(!/^\d+$/.test(r))return{error:"A whole number, 0 or more.",keep:0,deletes:0};const o=Number.parseInt(r,10);return{error:"",keep:o,deletes:Math.max(0,(Number(e)||0)-o)}}
@@ -1 +1 @@
1
- import{apiRequest as w}from"../api.js";const h=new Map;export function relatedProviders(e){return h.has(e)||h.set(e,w(`/related/providers?kind=${encodeURIComponent(e)}`,{method:"GET"}).then(t=>Array.isArray(t)?t:[]).catch(()=>[])),h.get(e)}export function relatedMenuItems(e,t,r=n=>n.label){return e.flatMap(n=>n.actions||[]).map(n=>({label:r(n),icon:n.icon||"arrow-right",action:()=>{location.hash=n.url.replace(/\{id\}/g,encodeURIComponent(t)).replace(/^#/,"")}}))}export function relatedWhen(e){if(e?.when)return String(e.when);if(!e?.startAt)return"";const t=new Date(e.startAt);if(Number.isNaN(t.getTime()))return"";const r={weekday:"short",day:"numeric",month:"short",year:"numeric"};return e.allDay?t.toLocaleDateString("en-GB",{...r,timeZone:"UTC"}):`${t.toLocaleDateString("en-GB",r)}, ${t.toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"})}`}const a=(e,t,r)=>{const n=document.createElement(e);return t&&(n.className=t),r!=null&&(n.textContent=r),n};export async function mountRelated(e,t,r,{onOpen:n,name:y=""}={}){e.hidden=!0;const f=await relatedProviders(t);if(!f.length||!e.isConnected)return;e.hidden=!1,e.replaceChildren(a("p","related-loading","Loading\u2026"));let g;try{g=await w(`/related?kind=${encodeURIComponent(t)}&id=${encodeURIComponent(r)}`,{method:"GET"})}catch{e.replaceChildren(a("p","related-note","Could not be loaded just now."));return}const v=(g?.providers||[]).map(o=>{const d=a("section","related-section"),c=a("div","related-head"),s=a("span");s.dataset.icon=o.icon||"link",s.dataset.iconSize="14",c.append(s,a("strong","",o.label));const p=(f.find(i=>i.id===o.id)?.actions||[])[0];if(p){const i=a("button","btn btn-sm btn-ghost related-add",p.label.replace(/\{name\}/g,y||"them"));i.type="button",i.addEventListener("click",()=>{n?.(),location.hash=p.url.replace(/\{id\}/g,encodeURIComponent(r)).replace(/^#/,"")}),c.append(i)}if(d.append(c),o.error)d.append(a("p","related-note",o.error));else if(!o.items?.length)d.append(a("p","related-note",`No ${o.label.toLowerCase()} yet.`));else{const i=a("ul","related-list");for(const l of o.items){const u=a("li",l.past?"is-past":""),m=a(l.url?"a":"span","related-title",l.title||"Untitled");l.url&&(m.href=l.url,m.addEventListener("click",()=>n?.())),u.append(m);const C=[relatedWhen(l),l.meta].filter(Boolean).join(" \xB7 ");C&&u.append(a("small","related-meta",C)),i.append(u)}d.append(i)}return d});e.replaceChildren(...v),window.I?.scan?.(e)}
1
+ import{apiRequest as w}from"../api.js";import{fmtDate as b,fmtDateTime as y}from"./dates.js";const f=new Map;export function relatedProviders(e){return f.has(e)||f.set(e,w(`/related/providers?kind=${encodeURIComponent(e)}`,{method:"GET"}).then(t=>Array.isArray(t)?t:[]).catch(()=>[])),f.get(e)}export function relatedMenuItems(e,t,i=n=>n.label){return e.flatMap(n=>n.actions||[]).map(n=>({label:i(n),icon:n.icon||"arrow-right",action:()=>{location.hash=n.url.replace(/\{id\}/g,encodeURIComponent(t)).replace(/^#/,"")}}))}export function relatedWhen(e){if(e?.when)return String(e.when);if(!e?.startAt)return"";const t=new Date(e.startAt);return Number.isNaN(t.getTime())?"":e.allDay?b(t.toISOString().slice(0,10)):y(t)}const a=(e,t,i)=>{const n=document.createElement(e);return t&&(n.className=t),i!=null&&(n.textContent=i),n};export async function mountRelated(e,t,i,{onOpen:n,name:v=""}={}){e.hidden=!0;const h=await relatedProviders(t);if(!h.length||!e.isConnected)return;e.hidden=!1,e.replaceChildren(a("p","related-loading","Loading\u2026"));let g;try{g=await w(`/related?kind=${encodeURIComponent(t)}&id=${encodeURIComponent(i)}`,{method:"GET"})}catch{e.replaceChildren(a("p","related-note","Could not be loaded just now."));return}const I=(g?.providers||[]).map(r=>{const d=a("section","related-section"),s=a("div","related-head"),c=a("span");c.dataset.icon=r.icon||"link",c.dataset.iconSize="14",s.append(c,a("strong","",r.label));const p=(h.find(o=>o.id===r.id)?.actions||[])[0];if(p){const o=a("button","btn btn-sm btn-ghost related-add",p.label.replace(/\{name\}/g,v||"them"));o.type="button",o.addEventListener("click",()=>{n?.(),location.hash=p.url.replace(/\{id\}/g,encodeURIComponent(i)).replace(/^#/,"")}),s.append(o)}if(d.append(s),r.error)d.append(a("p","related-note",r.error));else if(!r.items?.length)d.append(a("p","related-note",`No ${r.label.toLowerCase()} yet.`));else{const o=a("ul","related-list");for(const l of r.items){const m=a("li",l.past?"is-past":""),u=a(l.url?"a":"span","related-title",l.title||"Untitled");l.url&&(u.href=l.url,u.addEventListener("click",()=>n?.())),m.append(u);const C=[relatedWhen(l),l.meta].filter(Boolean).join(" \xB7 ");C&&m.append(a("small","related-meta",C)),o.append(m)}d.append(o)}return d});e.replaceChildren(...I),window.I?.scan?.(e)}
@@ -1,6 +1,6 @@
1
- import{openIconPicker as X}from"./card-builder.js";import{safeSetHTML as Z}from"./safe-html.js";function P(e){const t={};for(const[,i,l,o]of e.matchAll(/([\w-]+)=(?:"([^"]*)"|'([^']*)')/g))t[i]=l??o??"";const n=e.replace(/([\w-]+)=(?:"[^"]*"|'[^']*')/g,i=>" ".repeat(i.length));for(const[,i]of n.matchAll(/\b([\w-]+)\b/g))i in t||(t[i]=!0);return t}function h(e){return String(e).replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}const ee=new Set(["layout","theme","mode","id","class"]),te=new Set(["title","date","status","icon"]),ne=new Set(["slug","display","layout","theme","mode","title-field","date-field","status-field","icon-field","body-field","limit"]),M=["vertical","centred","horizontal"],q=["minimal","corporate","modern"],ie=["","planned","in-progress","completed","blocked"];let T=null,a=null,y=null,k=null,F=null,$=null,U=0;export const TimelineBuilder={open(e,t={}){if(T)return;y=e,k=t,a=j(),F=null;const n=parseTimelineAtCursor(e.value,e.selectionStart);n.found&&(F=n.range,a=n.state),re()},close(){N()}};function j(){const e=new Date().toISOString().slice(0,10);return{mode:"static",layout:"vertical",theme:"minimal",roadmap:!1,id:"",className:"",extras:{},events:[{title:"First Event",date:e,status:"completed",icon:"",body:"Describe this event.",extras:{}},{title:"Second Event",date:"",status:"planned",icon:"",body:"Describe this event.",extras:{}}],collection:{slug:"",titleField:"",dateField:"",statusField:"",iconField:"",bodyField:"",limit:""}}}export function parseTimelineAtCursor(e,t){let n=null;const i=/\[timeline([^\]]*)\]([\s\S]*?)\[\/timeline\]/gi;for(const s of e.matchAll(i)){const c=s.index,u=c+s[0].length;t>=c&&t<=u&&(!n||u-c<n.range.end-n.range.start)&&(n={kind:"static",range:{start:c,end:u},attrStr:s[1],body:s[2]})}const l=/\[collection([^\]]*?)\/\]/gi;for(const s of e.matchAll(l)){const c=s.index,u=c+s[0].length;if(t<c||t>u)continue;const w=P(s[1]);w.display==="timeline"&&(!n||u-c<n.range.end-n.range.start)&&(n={kind:"collection",range:{start:c,end:u},attrs:w})}if(!n)return{found:!1};if(n.kind==="static"){const s=P(n.attrStr),c={};Object.keys(s).forEach(g=>{!ee.has(g)&&s[g]!==!0&&(c[g]=s[g])});const u=[],w=/\[event([^\]]*)\]([\s\S]*?)\[\/event\]/gi;for(const g of n.body.matchAll(w)){const C=P(g[1]),p={};Object.keys(C).forEach(v=>{!te.has(v)&&C[v]!==!0&&(p[v]=C[v])}),u.push({title:C.title||"",date:C.date||"",status:C.status||"",icon:C.icon||"",body:g[2].trim(),extras:p})}return{found:!0,range:n.range,state:{mode:"static",layout:M.includes(s.layout)?s.layout:"vertical",theme:q.includes(s.theme)?s.theme:"minimal",roadmap:s.mode==="roadmap",id:s.id||"",className:s.class||"",extras:c,events:u.length?u:j().events,collection:j().collection}}}const o=n.attrs,m={};return Object.keys(o).forEach(s=>{!ne.has(s)&&o[s]!==!0&&(m[s]=o[s])}),{found:!0,range:n.range,state:{mode:"collection",layout:M.includes(o.layout)?o.layout:"vertical",theme:q.includes(o.theme)?o.theme:"minimal",roadmap:o.mode==="roadmap",id:"",className:"",extras:m,events:j().events,collection:{slug:o.slug||"",titleField:o["title-field"]||"",dateField:o["date-field"]||"",statusField:o["status-field"]||"",iconField:o["icon-field"]||"",bodyField:o["body-field"]||"",limit:o.limit||""}}}}function W(){return a.mode==="collection"?le():oe()}function oe(){const e=a,t=[];e.layout!=="vertical"&&t.push(`layout="${h(e.layout)}"`),e.theme!=="minimal"&&t.push(`theme="${h(e.theme)}"`),e.roadmap&&t.push('mode="roadmap"'),e.id&&t.push(`id="${h(e.id)}"`),e.className&&t.push(`class="${h(e.className)}"`),Object.entries(e.extras).forEach(([l,o])=>t.push(`${l}="${h(o)}"`));const n=t.length?" "+t.join(" "):"",i=e.events.map(l=>{const o=[];return l.title&&o.push(`title="${h(l.title)}"`),l.date&&o.push(`date="${h(l.date)}"`),l.status&&o.push(`status="${h(l.status)}"`),l.icon&&o.push(`icon="${h(l.icon)}"`),Object.entries(l.extras||{}).forEach(([s,c])=>o.push(`${s}="${h(c)}"`)),`[event${o.length?" "+o.join(" "):""}]
1
+ import{openIconPicker as X}from"./card-builder.js";import{safeSetHTML as Z}from"./safe-html.js";import{fmtDate as ee}from"./dates.js";function P(e){const t={};for(const[,i,l,o]of e.matchAll(/([\w-]+)=(?:"([^"]*)"|'([^']*)')/g))t[i]=l??o??"";const n=e.replace(/([\w-]+)=(?:"[^"]*"|'[^']*')/g,i=>" ".repeat(i.length));for(const[,i]of n.matchAll(/\b([\w-]+)\b/g))i in t||(t[i]=!0);return t}function h(e){return String(e).replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}const te=new Set(["layout","theme","mode","id","class"]),ne=new Set(["title","date","status","icon"]),ie=new Set(["slug","display","layout","theme","mode","title-field","date-field","status-field","icon-field","body-field","limit"]),M=["vertical","centred","horizontal"],q=["minimal","corporate","modern"],oe=["","planned","in-progress","completed","blocked"];let T=null,a=null,y=null,k=null,F=null,$=null,U=0;export const TimelineBuilder={open(e,t={}){if(T)return;y=e,k=t,a=j(),F=null;const n=parseTimelineAtCursor(e.value,e.selectionStart);n.found&&(F=n.range,a=n.state),se()},close(){N()}};function j(){const e=ee(new Date);return{mode:"static",layout:"vertical",theme:"minimal",roadmap:!1,id:"",className:"",extras:{},events:[{title:"First Event",date:e,status:"completed",icon:"",body:"Describe this event.",extras:{}},{title:"Second Event",date:"",status:"planned",icon:"",body:"Describe this event.",extras:{}}],collection:{slug:"",titleField:"",dateField:"",statusField:"",iconField:"",bodyField:"",limit:""}}}export function parseTimelineAtCursor(e,t){let n=null;const i=/\[timeline([^\]]*)\]([\s\S]*?)\[\/timeline\]/gi;for(const s of e.matchAll(i)){const c=s.index,u=c+s[0].length;t>=c&&t<=u&&(!n||u-c<n.range.end-n.range.start)&&(n={kind:"static",range:{start:c,end:u},attrStr:s[1],body:s[2]})}const l=/\[collection([^\]]*?)\/\]/gi;for(const s of e.matchAll(l)){const c=s.index,u=c+s[0].length;if(t<c||t>u)continue;const w=P(s[1]);w.display==="timeline"&&(!n||u-c<n.range.end-n.range.start)&&(n={kind:"collection",range:{start:c,end:u},attrs:w})}if(!n)return{found:!1};if(n.kind==="static"){const s=P(n.attrStr),c={};Object.keys(s).forEach(g=>{!te.has(g)&&s[g]!==!0&&(c[g]=s[g])});const u=[],w=/\[event([^\]]*)\]([\s\S]*?)\[\/event\]/gi;for(const g of n.body.matchAll(w)){const C=P(g[1]),p={};Object.keys(C).forEach(v=>{!ne.has(v)&&C[v]!==!0&&(p[v]=C[v])}),u.push({title:C.title||"",date:C.date||"",status:C.status||"",icon:C.icon||"",body:g[2].trim(),extras:p})}return{found:!0,range:n.range,state:{mode:"static",layout:M.includes(s.layout)?s.layout:"vertical",theme:q.includes(s.theme)?s.theme:"minimal",roadmap:s.mode==="roadmap",id:s.id||"",className:s.class||"",extras:c,events:u.length?u:j().events,collection:j().collection}}}const o=n.attrs,m={};return Object.keys(o).forEach(s=>{!ie.has(s)&&o[s]!==!0&&(m[s]=o[s])}),{found:!0,range:n.range,state:{mode:"collection",layout:M.includes(o.layout)?o.layout:"vertical",theme:q.includes(o.theme)?o.theme:"minimal",roadmap:o.mode==="roadmap",id:"",className:"",extras:m,events:j().events,collection:{slug:o.slug||"",titleField:o["title-field"]||"",dateField:o["date-field"]||"",statusField:o["status-field"]||"",iconField:o["icon-field"]||"",bodyField:o["body-field"]||"",limit:o.limit||""}}}}function W(){return a.mode==="collection"?de():le()}function le(){const e=a,t=[];e.layout!=="vertical"&&t.push(`layout="${h(e.layout)}"`),e.theme!=="minimal"&&t.push(`theme="${h(e.theme)}"`),e.roadmap&&t.push('mode="roadmap"'),e.id&&t.push(`id="${h(e.id)}"`),e.className&&t.push(`class="${h(e.className)}"`),Object.entries(e.extras).forEach(([l,o])=>t.push(`${l}="${h(o)}"`));const n=t.length?" "+t.join(" "):"",i=e.events.map(l=>{const o=[];return l.title&&o.push(`title="${h(l.title)}"`),l.date&&o.push(`date="${h(l.date)}"`),l.status&&o.push(`status="${h(l.status)}"`),l.icon&&o.push(`icon="${h(l.icon)}"`),Object.entries(l.extras||{}).forEach(([s,c])=>o.push(`${s}="${h(c)}"`)),`[event${o.length?" "+o.join(" "):""}]
2
2
  ${(l.body||"").trim()}
3
3
  [/event]`}).join(`
4
4
  `);return`[timeline${n}]
5
5
  ${i}
6
- [/timeline]`}function le(){const e=a,t=e.collection,n=[`slug="${h(t.slug)}"`,'display="timeline"'];return e.layout!=="vertical"&&n.push(`layout="${h(e.layout)}"`),e.theme!=="minimal"&&n.push(`theme="${h(e.theme)}"`),e.roadmap&&n.push('mode="roadmap"'),t.titleField&&n.push(`title-field="${h(t.titleField)}"`),t.dateField&&n.push(`date-field="${h(t.dateField)}"`),t.statusField&&n.push(`status-field="${h(t.statusField)}"`),t.iconField&&n.push(`icon-field="${h(t.iconField)}"`),t.bodyField&&n.push(`body-field="${h(t.bodyField)}"`),t.limit&&n.push(`limit="${h(t.limit)}"`),Object.entries(e.extras).forEach(([i,l])=>n.push(`${i}="${h(l)}"`)),`[collection ${n.join(" ")} /]`}function N(){$&&(clearTimeout($),$=null),T&&(T.remove(),T=null,a=null,F=null)}function de(){const e=W();if(F){const{start:t,end:n}=F;y.value=y.value.slice(0,t)+e+y.value.slice(n),y.selectionStart=y.selectionEnd=t+e.length}else{const t=y.selectionStart,n=y.selectionEnd;y.value=y.value.slice(0,t)+e+y.value.slice(n),y.selectionStart=y.selectionEnd=t+e.length}y.dispatchEvent(new Event("input",{bubbles:!0})),y.focus(),N()}function d(e,t=""){const n=document.createElement(e);return t&&(n.style.cssText=t),n}function z(e,t,n=14){e.textContent="";const i=document.createElement("span");return i.setAttribute("data-icon",t),i.setAttribute("data-icon-size",String(n)),i.style.cssText="display:inline-flex;align-items:center;vertical-align:middle;",e.appendChild(i),typeof I<"u"&&I.scan&&I.scan(e),e}function ae(e,t,n,i=14){return z(e,t,i),e.appendChild(document.createTextNode(" "+n)),e}const L="padding:7px 10px;border-radius:7px;border:1px solid var(--dm-border,#334155);background:var(--dm-surface,#0f172a);color:var(--dm-text,#e2e8f0);font-size:12px;font-family:inherit;width:100%",D="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--dm-text-muted,#475569)";function _(e,t){const n=d("div","display:flex;flex-direction:column;gap:5px"),i=d("div",D);return i.textContent=e,n.appendChild(i),n.appendChild(t),n}function K(){return d("div","height:1px;background:var(--dm-border,#334155);margin:2px 0")}function re(){T=d("div","position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:9999;padding:24px;isolation:isolate");const e=d("div","background:var(--dm-surface,#1e293b);border:1px solid var(--dm-border,#334155);border-radius:14px;width:100%;max-width:960px;max-height:90vh;display:flex;flex-direction:column;box-shadow:var(--dm-shadow-lg);overflow:hidden"),t=d("div","display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--dm-border,#334155);flex-shrink:0"),n=d("span","font-weight:700;color:var(--dm-text,#f1f5f9);font-size:14px");n.textContent=F?"Edit Timeline":"Insert Timeline";const i=d("button","background:var(--dm-border,#334155);border:none;color:var(--dm-text-muted,#94a3b8);border-radius:6px;width:26px;height:26px;cursor:pointer;font-size:14px");i.title="Close",z(i,"x"),i.addEventListener("click",N),t.appendChild(n),t.appendChild(i);const l=d("div","display:grid;grid-template-columns:340px 1fr;flex:1;overflow:hidden");l.appendChild(se()),l.appendChild(ve());const o=d("div","display:flex;justify-content:flex-end;gap:1rem;padding:12px 20px;border-top:1px solid var(--dm-border,#334155);flex-shrink:0"),m=d("button","padding:8px 18px;border-radius:8px;border:none;background:var(--dm-border,#334155);color:var(--dm-text-muted,#94a3b8);font-size:12px;font-weight:700;cursor:pointer");m.textContent="Cancel",m.addEventListener("click",N);const s=d("button","padding:8px 18px;border-radius:8px;border:none;background:var(--dm-primary,#6366f1);color:var(--dm-primary-text,#fff);font-size:12px;font-weight:700;cursor:pointer");s.textContent=F?"Save Timeline":"Insert Timeline",s.addEventListener("click",de),o.appendChild(m),o.appendChild(s),e.appendChild(t),e.appendChild(l),e.appendChild(o),T.appendChild(e),T.addEventListener("click",c=>{c.target===T&&N()}),document.body.appendChild(T),x()}function se(){const e=d("div","padding:18px;border-right:1px solid var(--dm-border,#334155);display:flex;flex-direction:column;gap:14px;overflow-y:auto");e.appendChild(ce()),e.appendChild(K()),e.appendChild(_("Layout",Y("layout",M.map(i=>[i,H(i)])))),e.appendChild(_("Theme",Y("theme",q.map(i=>[i,H(i)])))),e.appendChild(_("Mode",pe())),e.appendChild(K());const t=d("div","display:flex;flex-direction:column;gap:12px");t.id="tb-static",t.appendChild(ue()),e.appendChild(t);const n=d("div","display:flex;flex-direction:column;gap:12px");return n.id="tb-collection",n.appendChild(he()),e.appendChild(n),V(),e}function ce(){const e=d("div","display:flex;gap:6px"),t=(n,i)=>{const l=d("button"),o=()=>{const m=a.mode===n;l.style.cssText=`flex:1;padding:8px 10px;border-radius:8px;border:1px solid;font-size:12px;font-weight:700;cursor:pointer;${m?"border-color:var(--dm-primary,#6366f1);background:color-mix(in srgb, var(--dm-primary,#6366f1) 15%, transparent);color:var(--dm-primary,#a5b4fc)":"border-color:var(--dm-border,#334155);background:var(--dm-surface,#0f172a);color:var(--dm-text-muted,#94a3b8)"}`};return l.type="button",l.textContent=i,o(),l.addEventListener("click",()=>{a.mode=n,e.querySelectorAll("button").forEach(m=>m._paint&&m._paint()),V(),x()}),l._paint=o,l};return e.appendChild(t("static","Static")),e.appendChild(t("collection","From Collection")),e}function V(){const e=document.getElementById("tb-static"),t=document.getElementById("tb-collection");e&&(e.style.display=a.mode==="static"?"":"none"),t&&(t.style.display=a.mode==="collection"?"":"none")}function Y(e,t){const n=d("select",L+";cursor:pointer");return t.forEach(([i,l])=>{const o=document.createElement("option");o.value=i,o.textContent=l,i===a[e]&&(o.selected=!0),n.appendChild(o)}),n.addEventListener("change",()=>{a[e]=n.value,x()}),n}function pe(){const e=d("label","display:flex;align-items:center;gap:.5rem;cursor:pointer;font-size:12px;color:var(--dm-text,#e2e8f0)"),t=document.createElement("input");return t.type="checkbox",t.checked=!!a.roadmap,t.addEventListener("change",()=>{a.roadmap=t.checked,x()}),e.appendChild(t),e.appendChild(document.createTextNode("Roadmap mode")),e}function ue(){const e=d("div","display:flex;flex-direction:column;gap:8px"),t=d("div","display:flex;align-items:center;justify-content:space-between"),n=d("div",D);n.textContent="Events";const i=d("button","padding:4px 10px;border-radius:6px;border:1px dashed var(--dm-border,#334155);background:transparent;color:var(--dm-text-muted,#94a3b8);font-size:11px;cursor:pointer");i.type="button",ae(i,"plus","Add event",12),i.addEventListener("click",()=>{a.events.push({title:"New Event",date:"",status:"",icon:"",body:"",extras:{}}),A(),x()}),t.appendChild(n),t.appendChild(i),e.appendChild(t);const l=d("div","display:flex;flex-direction:column;gap:8px");return l.id="tb-events",e.appendChild(l),A(l),e}function A(e){const t=e||document.getElementById("tb-events");if(t){for(;t.firstChild;)t.removeChild(t.firstChild);a.events.forEach((n,i)=>t.appendChild(me(n,i)))}}function me(e,t){const n=d("div","border:1px solid var(--dm-border,#334155);border-radius:8px;padding:10px;background:var(--dm-background-alt,#0b1120);display:flex;flex-direction:column;gap:8px");n.draggable=!0,n.dataset.idx=String(t);const i=d("div","display:flex;align-items:center;gap:6px"),l=d("span","cursor:grab;color:var(--dm-text-muted,#475569);font-size:14px;user-select:none;padding:0 4px");z(l,"menu"),l.title="Drag to reorder",i.appendChild(l);const o=d("div","flex:1;font-size:12px;color:var(--dm-text,#e2e8f0);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis");o.textContent=e.title||"(untitled event)",i.appendChild(o);const m=(r,f,b,S)=>{const E=document.createElement("button");return E.type="button",E.className="btn btn-xs btn-ghost",E.style.cssText=`line-height:1;opacity:${S?".3":".6"};cursor:${S?"not-allowed":"pointer"};`,E.title=f,z(E,r),S||E.addEventListener("click",b),E};i.appendChild(m("arrow-up","Move up",()=>J(t,-1),t===0)),i.appendChild(m("arrow-down","Move down",()=>J(t,1),t===a.events.length-1)),i.appendChild(m("x","Delete",()=>fe(t),a.events.length<=1)),n.appendChild(i);const s=G(e.title,"Event title",r=>{e.title=r,o.textContent=r||"(untitled event)",x()});n.appendChild(B("Title",s));const c=d("div","display:grid;grid-template-columns:1fr 1fr;gap:6px"),u=document.createElement("input");u.type="date",u.value=e.date||"",u.style.cssText=L,u.addEventListener("input",()=>{e.date=u.value,x()}),c.appendChild(B("Date",u));const w=document.createElement("select");w.style.cssText=L+";cursor:pointer",ie.forEach(r=>{const f=document.createElement("option");f.value=r,f.textContent=r===""?"- none -":H(r),r===e.status&&(f.selected=!0),w.appendChild(f)}),w.addEventListener("change",()=>{e.status=w.value,x()}),c.appendChild(B("Status",w)),n.appendChild(c);const g=d("div","display:flex;gap:6px"),C=G(e.icon,"e.g. rocket",r=>{e.icon=r,x()});C.style.flex="1";const p=d("button","padding:0 10px;background:var(--dm-border,#334155);border:none;color:var(--dm-text-muted,#94a3b8);border-radius:7px;font-size:12px;cursor:pointer;flex-shrink:0");p.type="button",p.title="Browse icons",z(p,"star"),p.addEventListener("click",()=>X(r=>{e.icon=r,C.value=r,x()})),g.appendChild(C),g.appendChild(p),n.appendChild(B("Icon",g));const v=document.createElement("textarea");return v.value=e.body||"",v.rows=2,v.style.cssText=L+";resize:vertical;min-height:48px;line-height:1.5",v.addEventListener("input",()=>{e.body=v.value,x()}),n.appendChild(B("Body (Markdown)",v)),n.addEventListener("dragstart",r=>{r.dataTransfer.effectAllowed="move",r.dataTransfer.setData("text/plain",String(t)),n.style.opacity="0.5"}),n.addEventListener("dragend",()=>{n.style.opacity=""}),n.addEventListener("dragover",r=>{r.preventDefault(),r.dataTransfer.dropEffect="move",n.style.borderColor="var(--dm-primary,#6366f1)"}),n.addEventListener("dragleave",()=>{n.style.borderColor="var(--dm-border,#334155)"}),n.addEventListener("drop",r=>{r.preventDefault(),n.style.borderColor="var(--dm-border,#334155)";const f=parseInt(r.dataTransfer.getData("text/plain"),10);if(Number.isNaN(f)||f===t)return;const[b]=a.events.splice(f,1);a.events.splice(t,0,b),A(),x()}),n}function J(e,t){const n=e+t;if(n<0||n>=a.events.length)return;const[i]=a.events.splice(e,1);a.events.splice(n,0,i),A(),x()}function fe(e){a.events.length<=1||(a.events.splice(e,1),A(),x())}function G(e,t,n){const i=document.createElement("input");return i.type="text",i.value=e||"",i.placeholder=t||"",i.style.cssText=L,i.addEventListener("input",()=>n(i.value)),i}function B(e,t){const n=d("div","display:flex;flex-direction:column;gap:4px"),i=d("div","font-size:10px;font-weight:600;color:var(--dm-text-muted,#64748b);letter-spacing:.04em");return i.textContent=e,n.appendChild(i),n.appendChild(t),n}function he(){const e=d("div","display:flex;flex-direction:column;gap:10px"),t=d("select",L+";cursor:pointer"),n=document.createElement("option");n.value="",n.textContent="Loading collections\u2026",t.appendChild(n),e.appendChild(_("Collection",t));const i=O("titleField"),l=O("dateField"),o=O("statusField"),m=O("iconField"),s=O("bodyField");e.appendChild(_("Title field",i));const c=d("div","display:grid;grid-template-columns:1fr 1fr;gap:8px");c.appendChild(_("Date field",l)),c.appendChild(_("Status field",o)),e.appendChild(c),e.appendChild(_("Icon field",m)),e.appendChild(_("Body field",s));const u=G(a.collection.limit,"All",p=>{a.collection.limit=p,x()});u.type="number",u.min="1",e.appendChild(_("Limit (optional)",u));const w=d("div","font-size:11px;color:var(--dm-text-muted,#64748b);line-height:1.4");w.textContent="Picks entries in their default collection order. Set date, status, and icon fields to drive visual cues on the page.",e.appendChild(w);const g=p=>{[i,l,o,m,s].forEach(b=>{for(;b.firstChild;)b.removeChild(b.firstChild);const S=document.createElement("option");S.value="",S.textContent="- none -",b.appendChild(S),p.forEach(E=>{const R=document.createElement("option");R.value=E.name,R.textContent=E.label||E.name,b.appendChild(R)})});const r=new Set(p.map(b=>b.name)),f=(b,S)=>{a.collection[b]&&r.has(a.collection[b])||(a.collection[b]=r.has(S)?S:"")};f("titleField","title"),f("dateField","date"),f("statusField","status"),f("iconField","icon"),f("bodyField","description"),i.value=a.collection.titleField,l.value=a.collection.dateField,o.value=a.collection.statusField,m.value=a.collection.iconField,s.value=a.collection.bodyField},C=async p=>{if(!p){g([]);return}try{const v=k.onGetCollection?await k.onGetCollection(p):null,r=v&&v.fields||[];g(r)}catch{g([])}};return t.addEventListener("change",()=>{a.collection.slug=t.value,C(t.value).then(x)}),(async()=>{let p=[];try{p=k.onListCollections?await k.onListCollections():[]}catch{p=[]}for(;t.firstChild;)t.removeChild(t.firstChild);if(!p.length){const r=document.createElement("option");r.value="",r.textContent="No collections available",t.appendChild(r),g([]);return}p.forEach(r=>{const f=document.createElement("option");f.value=r.slug,f.textContent=r.title||r.slug,t.appendChild(f)});const v=a.collection.slug;v&&p.some(r=>r.slug===v)?t.value=v:(a.collection.slug=p[0].slug,t.value=p[0].slug),await C(t.value),x()})(),e}function O(e){const t=d("select",L+";cursor:pointer"),n=document.createElement("option");return n.value="",n.textContent="- none -",t.appendChild(n),t.addEventListener("change",()=>{a.collection[e]=t.value,x()}),t}function ve(){const e=d("div","padding:20px;background:var(--dm-surface,#0f172a);display:flex;flex-direction:column;gap:14px;overflow-y:auto"),t=d("div",D);t.textContent="Live Preview";const n=d("div","flex:1;min-height:160px;background:var(--dm-background-alt,#fff);border-radius:10px;padding:16px;color:var(--dm-text,#0f172a)");n.id="tb-preview-area";const i=d("div",D+";margin-bottom:4px");i.textContent="Shortcode";const l=d("pre","background:var(--dm-background-alt,#1e293b);border:1px solid var(--dm-border,#334155);border-radius:8px;padding:10px 14px;font-family:monospace;font-size:11px;color:var(--dm-primary,#a5b4fc);line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:160px;overflow:auto;margin:0");return l.id="tb-snippet",e.appendChild(t),e.appendChild(n),e.appendChild(i),e.appendChild(l),e}function x(){const e=document.getElementById("tb-snippet");e&&(e.textContent=W()),xe()}function xe(){$&&clearTimeout($),$=setTimeout(ge,300)}async function ge(){const e=document.getElementById("tb-preview-area");if(!e)return;const t=W(),n=++U;be(e);try{const i=k.onPreview?await k.onPreview(t):null;if(n!==U)return;if(!i||typeof i.html!="string"){Q(e);return}Z(e,i.html),typeof I<"u"&&I.scan&&I.scan(e)}catch{if(n!==U)return;Q(e)}}function be(e){if(!e.dataset.loaded){for(;e.firstChild;)e.removeChild(e.firstChild);const t=d("div","color:var(--dm-text-muted,#94a3b8);font-size:12px");t.textContent="Rendering preview\u2026",e.appendChild(t),e.dataset.loaded="1"}}function Q(e){for(;e.firstChild;)e.removeChild(e.firstChild);const t=d("div","color:var(--dm-text-muted,#94a3b8);font-size:12px;font-style:italic");t.textContent="Preview unavailable",e.appendChild(t)}function H(e){return e.charAt(0).toUpperCase()+e.slice(1)}
6
+ [/timeline]`}function de(){const e=a,t=e.collection,n=[`slug="${h(t.slug)}"`,'display="timeline"'];return e.layout!=="vertical"&&n.push(`layout="${h(e.layout)}"`),e.theme!=="minimal"&&n.push(`theme="${h(e.theme)}"`),e.roadmap&&n.push('mode="roadmap"'),t.titleField&&n.push(`title-field="${h(t.titleField)}"`),t.dateField&&n.push(`date-field="${h(t.dateField)}"`),t.statusField&&n.push(`status-field="${h(t.statusField)}"`),t.iconField&&n.push(`icon-field="${h(t.iconField)}"`),t.bodyField&&n.push(`body-field="${h(t.bodyField)}"`),t.limit&&n.push(`limit="${h(t.limit)}"`),Object.entries(e.extras).forEach(([i,l])=>n.push(`${i}="${h(l)}"`)),`[collection ${n.join(" ")} /]`}function N(){$&&(clearTimeout($),$=null),T&&(T.remove(),T=null,a=null,F=null)}function ae(){const e=W();if(F){const{start:t,end:n}=F;y.value=y.value.slice(0,t)+e+y.value.slice(n),y.selectionStart=y.selectionEnd=t+e.length}else{const t=y.selectionStart,n=y.selectionEnd;y.value=y.value.slice(0,t)+e+y.value.slice(n),y.selectionStart=y.selectionEnd=t+e.length}y.dispatchEvent(new Event("input",{bubbles:!0})),y.focus(),N()}function d(e,t=""){const n=document.createElement(e);return t&&(n.style.cssText=t),n}function z(e,t,n=14){e.textContent="";const i=document.createElement("span");return i.setAttribute("data-icon",t),i.setAttribute("data-icon-size",String(n)),i.style.cssText="display:inline-flex;align-items:center;vertical-align:middle;",e.appendChild(i),typeof I<"u"&&I.scan&&I.scan(e),e}function re(e,t,n,i=14){return z(e,t,i),e.appendChild(document.createTextNode(" "+n)),e}const L="padding:7px 10px;border-radius:7px;border:1px solid var(--dm-border,#334155);background:var(--dm-surface,#0f172a);color:var(--dm-text,#e2e8f0);font-size:12px;font-family:inherit;width:100%",O="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--dm-text-muted,#475569)";function _(e,t){const n=d("div","display:flex;flex-direction:column;gap:5px"),i=d("div",O);return i.textContent=e,n.appendChild(i),n.appendChild(t),n}function K(){return d("div","height:1px;background:var(--dm-border,#334155);margin:2px 0")}function se(){T=d("div","position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:9999;padding:24px;isolation:isolate");const e=d("div","background:var(--dm-surface,#1e293b);border:1px solid var(--dm-border,#334155);border-radius:14px;width:100%;max-width:960px;max-height:90vh;display:flex;flex-direction:column;box-shadow:var(--dm-shadow-lg);overflow:hidden"),t=d("div","display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--dm-border,#334155);flex-shrink:0"),n=d("span","font-weight:700;color:var(--dm-text,#f1f5f9);font-size:14px");n.textContent=F?"Edit Timeline":"Insert Timeline";const i=d("button","background:var(--dm-border,#334155);border:none;color:var(--dm-text-muted,#94a3b8);border-radius:6px;width:26px;height:26px;cursor:pointer;font-size:14px");i.title="Close",z(i,"x"),i.addEventListener("click",N),t.appendChild(n),t.appendChild(i);const l=d("div","display:grid;grid-template-columns:340px 1fr;flex:1;overflow:hidden");l.appendChild(ce()),l.appendChild(xe());const o=d("div","display:flex;justify-content:flex-end;gap:1rem;padding:12px 20px;border-top:1px solid var(--dm-border,#334155);flex-shrink:0"),m=d("button","padding:8px 18px;border-radius:8px;border:none;background:var(--dm-border,#334155);color:var(--dm-text-muted,#94a3b8);font-size:12px;font-weight:700;cursor:pointer");m.textContent="Cancel",m.addEventListener("click",N);const s=d("button","padding:8px 18px;border-radius:8px;border:none;background:var(--dm-primary,#6366f1);color:var(--dm-primary-text,#fff);font-size:12px;font-weight:700;cursor:pointer");s.textContent=F?"Save Timeline":"Insert Timeline",s.addEventListener("click",ae),o.appendChild(m),o.appendChild(s),e.appendChild(t),e.appendChild(l),e.appendChild(o),T.appendChild(e),T.addEventListener("click",c=>{c.target===T&&N()}),document.body.appendChild(T),x()}function ce(){const e=d("div","padding:18px;border-right:1px solid var(--dm-border,#334155);display:flex;flex-direction:column;gap:14px;overflow-y:auto");e.appendChild(pe()),e.appendChild(K()),e.appendChild(_("Layout",Y("layout",M.map(i=>[i,H(i)])))),e.appendChild(_("Theme",Y("theme",q.map(i=>[i,H(i)])))),e.appendChild(_("Mode",ue())),e.appendChild(K());const t=d("div","display:flex;flex-direction:column;gap:12px");t.id="tb-static",t.appendChild(me()),e.appendChild(t);const n=d("div","display:flex;flex-direction:column;gap:12px");return n.id="tb-collection",n.appendChild(ve()),e.appendChild(n),V(),e}function pe(){const e=d("div","display:flex;gap:6px"),t=(n,i)=>{const l=d("button"),o=()=>{const m=a.mode===n;l.style.cssText=`flex:1;padding:8px 10px;border-radius:8px;border:1px solid;font-size:12px;font-weight:700;cursor:pointer;${m?"border-color:var(--dm-primary,#6366f1);background:color-mix(in srgb, var(--dm-primary,#6366f1) 15%, transparent);color:var(--dm-primary,#a5b4fc)":"border-color:var(--dm-border,#334155);background:var(--dm-surface,#0f172a);color:var(--dm-text-muted,#94a3b8)"}`};return l.type="button",l.textContent=i,o(),l.addEventListener("click",()=>{a.mode=n,e.querySelectorAll("button").forEach(m=>m._paint&&m._paint()),V(),x()}),l._paint=o,l};return e.appendChild(t("static","Static")),e.appendChild(t("collection","From Collection")),e}function V(){const e=document.getElementById("tb-static"),t=document.getElementById("tb-collection");e&&(e.style.display=a.mode==="static"?"":"none"),t&&(t.style.display=a.mode==="collection"?"":"none")}function Y(e,t){const n=d("select",L+";cursor:pointer");return t.forEach(([i,l])=>{const o=document.createElement("option");o.value=i,o.textContent=l,i===a[e]&&(o.selected=!0),n.appendChild(o)}),n.addEventListener("change",()=>{a[e]=n.value,x()}),n}function ue(){const e=d("label","display:flex;align-items:center;gap:.5rem;cursor:pointer;font-size:12px;color:var(--dm-text,#e2e8f0)"),t=document.createElement("input");return t.type="checkbox",t.checked=!!a.roadmap,t.addEventListener("change",()=>{a.roadmap=t.checked,x()}),e.appendChild(t),e.appendChild(document.createTextNode("Roadmap mode")),e}function me(){const e=d("div","display:flex;flex-direction:column;gap:8px"),t=d("div","display:flex;align-items:center;justify-content:space-between"),n=d("div",O);n.textContent="Events";const i=d("button","padding:4px 10px;border-radius:6px;border:1px dashed var(--dm-border,#334155);background:transparent;color:var(--dm-text-muted,#94a3b8);font-size:11px;cursor:pointer");i.type="button",re(i,"plus","Add event",12),i.addEventListener("click",()=>{a.events.push({title:"New Event",date:"",status:"",icon:"",body:"",extras:{}}),A(),x()}),t.appendChild(n),t.appendChild(i),e.appendChild(t);const l=d("div","display:flex;flex-direction:column;gap:8px");return l.id="tb-events",e.appendChild(l),A(l),e}function A(e){const t=e||document.getElementById("tb-events");if(t){for(;t.firstChild;)t.removeChild(t.firstChild);a.events.forEach((n,i)=>t.appendChild(fe(n,i)))}}function fe(e,t){const n=d("div","border:1px solid var(--dm-border,#334155);border-radius:8px;padding:10px;background:var(--dm-background-alt,#0b1120);display:flex;flex-direction:column;gap:8px");n.draggable=!0,n.dataset.idx=String(t);const i=d("div","display:flex;align-items:center;gap:6px"),l=d("span","cursor:grab;color:var(--dm-text-muted,#475569);font-size:14px;user-select:none;padding:0 4px");z(l,"menu"),l.title="Drag to reorder",i.appendChild(l);const o=d("div","flex:1;font-size:12px;color:var(--dm-text,#e2e8f0);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis");o.textContent=e.title||"(untitled event)",i.appendChild(o);const m=(r,f,b,S)=>{const E=document.createElement("button");return E.type="button",E.className="btn btn-xs btn-ghost",E.style.cssText=`line-height:1;opacity:${S?".3":".6"};cursor:${S?"not-allowed":"pointer"};`,E.title=f,z(E,r),S||E.addEventListener("click",b),E};i.appendChild(m("arrow-up","Move up",()=>J(t,-1),t===0)),i.appendChild(m("arrow-down","Move down",()=>J(t,1),t===a.events.length-1)),i.appendChild(m("x","Delete",()=>he(t),a.events.length<=1)),n.appendChild(i);const s=G(e.title,"Event title",r=>{e.title=r,o.textContent=r||"(untitled event)",x()});n.appendChild(B("Title",s));const c=d("div","display:grid;grid-template-columns:1fr 1fr;gap:6px"),u=document.createElement("input");u.type="date",u.value=e.date||"",u.style.cssText=L,u.addEventListener("input",()=>{e.date=u.value,x()}),c.appendChild(B("Date",u));const w=document.createElement("select");w.style.cssText=L+";cursor:pointer",oe.forEach(r=>{const f=document.createElement("option");f.value=r,f.textContent=r===""?"- none -":H(r),r===e.status&&(f.selected=!0),w.appendChild(f)}),w.addEventListener("change",()=>{e.status=w.value,x()}),c.appendChild(B("Status",w)),n.appendChild(c);const g=d("div","display:flex;gap:6px"),C=G(e.icon,"e.g. rocket",r=>{e.icon=r,x()});C.style.flex="1";const p=d("button","padding:0 10px;background:var(--dm-border,#334155);border:none;color:var(--dm-text-muted,#94a3b8);border-radius:7px;font-size:12px;cursor:pointer;flex-shrink:0");p.type="button",p.title="Browse icons",z(p,"star"),p.addEventListener("click",()=>X(r=>{e.icon=r,C.value=r,x()})),g.appendChild(C),g.appendChild(p),n.appendChild(B("Icon",g));const v=document.createElement("textarea");return v.value=e.body||"",v.rows=2,v.style.cssText=L+";resize:vertical;min-height:48px;line-height:1.5",v.addEventListener("input",()=>{e.body=v.value,x()}),n.appendChild(B("Body (Markdown)",v)),n.addEventListener("dragstart",r=>{r.dataTransfer.effectAllowed="move",r.dataTransfer.setData("text/plain",String(t)),n.style.opacity="0.5"}),n.addEventListener("dragend",()=>{n.style.opacity=""}),n.addEventListener("dragover",r=>{r.preventDefault(),r.dataTransfer.dropEffect="move",n.style.borderColor="var(--dm-primary,#6366f1)"}),n.addEventListener("dragleave",()=>{n.style.borderColor="var(--dm-border,#334155)"}),n.addEventListener("drop",r=>{r.preventDefault(),n.style.borderColor="var(--dm-border,#334155)";const f=parseInt(r.dataTransfer.getData("text/plain"),10);if(Number.isNaN(f)||f===t)return;const[b]=a.events.splice(f,1);a.events.splice(t,0,b),A(),x()}),n}function J(e,t){const n=e+t;if(n<0||n>=a.events.length)return;const[i]=a.events.splice(e,1);a.events.splice(n,0,i),A(),x()}function he(e){a.events.length<=1||(a.events.splice(e,1),A(),x())}function G(e,t,n){const i=document.createElement("input");return i.type="text",i.value=e||"",i.placeholder=t||"",i.style.cssText=L,i.addEventListener("input",()=>n(i.value)),i}function B(e,t){const n=d("div","display:flex;flex-direction:column;gap:4px"),i=d("div","font-size:10px;font-weight:600;color:var(--dm-text-muted,#64748b);letter-spacing:.04em");return i.textContent=e,n.appendChild(i),n.appendChild(t),n}function ve(){const e=d("div","display:flex;flex-direction:column;gap:10px"),t=d("select",L+";cursor:pointer"),n=document.createElement("option");n.value="",n.textContent="Loading collections\u2026",t.appendChild(n),e.appendChild(_("Collection",t));const i=D("titleField"),l=D("dateField"),o=D("statusField"),m=D("iconField"),s=D("bodyField");e.appendChild(_("Title field",i));const c=d("div","display:grid;grid-template-columns:1fr 1fr;gap:8px");c.appendChild(_("Date field",l)),c.appendChild(_("Status field",o)),e.appendChild(c),e.appendChild(_("Icon field",m)),e.appendChild(_("Body field",s));const u=G(a.collection.limit,"All",p=>{a.collection.limit=p,x()});u.type="number",u.min="1",e.appendChild(_("Limit (optional)",u));const w=d("div","font-size:11px;color:var(--dm-text-muted,#64748b);line-height:1.4");w.textContent="Picks entries in their default collection order. Set date, status, and icon fields to drive visual cues on the page.",e.appendChild(w);const g=p=>{[i,l,o,m,s].forEach(b=>{for(;b.firstChild;)b.removeChild(b.firstChild);const S=document.createElement("option");S.value="",S.textContent="- none -",b.appendChild(S),p.forEach(E=>{const R=document.createElement("option");R.value=E.name,R.textContent=E.label||E.name,b.appendChild(R)})});const r=new Set(p.map(b=>b.name)),f=(b,S)=>{a.collection[b]&&r.has(a.collection[b])||(a.collection[b]=r.has(S)?S:"")};f("titleField","title"),f("dateField","date"),f("statusField","status"),f("iconField","icon"),f("bodyField","description"),i.value=a.collection.titleField,l.value=a.collection.dateField,o.value=a.collection.statusField,m.value=a.collection.iconField,s.value=a.collection.bodyField},C=async p=>{if(!p){g([]);return}try{const v=k.onGetCollection?await k.onGetCollection(p):null,r=v&&v.fields||[];g(r)}catch{g([])}};return t.addEventListener("change",()=>{a.collection.slug=t.value,C(t.value).then(x)}),(async()=>{let p=[];try{p=k.onListCollections?await k.onListCollections():[]}catch{p=[]}for(;t.firstChild;)t.removeChild(t.firstChild);if(!p.length){const r=document.createElement("option");r.value="",r.textContent="No collections available",t.appendChild(r),g([]);return}p.forEach(r=>{const f=document.createElement("option");f.value=r.slug,f.textContent=r.title||r.slug,t.appendChild(f)});const v=a.collection.slug;v&&p.some(r=>r.slug===v)?t.value=v:(a.collection.slug=p[0].slug,t.value=p[0].slug),await C(t.value),x()})(),e}function D(e){const t=d("select",L+";cursor:pointer"),n=document.createElement("option");return n.value="",n.textContent="- none -",t.appendChild(n),t.addEventListener("change",()=>{a.collection[e]=t.value,x()}),t}function xe(){const e=d("div","padding:20px;background:var(--dm-surface,#0f172a);display:flex;flex-direction:column;gap:14px;overflow-y:auto"),t=d("div",O);t.textContent="Live Preview";const n=d("div","flex:1;min-height:160px;background:var(--dm-background-alt,#fff);border-radius:10px;padding:16px;color:var(--dm-text,#0f172a)");n.id="tb-preview-area";const i=d("div",O+";margin-bottom:4px");i.textContent="Shortcode";const l=d("pre","background:var(--dm-background-alt,#1e293b);border:1px solid var(--dm-border,#334155);border-radius:8px;padding:10px 14px;font-family:monospace;font-size:11px;color:var(--dm-primary,#a5b4fc);line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:160px;overflow:auto;margin:0");return l.id="tb-snippet",e.appendChild(t),e.appendChild(n),e.appendChild(i),e.appendChild(l),e}function x(){const e=document.getElementById("tb-snippet");e&&(e.textContent=W()),ge()}function ge(){$&&clearTimeout($),$=setTimeout(be,300)}async function be(){const e=document.getElementById("tb-preview-area");if(!e)return;const t=W(),n=++U;ye(e);try{const i=k.onPreview?await k.onPreview(t):null;if(n!==U)return;if(!i||typeof i.html!="string"){Q(e);return}Z(e,i.html),typeof I<"u"&&I.scan&&I.scan(e)}catch{if(n!==U)return;Q(e)}}function ye(e){if(!e.dataset.loaded){for(;e.firstChild;)e.removeChild(e.firstChild);const t=d("div","color:var(--dm-text-muted,#94a3b8);font-size:12px");t.textContent="Rendering preview\u2026",e.appendChild(t),e.dataset.loaded="1"}}function Q(e){for(;e.firstChild;)e.removeChild(e.firstChild);const t=d("div","color:var(--dm-text-muted,#94a3b8);font-size:12px;font-style:italic");t.textContent="Preview unavailable",e.appendChild(t)}function H(e){return e.charAt(0).toUpperCase()+e.slice(1)}
@@ -45,7 +45,7 @@
45
45
  </div>
46
46
  <div>
47
47
  <label class="form-label" for="action-slug">Slug
48
- <span data-help-title="Slug" data-help="The action's id - in its admin address and in the [transitions] shortcode. Left blank, it is made from the title. It cannot be changed once the action exists."></span></label>
48
+ <span data-help-title="Slug" data-help="The action's id - in its admin address and in API calls. Left blank, it is made from the title. It cannot be changed once the action exists."></span></label>
49
49
  <input id="action-slug" type="text" class="form-input aex-mono" maxlength="80" data-model="slug.value" data-bind-disabled="editing" data-bind-placeholder="slugHint.value">
50
50
  <small class="aex-err" data-bind-hidden="!err.value.slug" data-bind-text="err.value.slug"></small>
51
51
  </div>
@@ -81,7 +81,7 @@
81
81
  <div class="tab-panel">
82
82
  <div class="aex-card">
83
83
  <div class="aex-card-title">The button
84
- <span data-help-title="Trigger" data-help="How the action is started. Manual actions appear as a button on each entry of the collection (and in the [transitions] shortcode when they have a transition)."></span></div>
84
+ <span data-help-title="Trigger" data-help="How the action is started. Manual actions appear as a button on each entry of the collection (and, when they have a transition, on the site in an interactive [collection] with the transitions attribute)."></span></div>
85
85
  <div class="aex-grid">
86
86
  <div>
87
87
  <label class="form-label" for="action-trigger-type">Trigger</label>
@@ -113,7 +113,7 @@
113
113
  <div class="aex-card">
114
114
  <div class="aex-setting">
115
115
  <label class="form-label" for="action-transition-enabled">State transition (optional)
116
- <span data-help-title="State transition" data-help="Makes the action a step in a workflow: it is offered only while the entry's state field holds one of the From values (the Collection Browser and the [transitions] shortcode list the transitions available for each entry). To is what the steps are expected to set it to - informational; an Update a field step does the writing."></span></label>
116
+ <span data-help-title="State transition" data-help="Makes the action a step in a workflow: it is offered only while the entry's state field holds one of the From values (the Collection Browser - an interactive [collection] with the transitions attribute - lists the transitions available for each entry). To is what the steps are expected to set it to - informational; an Update a field step does the writing."></span></label>
117
117
  <input id="action-transition-enabled" type="checkbox" class="form-switch" data-model="transitionEnabled.value">
118
118
  </div>
119
119
  <div class="aex-grid" data-bind-hidden="!transitionEnabled.value">
@@ -146,6 +146,7 @@
146
146
  <option value="updateField">Update a field</option>
147
147
  <option value="deleteEntry">Delete the entry</option>
148
148
  <option value="moveToCollection">Move to another collection</option>
149
+ <option value="createInCollection">Create an entry in another collection</option>
149
150
  <option value="webhook">Call a webhook</option>
150
151
  <option value="email">Send an email</option>
151
152
  <option value="notify">Notify the admins</option>
@@ -167,14 +168,14 @@
167
168
  <div class="tab-panel">
168
169
  <div class="aex-card">
169
170
  <div class="aex-card-title">Who can run it
170
- <span data-help-title="Allowed roles" data-help="A role here lets its holders run the action - and everyone in a more senior role too (the check compares role levels). Roles come from System › Roles; one the action names that no longer exists is kept and flagged."></span></div>
171
+ <span data-help-title="Allowed roles" data-help="Any role ticked here is enough: its holders may run the action, and so may everyone in a more senior role (the same rule as page and view visibility). Ticking admin and hr lets both in. Roles come from System › Roles; one the action names that no longer exists is kept and flagged, and admits nobody but the level-0 role."></span></div>
171
172
  <div class="aex-roles" data-each="roleRows.value key=name"></div>
172
173
  <small class="aex-err" data-bind-hidden="!err.value.roles" data-bind-text="err.value.roles"></small>
173
174
  </div>
174
175
  <div class="aex-card">
175
176
  <div class="aex-setting">
176
177
  <label class="form-label" for="action-rowlevel-enabled">Only on some entries
177
- <span data-help-title="Row-level access" data-help="Limits a user to entries they own (they created it) or that name them in a field (Field match). Admins always bypass this."></span></label>
178
+ <span data-help-title="Row-level access" data-help="Limits a user to entries they own (they created it) or that name them in a field (Field match). It is checked on the server whenever the action runs. The level-0 role always bypasses this."></span></label>
178
179
  <input id="action-rowlevel-enabled" type="checkbox" class="form-switch" data-model="rowLevelEnabled.value">
179
180
  </div>
180
181
  <div class="aex-grid" data-bind-hidden="!rowLevelEnabled.value">
@@ -4,7 +4,7 @@
4
4
  <div class="acx">
5
5
  <p class="acx-note">Buttons on a collection's entries that run steps - update a field, move or create an entry,
6
6
  call a webhook, send an email.
7
- <span data-help-title="Actions" data-help="An action belongs to one collection and shows as a button on its entries (in the admin's entry list, and on the site with the [transitions] shortcode). Its steps run in order. With a transition set, it only appears while the entry's field has one of its 'from' values, and moves it to 'to'. Roles decide who may run it."></span></p>
7
+ <span data-help-title="Actions" data-help="An action belongs to one collection and shows as a button on its entries (in the admin's entry list, and on the site in an interactive [collection] with the transitions attribute). Its steps run in order. With a transition set, it only appears while the entry's field has one of its 'from' values, and moves it to 'to'. Roles decide who may run it."></span></p>
8
8
 
9
9
  <!-- File mode: actions live in MongoDB. A state of the site, explained, not an error. -->
10
10
  <div class="acx-card acx-pro" data-bind-hidden="!needsMongo.value">
@@ -24,7 +24,7 @@
24
24
  <div class="ct-chips-row">
25
25
  <div class="ct-chips" data-each="chips.value key=key"></div>
26
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>
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. Groups are shared: everyone who uses Contacts has the same list, so renaming or deleting one changes it for them too - their contacts themselves stay private to them. That is why only a group's creator, or someone whose role has Manage groups in Contacts, can rename or delete it."></span>
28
28
  </div>
29
29
 
30
30
  <div class="ct-bulk" data-if="hasSelection.value">
@@ -69,77 +69,103 @@
69
69
  <div class="col-12">
70
70
  <div class="docs-body">
71
71
 
72
- <p>Actions require a MongoDB connection. All admin endpoints require authentication and the
73
- <code>actions</code> permission. Action configs are stored in <code>cms__actions</code>.</p>
74
-
75
- <h3 style="margin-top:16px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">
76
- Admin Endpoints</h3>
77
-
78
- <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/actions</span>
79
- </h3>
80
- <p class="auth-note">Requires: <code>actions</code> permission</p>
81
- <p>List all action configs.</p>
82
-
83
- <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/actions</span>
84
- </h3>
85
- <p class="auth-note">Requires: <code>actions</code> permission</p>
86
- <p>Create a new action config. Returns <code>201</code> on success.</p>
87
- <pre class="code-block"><code>{
72
+ <p>Actions need a MongoDB connection (a Pro feature): action configs are kept in the <code>cms__actions</code>
73
+ collection on the <code>default</code> connection, and without it these endpoints answer 503. The admin endpoints
74
+ need the <code>actions</code> permission for the action named (read, create, update, delete).</p>
75
+
76
+ <h3 style="margin-top:16px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">
77
+ Admin Endpoints</h3>
78
+
79
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/actions</span></h3>
80
+ <p class="auth-note">Requires: <code>actions</code> read permission</p>
81
+ <p>Every action config.</p>
82
+
83
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/actions</span></h3>
84
+ <p class="auth-note">Requires: <code>actions</code> create permission</p>
85
+ <p>Create an action. Answers <code>201</code>. Step types: <code>updateField</code>, <code>deleteEntry</code>,
86
+ <code>moveToCollection</code>, <code>createInCollection</code>, <code>webhook</code>, <code>email</code>,
87
+ <code>notify</code> (see Documentation &gt; Usage &gt; Actions). <code>manual</code> is the only trigger.</p>
88
+ <pre class="code-block"><code>{
88
89
  "title": "Approve Application",
89
90
  "slug": "approve-application",
90
91
  "collection": "applications",
91
92
  "trigger": { "type": "manual", "label": "Approve", "icon": "check-circle", "confirmMessage": "Approve this application?" },
93
+ "transition": { "field": "status", "from": ["submitted", "reviewing"], "to": "approved" },
92
94
  "steps": [
93
95
  { "type": "updateField", "config": { "field": "status", "value": "approved" } },
94
- { "type": "updateField", "config": { "field": "approvedAt", "value": "{{now}}" } },
95
- { "type": "email", "config": { "to": "{{entry.data.email}}", "subject": "Application approved",
96
- "template": "Hi {{entry.data.name}}, your application is approved." } }
96
+ { "type": "updateField", "config": { "field": "approvedAt", "value": "&#123;&#123;now&#125;&#125;" } },
97
+ { "type": "email", "config": { "to": "&#123;&#123;entry.data.email&#125;&#125;", "subject": "Application approved",
98
+ "template": "Hi &#123;&#123;entry.data.name&#125;&#125;, your application is approved." } }
97
99
  ],
98
- "access": { "roles": ["admin", "manager"] }
100
+ "access": { "roles": ["admin"], "rowLevel": null }
99
101
  }</code></pre>
100
-
101
- <h3><span class="method-badge method-get">GET</span><span
102
- class="endpoint-path">/api/actions/:slug</span></h3>
103
- <p class="auth-note">Requires: <code>actions</code> permission</p>
104
- <p>Return a single action config by slug.</p>
105
-
106
- <h3><span class="method-badge method-put">PUT</span><span
107
- class="endpoint-path">/api/actions/:slug</span></h3>
108
- <p class="auth-note">Requires: <code>actions</code> permission</p>
109
- <p>Update an action config.</p>
110
-
111
- <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/actions/:slug</span>
112
- </h3>
113
- <p class="auth-note">Requires: <code>actions</code> permission</p>
114
- <p>Delete an action config.</p>
115
-
116
- <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/actions/:slug/execute</span>
117
- </h3>
118
- <p class="auth-note">Requires: <code>actions</code> permission</p>
119
- <p>Execute an action against a specific entry.</p>
120
- <pre class="code-block"><code>// Request body
102
+ <p><strong>Who may run it.</strong> Any role in <code>access.roles</code> is enough: its holders may run the action,
103
+ and so may everyone in a more senior role, across all the roles a user holds - the page and view visibility
104
+ rule. An action naming no roles is for admins (levels 0 and 1); a role name the site does not have admits only
105
+ the level-0 role. <code>access.rowLevel</code> (<code>{"mode": "owner"}</code>,
106
+ <code>{"mode": "field", "field": "assignedTo"}</code> or <code>reference</code>) limits a user to the rows they
107
+ own; the level-0 role bypasses it.</p>
108
+ <p><strong>Transitions.</strong> With <code>transition</code> set, the action only runs while the entry's
109
+ <code>field</code> holds one of the <code>from</code> values. <code>to</code> is informational - an
110
+ <code>updateField</code> step does the writing.</p>
111
+
112
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/actions/:slug</span></h3>
113
+ <p class="auth-note">Requires: <code>actions</code> read permission</p>
114
+ <p>One action config.</p>
115
+
116
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/actions/:slug</span></h3>
117
+ <p class="auth-note">Requires: <code>actions</code> update permission</p>
118
+ <p>Update an action.</p>
119
+
120
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/actions/:slug</span></h3>
121
+ <p class="auth-note">Requires: <code>actions</code> delete permission</p>
122
+ <p>Delete an action.</p>
123
+
124
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/actions/:slug/execute</span></h3>
125
+ <p class="auth-note">Requires: <code>actions</code> read permission</p>
126
+ <p>Run an action on one entry (the buttons on the admin's entry list). The row-level rule is checked (403
127
+ <code>Row-level access denied</code>), then the transition (409 when the entry has moved on). Steps run in
128
+ order and stop at the first failure; steps already run are not undone.</p>
129
+ <pre class="code-block"><code>// Request body
121
130
  { "entryId": "uuid-of-the-entry" }
122
-
123
131
  // Response
124
- { "success": true, "stepsCompleted": 4, "results": [ { "type": "updateField", "success": true, ... } ] }
125
-
132
+ { "success": true, "stepsCompleted": 3, "results": [ { "type": "updateField", "success": true, "result": { ... } }, ... ] }
126
133
  // Partial failure
127
- { "success": false, "stepsCompleted": 2, "results": [ ..., { "type": "webhook", "success": false, "error": "Webhook returned HTTP 500" } ] }</code></pre>
128
-
129
- <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/actions/collection/:slug</span>
130
- </h3>
131
- <p class="auth-note">Requires: <code>actions</code> permission</p>
132
- <p>List all action configs targeting a given collection slug. Used by the entry list view to populate
133
- per-row trigger buttons.</p>
134
-
135
- <h3 style="margin-top:24px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">
136
- Public Endpoint</h3>
137
-
138
- <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/actions/:slug/public</span>
139
- </h3>
140
- <p class="auth-note">Requires: JWT + role in <code>access.roles</code></p>
141
- <p>Execute an action publicly. Always requires a valid JWT - the role is checked against <code>access.roles</code>.
142
- Request body and response shape are identical to the admin <code>/execute</code> endpoint.</p>
134
+ { "success": false, "stepsCompleted": 2, "results": [ ..., { "type": "webhook", "success": false, "error": "Webhook returned HTTP 500" } ] }
135
+ // Error 409 - the transition no longer applies
136
+ { "error": "Cannot apply \"Approve Application\" - current status is \"rejected\", allowed: submitted, reviewing" }</code></pre>
137
+
138
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/actions/:slug/check-access</span></h3>
139
+ <p class="auth-note">Requires: <code>actions</code> read permission</p>
140
+ <p>Which of the given entries the row-level rule lets you run the action on.</p>
141
+ <pre class="code-block"><code>// Request body
142
+ { "entryIds": ["id-1", "id-2"] }
143
+ // Response
144
+ { "allowed": ["id-1"] }</code></pre>
145
+
146
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/actions/collection/:slug</span></h3>
147
+ <p class="auth-note">Requires: <code>actions</code> read permission</p>
148
+ <p>The actions on a collection - the per-row buttons of its entry list.</p>
149
+
150
+ <h3 style="margin-top:24px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">
151
+ Public Endpoints</h3>
152
+
153
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/actions/:slug/public</span></h3>
154
+ <p class="auth-note">Requires: a signed-in user (JWT) whom <code>access.roles</code> admits</p>
155
+ <p>Run an action from the public site - <code>[cta]</code>, a <code>[collection]</code> with <code>cta=</code>, or a
156
+ transition button. 401 without a token, 403 when the roles do not admit the user. The body and response are the
157
+ same as <code>/execute</code>, plus an optional <code>scope: "mine"</code>, which a
158
+ <code>[collection scope="mine"]</code> block sends: a row the user did not create is then refused with 403,
159
+ before the transition check (409).</p>
160
+
161
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/actions/transitions</span></h3>
162
+ <p class="auth-note">Authentication optional; anonymous callers get none</p>
163
+ <p>The transitions a user can take on one entry right now - what an interactive
164
+ <code>[collection ... transitions]</code> shows as buttons. Query: <code>collection</code>, <code>entryId</code>,
165
+ and <code>scope=mine</code> from a "my entries" block. Only actions whose <code>from</code> matches the entry's
166
+ state, whose roles admit the user and whose row-level rule passes are listed.</p>
167
+ <pre class="code-block"><code>// Response
168
+ { "transitions": [ { "slug": "withdraw-application", "title": "Withdraw", "trigger": { ... }, "transition": { ... } } ] }</code></pre>
143
169
 
144
170
  </div>
145
171
  </div>