domma-cms 0.69.4 → 0.70.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.
@@ -1,9 +1,9 @@
1
1
  /*!
2
- * Domma Tools CSS v0.44.0
2
+ * Domma Tools CSS v0.44.1
3
3
  * Dynamic Object Manipulation & Modeling API
4
4
  * (c) 2026 Darryl Waterhouse & DCBW-IT
5
- * Built: 2026-09-22T03:17:43.206Z
6
- * Commit: 4c4889f
5
+ * Built: 2026-09-23T16:21:30.188Z
6
+ * Commit: eca0079
7
7
  */
8
8
 
9
9
  /*!
@@ -1,9 +1,9 @@
1
1
  /*!
2
- * Domma Tools v0.44.0
2
+ * Domma Tools v0.44.1
3
3
  * Developer tools: Theme Roller & Page Roller
4
4
  * (c) 2026 Darryl Waterhouse & DCBW-IT
5
- * Built: 2026-09-22T03:17:34.493Z
6
- * Commit: 4c4889f
5
+ * Built: 2026-09-23T16:21:21.035Z
6
+ * Commit: eca0079
7
7
  *
8
8
  * Requires: domma.min.js
9
9
  */
@@ -1,3 +1,3 @@
1
- import{SHORTCODES as $}from"./shortcode-registry.js";import{EFFECTS as C}from"./effect-defs.js";import{boolState as V,boolValue as G,findEnclosingShortcode as M,setAttr as N,setAttrs as X,effectAttrKeys as q}from"./shortcode-tags.js";import{insertAtCursor as J}from"./markdown-toolbar.js";import{clearLookupCache as Q,lookupOptions as Z}from"./shortcode-lookups.js";import{insertMenuGroups as ee,insertShortcodeByName as te}from"./shortcode-insert.js";import{openShortcodeForm as R}from"./shortcode-form.js";function ne(e){return Z(e,c?.span?.attrs||{})}const oe=$.map(e=>({name:e.name,selfClosing:!!e.selfClosing})),se=new Map($.map(e=>[e.name,e])),ie=new Map(C.map(e=>[e.name,e])),ce=C.map(e=>({name:e.name,selfClosing:!!e.selfClosing})),z=[...oe,...ce],D=new Set(["card","hero","banner","col","button"]);let c=null;const P="position:fixed;z-index:10000;min-width:210px;max-height:70vh;overflow-y:auto;background:var(--dm-surface,#1c1c30);border:1px solid var(--dm-border,#2a2a40);border-radius:6px;box-shadow:var(--dm-shadow-lg);padding:.3rem;font-size:.83rem;color:var(--dm-text,#e5e5e5);",p="display:flex;align-items:center;gap:.5rem;width:100%;text-align:left;padding:.35rem .55rem;border:none;background:transparent;color:inherit;border-radius:4px;cursor:pointer;font-size:.83rem;",S="padding:.3rem .55rem;color:var(--dm-text-muted,#8a8a9a);font-size:.68rem;text-transform:uppercase;letter-spacing:.06em;",H="flex:1;min-width:0;padding:.2rem .4rem;background:var(--dm-input-bg,#12121e);border:1px solid var(--dm-border,#333);border-radius:3px;color:var(--dm-text,#eee);font-size:.8rem;",ae="height:1px;background:var(--dm-border,#2a2a40);margin:.3rem .2rem;";function m(e){return e.addEventListener("mouseenter",()=>{e.style.background="var(--dm-primary,#4338ca)",e.style.color="var(--dm-primary-text,#fff)"}),e.addEventListener("mouseleave",()=>{e.style.background="transparent",e.style.color="inherit"}),e}export function attachContextMenu(e){return typeof Domma>"u"||!Domma.elements?.contextMenu?(e.addEventListener("contextmenu",n=>{n.preventDefault(),f();const t=M(e,z);F(e,t,n.clientX,n.clientY)}),null):Domma.elements.contextMenu(e,{inherit:!1,render:n=>{f();const t=M(e,z);F(e,t,n.x,n.y)}})}function f(){c&&(v(),c.root.remove(),document.removeEventListener("mousedown",W,!0),document.removeEventListener("keydown",j,!0),window.removeEventListener("scroll",Y,!0),c=null)}const W=e=>{c&&!c.root.contains(e.target)&&!(c.submenu&&c.submenu.contains(e.target))&&f()},Y=e=>{c&&!c.root.contains(e.target)&&!(c.submenu&&c.submenu.contains(e.target))&&f()},j=e=>{e.key==="Escape"&&f()};function F(e,n,t,o){Q();const s=document.createElement("div");s.className="dm-sc-menu",s.style.cssText=P,c={root:s,textarea:e,span:n,submenu:null};const i=n&&(se.get(n.name)||ie.get(n.name)),l=n&&D.has(n.name)?C.filter(r=>r.name in n.attrs):[];i&&re(s,i);for(const r of l)xe(s,r);(i||l.length)&&s.appendChild(le()),s.appendChild(y("Add effect\u2026",()=>be())),s.appendChild(y("Insert\u2026",()=>ye()));const a=e.selectionStart!==e.selectionEnd;(n||a)&&s.appendChild(y(a?"Wrap selection in\u2026":"Wrap in\u2026",()=>Ee())),document.body.appendChild(s),U(s,t,o),document.addEventListener("mousedown",W,!0),document.addEventListener("keydown",j,!0),window.addEventListener("scroll",Y,!0),window.I&&I.scan(s)}function le(){const e=document.createElement("div");return e.style.cssText=ae,e}function k(e){const n=document.createElement("div");return n.style.cssText=S,n.textContent=e,n}function re(e,n){e.appendChild(k(`${n.label} options`));const t=m(document.createElement("button"));t.type="button",t.style.cssText=p,t.innerHTML='<span data-icon="edit" data-icon-size="13" style="display:inline-flex;align-items:center;"></span>',t.appendChild(document.createTextNode(`Edit ${n.label}\u2026`)),t.addEventListener("click",()=>{const{textarea:o,span:s}=c;f(),R(o,n,{span:s})}),e.appendChild(t);for(const o of n.attrs||[])e.appendChild(O(o))}function O(e,n=e.name){const t=c.span.attrs[n];switch(e.type){case"checkbox":return de(e,n,V(t,e.default));case"select":return me(e,n,t);case"lookup":return he(e,n,t);case"colour":return pe(e,n,t);case"icon":return fe(e,n,t);default:return ue(e,n,t)}}function b(e,n){c.span=N(c.textarea,c.span,e,n)}function de(e,n,t){const o=m(document.createElement("label"));o.style.cssText=p;const s=document.createElement("input");return s.type="checkbox",s.checked=t,s.addEventListener("change",()=>b(n,G(e,s.checked))),o.appendChild(s),o.appendChild(document.createTextNode(e.label)),o}function ue(e,n,t){const o=document.createElement("div");o.style.cssText=p+"cursor:default;";const s=document.createElement("span");s.textContent=e.label,s.style.cssText="white-space:nowrap;color:var(--dm-text-muted,#9a9aac);";const i=document.createElement("input");i.type=e.type==="number"?"number":"text",i.value=t===!0||t==null?"":t,e.default!=null&&e.default!==""&&(i.placeholder=String(e.default)),i.style.cssText=H;let l=null;return i.addEventListener("input",()=>{clearTimeout(l),l=setTimeout(()=>b(n,i.value.trim()),180)}),i.addEventListener("mousedown",a=>a.stopPropagation()),o.appendChild(s),o.appendChild(i),o}function pe(e,n,t){const o=document.createElement("div");o.style.cssText=p+"cursor:default;";const s=document.createElement("span");s.textContent=e.label,s.style.flex="1";const i=document.createElement("input");return i.type="color",typeof t=="string"&&/^#/.test(t)?i.value=t:typeof e.default=="string"&&/^#/.test(e.default)&&(i.value=e.default),i.addEventListener("input",()=>b(n,i.value)),o.appendChild(s),o.appendChild(i),o}function me(e,n,t){const o=t==null||t===!0?e.default??"":t;return y(`${e.label}${o!==""?`: ${o}`:""}`,()=>{const s=g();for(const[i,l]of e.options||[]){const a=m(document.createElement("button"));a.type="button",a.style.cssText=p,a.textContent=l,i===o&&(a.style.fontWeight="700"),a.addEventListener("click",()=>{b(n,i),v()}),s.appendChild(a)}return s})}function fe(e,n,t){const o=t==null||t===!0?"":String(t),s=o?`${e.label}: ${o}`:e.label;return y(s,()=>{const i=g();i.style.width="232px";const l=document.createElement("input");l.type="text",l.placeholder="Search icons\u2026",l.style.cssText=H+"margin:6px;width:calc(100% - 12px);",l.addEventListener("mousedown",T=>T.stopPropagation()),i.appendChild(l);const a=m(document.createElement("button"));a.type="button",a.style.cssText=p,a.textContent="- none -",o||(a.style.fontWeight="700"),a.addEventListener("click",()=>{b(n,null),v()}),i.appendChild(a);const r=document.createElement("div");r.style.cssText="display:flex;flex-wrap:wrap;gap:2px;padding:4px 6px;max-height:220px;overflow-y:auto;",i.appendChild(r);const u=96,w=()=>{const T=typeof I<"u"&&typeof I.list=="function"?I.list():[],A=l.value.trim().toLowerCase(),_=A?T.filter(d=>d.toLowerCase().includes(A)):T;r.replaceChildren();for(const d of _.slice(0,u)){const x=m(document.createElement("button"));x.type="button",x.title=d,x.style.cssText="width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid transparent;border-radius:4px;cursor:pointer;color:inherit;",d===o&&(x.style.borderColor="var(--dm-primary,#5b8cff)");const L=document.createElement("span");L.setAttribute("data-icon",d),L.setAttribute("data-icon-size","15"),L.style.cssText="display:inline-flex;align-items:center;",x.appendChild(L),x.addEventListener("click",()=>{b(n,d),v()}),r.appendChild(x)}if(_.length>u){const d=document.createElement("div");d.style.cssText=S,d.textContent=`${_.length-u} more - refine search`,r.appendChild(d)}if(!_.length){const d=document.createElement("div");d.style.cssText=S,d.textContent="No icons match",r.appendChild(d)}window.I&&I.scan(r)};let h=null;return l.addEventListener("input",()=>{clearTimeout(h),h=setTimeout(w,120)}),w(),setTimeout(()=>l.focus(),0),i})}function he(e,n,t){const o=t==null||t===!0?e.default??"":t;return y(`${e.label}${o!==""?`: ${o}`:""}`,()=>{const s=g();s.style.minHeight="48px";const i=typeof E<"u"&&E.loader?E.loader(s,{type:"dots"}):null;return ne(e.source).then(l=>{if(i&&i.destroy(),!c||c.submenu!==s)return;if(s.style.minHeight="",!l.length){const u=document.createElement("div");u.style.cssText=S,u.textContent="Nothing found",s.appendChild(u)}const a=l.some(([u])=>u===o),r=[["","- none -"],...l];o!==""&&!a&&r.push([o,`${o} (missing)`]);for(const[u,w]of r){const h=m(document.createElement("button"));h.type="button",h.style.cssText=p,h.textContent=w,u===o&&(h.style.fontWeight="700"),h.addEventListener("click",()=>{b(n,u===""?null:u),v()}),s.appendChild(h)}}),s})}function xe(e,n){e.appendChild(k(`Effect: ${n.label}`));for(const o of n.attrs||[])e.appendChild(O(o,`${n.name}-${o.name}`));const t=m(document.createElement("button"));t.type="button",t.style.cssText=p,t.innerHTML='<span data-icon="trash" data-icon-size="13" style="display:inline-flex;align-items:center;"></span> Remove effect',t.addEventListener("click",()=>{const o={};for(const s of q(c.span.attrs,n.name))o[s]=null;X(c.textarea,c.span,o),f()}),e.appendChild(t)}function be(){const e=g(),n=c.span,t=n&&D.has(n.name)?n.name:null;for(const o of[...new Set(C.map(s=>s.category))]){const s=C.filter(i=>i.category===o);if(s.length){e.appendChild(k(o));for(const i of s){const l=t&&(i.hosts||[]).includes(t)&&!(i.name in n.attrs),a=m(document.createElement("button"));if(a.type="button",a.style.cssText=p+"justify-content:space-between;",a.appendChild(document.createTextNode(i.label)),l){const r=document.createElement("span");r.textContent=`inject \u2192 [${t}]`,r.style.cssText="opacity:.6;font-size:.7rem;",a.appendChild(r)}a.addEventListener("click",()=>{if(l)N(c.textarea,c.span,i.name,!0);else{const r=i.selfClosing?`[${i.name} /]`:`[${i.name}]
1
+ import{SHORTCODES as $}from"./shortcode-registry.js";import{EFFECTS as C}from"./effect-defs.js";import{boolState as V,boolValue as G,findEnclosingShortcode as M,setAttr as N,setAttrs as X,effectAttrKeys as q}from"./shortcode-tags.js";import{insertAtCursor as J}from"./markdown-toolbar.js";import{clearLookupCache as Q,lookupOptions as Z}from"./shortcode-lookups.js";import{insertMenuGroups as ee,insertShortcodeByName as te}from"./shortcode-insert.js";import{openShortcodeForm as R}from"./shortcode-form.js";function ne(e){return Z(e,c?.span?.attrs||{})}const oe=$.map(e=>({name:e.name,selfClosing:!!e.selfClosing})),se=new Map($.map(e=>[e.name,e])),ie=new Map(C.map(e=>[e.name,e])),ce=C.map(e=>({name:e.name,selfClosing:!!e.selfClosing})),z=[...oe,...ce],D=new Set(["card","hero","banner","col","button"]);let c=null;const P="position:fixed;z-index:10000;min-width:210px;max-height:70vh;overflow-y:auto;background:var(--dm-surface,#1c1c30);border:1px solid var(--dm-border,#2a2a40);border-radius:6px;box-shadow:var(--dm-shadow-lg);padding:.3rem;font-size:.83rem;color:var(--dm-text,#e5e5e5);",p="display:flex;align-items:center;gap:.5rem;width:100%;text-align:left;padding:.35rem .55rem;border:none;background:transparent;color:inherit;border-radius:4px;cursor:pointer;font-size:.83rem;",S="padding:.3rem .55rem;color:var(--dm-text-muted,#8a8a9a);font-size:.68rem;text-transform:uppercase;letter-spacing:.06em;",H="flex:1;min-width:0;padding:.2rem .4rem;background:var(--dm-input-bg,#12121e);border:1px solid var(--dm-border,#333);border-radius:3px;color:var(--dm-text,#eee);font-size:.8rem;",ae="height:1px;background:var(--dm-border,#2a2a40);margin:.3rem .2rem;";function m(e){return e.addEventListener("mouseenter",()=>{e.style.background="var(--dm-primary,#4338ca)",e.style.color="var(--dm-primary-text,#fff)"}),e.addEventListener("mouseleave",()=>{e.style.background="transparent",e.style.color="inherit"}),e}export function attachContextMenu(e){return typeof Domma>"u"||!Domma.elements?.contextMenu?(e.addEventListener("contextmenu",n=>{n.preventDefault(),f();const t=M(e,z);F(e,t,n.clientX,n.clientY)}),null):Domma.elements.contextMenu(e,{inherit:!1,cursor:!1,render:n=>{f();const t=M(e,z);F(e,t,n.x,n.y)}})}function f(){c&&(v(),c.root.remove(),document.removeEventListener("mousedown",W,!0),document.removeEventListener("keydown",j,!0),window.removeEventListener("scroll",Y,!0),c=null)}const W=e=>{c&&!c.root.contains(e.target)&&!(c.submenu&&c.submenu.contains(e.target))&&f()},Y=e=>{c&&!c.root.contains(e.target)&&!(c.submenu&&c.submenu.contains(e.target))&&f()},j=e=>{e.key==="Escape"&&f()};function F(e,n,t,o){Q();const s=document.createElement("div");s.className="dm-sc-menu",s.style.cssText=P,c={root:s,textarea:e,span:n,submenu:null};const i=n&&(se.get(n.name)||ie.get(n.name)),l=n&&D.has(n.name)?C.filter(r=>r.name in n.attrs):[];i&&re(s,i);for(const r of l)xe(s,r);(i||l.length)&&s.appendChild(le()),s.appendChild(y("Add effect\u2026",()=>be())),s.appendChild(y("Insert\u2026",()=>ye()));const a=e.selectionStart!==e.selectionEnd;(n||a)&&s.appendChild(y(a?"Wrap selection in\u2026":"Wrap in\u2026",()=>Ee())),document.body.appendChild(s),U(s,t,o),document.addEventListener("mousedown",W,!0),document.addEventListener("keydown",j,!0),window.addEventListener("scroll",Y,!0),window.I&&I.scan(s)}function le(){const e=document.createElement("div");return e.style.cssText=ae,e}function k(e){const n=document.createElement("div");return n.style.cssText=S,n.textContent=e,n}function re(e,n){e.appendChild(k(`${n.label} options`));const t=m(document.createElement("button"));t.type="button",t.style.cssText=p,t.innerHTML='<span data-icon="edit" data-icon-size="13" style="display:inline-flex;align-items:center;"></span>',t.appendChild(document.createTextNode(`Edit ${n.label}\u2026`)),t.addEventListener("click",()=>{const{textarea:o,span:s}=c;f(),R(o,n,{span:s})}),e.appendChild(t);for(const o of n.attrs||[])e.appendChild(O(o))}function O(e,n=e.name){const t=c.span.attrs[n];switch(e.type){case"checkbox":return de(e,n,V(t,e.default));case"select":return me(e,n,t);case"lookup":return he(e,n,t);case"colour":return pe(e,n,t);case"icon":return fe(e,n,t);default:return ue(e,n,t)}}function b(e,n){c.span=N(c.textarea,c.span,e,n)}function de(e,n,t){const o=m(document.createElement("label"));o.style.cssText=p;const s=document.createElement("input");return s.type="checkbox",s.checked=t,s.addEventListener("change",()=>b(n,G(e,s.checked))),o.appendChild(s),o.appendChild(document.createTextNode(e.label)),o}function ue(e,n,t){const o=document.createElement("div");o.style.cssText=p+"cursor:default;";const s=document.createElement("span");s.textContent=e.label,s.style.cssText="white-space:nowrap;color:var(--dm-text-muted,#9a9aac);";const i=document.createElement("input");i.type=e.type==="number"?"number":"text",i.value=t===!0||t==null?"":t,e.default!=null&&e.default!==""&&(i.placeholder=String(e.default)),i.style.cssText=H;let l=null;return i.addEventListener("input",()=>{clearTimeout(l),l=setTimeout(()=>b(n,i.value.trim()),180)}),i.addEventListener("mousedown",a=>a.stopPropagation()),o.appendChild(s),o.appendChild(i),o}function pe(e,n,t){const o=document.createElement("div");o.style.cssText=p+"cursor:default;";const s=document.createElement("span");s.textContent=e.label,s.style.flex="1";const i=document.createElement("input");return i.type="color",typeof t=="string"&&/^#/.test(t)?i.value=t:typeof e.default=="string"&&/^#/.test(e.default)&&(i.value=e.default),i.addEventListener("input",()=>b(n,i.value)),o.appendChild(s),o.appendChild(i),o}function me(e,n,t){const o=t==null||t===!0?e.default??"":t;return y(`${e.label}${o!==""?`: ${o}`:""}`,()=>{const s=g();for(const[i,l]of e.options||[]){const a=m(document.createElement("button"));a.type="button",a.style.cssText=p,a.textContent=l,i===o&&(a.style.fontWeight="700"),a.addEventListener("click",()=>{b(n,i),v()}),s.appendChild(a)}return s})}function fe(e,n,t){const o=t==null||t===!0?"":String(t),s=o?`${e.label}: ${o}`:e.label;return y(s,()=>{const i=g();i.style.width="232px";const l=document.createElement("input");l.type="text",l.placeholder="Search icons\u2026",l.style.cssText=H+"margin:6px;width:calc(100% - 12px);",l.addEventListener("mousedown",T=>T.stopPropagation()),i.appendChild(l);const a=m(document.createElement("button"));a.type="button",a.style.cssText=p,a.textContent="- none -",o||(a.style.fontWeight="700"),a.addEventListener("click",()=>{b(n,null),v()}),i.appendChild(a);const r=document.createElement("div");r.style.cssText="display:flex;flex-wrap:wrap;gap:2px;padding:4px 6px;max-height:220px;overflow-y:auto;",i.appendChild(r);const u=96,w=()=>{const T=typeof I<"u"&&typeof I.list=="function"?I.list():[],A=l.value.trim().toLowerCase(),_=A?T.filter(d=>d.toLowerCase().includes(A)):T;r.replaceChildren();for(const d of _.slice(0,u)){const x=m(document.createElement("button"));x.type="button",x.title=d,x.style.cssText="width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid transparent;border-radius:4px;cursor:pointer;color:inherit;",d===o&&(x.style.borderColor="var(--dm-primary,#5b8cff)");const L=document.createElement("span");L.setAttribute("data-icon",d),L.setAttribute("data-icon-size","15"),L.style.cssText="display:inline-flex;align-items:center;",x.appendChild(L),x.addEventListener("click",()=>{b(n,d),v()}),r.appendChild(x)}if(_.length>u){const d=document.createElement("div");d.style.cssText=S,d.textContent=`${_.length-u} more - refine search`,r.appendChild(d)}if(!_.length){const d=document.createElement("div");d.style.cssText=S,d.textContent="No icons match",r.appendChild(d)}window.I&&I.scan(r)};let h=null;return l.addEventListener("input",()=>{clearTimeout(h),h=setTimeout(w,120)}),w(),setTimeout(()=>l.focus(),0),i})}function he(e,n,t){const o=t==null||t===!0?e.default??"":t;return y(`${e.label}${o!==""?`: ${o}`:""}`,()=>{const s=g();s.style.minHeight="48px";const i=typeof E<"u"&&E.loader?E.loader(s,{type:"dots"}):null;return ne(e.source).then(l=>{if(i&&i.destroy(),!c||c.submenu!==s)return;if(s.style.minHeight="",!l.length){const u=document.createElement("div");u.style.cssText=S,u.textContent="Nothing found",s.appendChild(u)}const a=l.some(([u])=>u===o),r=[["","- none -"],...l];o!==""&&!a&&r.push([o,`${o} (missing)`]);for(const[u,w]of r){const h=m(document.createElement("button"));h.type="button",h.style.cssText=p,h.textContent=w,u===o&&(h.style.fontWeight="700"),h.addEventListener("click",()=>{b(n,u===""?null:u),v()}),s.appendChild(h)}}),s})}function xe(e,n){e.appendChild(k(`Effect: ${n.label}`));for(const o of n.attrs||[])e.appendChild(O(o,`${n.name}-${o.name}`));const t=m(document.createElement("button"));t.type="button",t.style.cssText=p,t.innerHTML='<span data-icon="trash" data-icon-size="13" style="display:inline-flex;align-items:center;"></span> Remove effect',t.addEventListener("click",()=>{const o={};for(const s of q(c.span.attrs,n.name))o[s]=null;X(c.textarea,c.span,o),f()}),e.appendChild(t)}function be(){const e=g(),n=c.span,t=n&&D.has(n.name)?n.name:null;for(const o of[...new Set(C.map(s=>s.category))]){const s=C.filter(i=>i.category===o);if(s.length){e.appendChild(k(o));for(const i of s){const l=t&&(i.hosts||[]).includes(t)&&!(i.name in n.attrs),a=m(document.createElement("button"));if(a.type="button",a.style.cssText=p+"justify-content:space-between;",a.appendChild(document.createTextNode(i.label)),l){const r=document.createElement("span");r.textContent=`inject \u2192 [${t}]`,r.style.cssText="opacity:.6;font-size:.7rem;",a.appendChild(r)}a.addEventListener("click",()=>{if(l)N(c.textarea,c.span,i.name,!0);else{const r=i.selfClosing?`[${i.name} /]`:`[${i.name}]
2
2
  Content
3
3
  [/${i.name}]`;J(c.textarea,r)}f()}),e.appendChild(a)}}}return e}function y(e,n){const t=m(document.createElement("button"));t.type="button",t.style.cssText=p+"justify-content:space-between;",t.appendChild(document.createTextNode(e));const o=document.createElement("span");return o.setAttribute("data-icon","chevron-right"),o.setAttribute("data-icon-size","12"),o.style.cssText="opacity:.6;display:inline-flex;align-items:center;flex-shrink:0;",t.appendChild(o),t.addEventListener("mouseenter",()=>B(t,n)),t.addEventListener("click",s=>{s.stopPropagation(),B(t,n)}),t}function g(){const e=document.createElement("div");return e.className="dm-sc-submenu",e.style.cssText=P,e}function B(e,n){v();const t=n();c.submenu=t,document.body.appendChild(t);const o=e.getBoundingClientRect();U(t,o.right+2,o.top,o.left-2),window.I&&I.scan(t)}function v(){c&&c.submenu&&(c.submenu.remove(),c.submenu=null)}function K(e){const n=m(document.createElement("button"));if(n.type="button",n.style.cssText=p,e.icon){const t=document.createElement("span");t.setAttribute("data-icon",e.icon),t.setAttribute("data-icon-size","14"),t.style.cssText="opacity:.75;display:inline-flex;align-items:center;flex-shrink:0;",n.appendChild(t)}return n.appendChild(document.createTextNode(e.label)),n}function ye(){const e=g();for(const{category:n,items:t}of ee()){e.appendChild(k(n));for(const o of t){const s=K(o);s.addEventListener("click",()=>{const i=c.textarea;f(),te(o.name,i)}),e.appendChild(s)}}return e}function Ee(){const e=g();for(const n of $.filter(t=>t.wrappable||t.container)){const t=K(n);t.addEventListener("click",()=>{const o=c.textarea;f(),R(o,n)}),e.appendChild(t)}return e}function U(e,n,t,o){e.style.left="0px",e.style.top="0px";const s=e.getBoundingClientRect();let i=n;n+s.width>window.innerWidth-8&&(i=o!=null?o-s.width:n-s.width);let l=t;t+s.height>window.innerHeight-8&&(l=Math.max(8,window.innerHeight-s.height-8)),e.style.left=`${Math.max(8,i)}px`,e.style.top=`${Math.max(8,l)}px`}
@@ -59,4 +59,4 @@ import{apiRequest as v,getToken as Z,refreshAccessToken as Q}from"../api.js";imp
59
59
  <span class="spacer"></span>
60
60
  <button type="button" class="btn btn-sm btn-primary" data-on-click="done">Done</button>
61
61
  </div>
62
- </div>`),n=(o,K=T=>T)=>M.computed({read:()=>t.value[o],write:T=>W(o,K(T))}),p={range:n("range"),limit:n("limit",Number),chart:n("chart"),live:n("live"),confirmReset:n("confirmReset")};e.element.appendChild(a);const m=V(a),f=M.applyBindings(p,a,{methods:{done:()=>e.close(),reset:()=>{t.value={...oe}}}});ee(e,()=>{f.dispose(),m()}),e.open()}X(r,{id:"analytics-settings",icon:"settings",title:"Analytics settings",onClick:J});const h=[...s.querySelectorAll(".an-list")];h[0].innerHTML=ve,h[1].innerHTML=A,h[2].innerHTML=A,h[3].innerHTML=A,h[4].innerHTML=be,l.add(V(s,{position:"bottom"}));const z=M.applyBindings(G,s,{methods:{refresh:()=>{w(),C()},showPages:()=>{d.value="pages"},showJourneys:()=>{d.value="journeys"},showSpikes:()=>{d.value="spikes"}}});l.add(()=>z.dispose());const N=e=>new URL(e,location.origin).href,Y=E.contextMenu(s,{match:".an-row",inherit:!1,density:"compact",items:e=>{const a=e.dataset.url;return[{label:"Open on the site",icon:"external-link",action:()=>window.open(N(a),"_blank","noopener")},{label:"Copy address",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(N(a)),E.toast("Address copied",{type:"success"})}catch{E.toast("The browser would not allow copying",{type:"error"})}}},{label:"Show only this page",icon:"filter",action:()=>{d.value="pages",u.value=a}}]}});l.add(()=>Y?.destroy?.());const F=E.contextMenu(s.querySelector(".an-bar-end"),{match:".an-more",inherit:!1,density:"compact",items:()=>[{label:`Export ${B[c.peek()].toLowerCase()} as CSV`,icon:"download",action:async()=>{try{await ye(c.peek())}catch{E.toast("Export failed.",{type:"error"})}}},{type:"divider"},{label:"Reset all statistics\u2026",icon:"trash",danger:!0,action:async()=>{if(!(t.peek().confirmReset&&!await E.confirm("Delete every analytics counter for this site? This cannot be undone.")))try{await v("/analytics/stats",{method:"DELETE"}),E.toast("Analytics reset.",{type:"success"}),w()}catch{E.toast("Reset failed.",{type:"error"})}}}]});l.add(()=>F?.destroy?.()),s.addEventListener("click",e=>{const a=e.target.closest(".an-kebab, .an-more");a&&te(a)}),I.scan(s);try{await he()}catch{E.toast("The chart library could not be loaded; figures still work.",{type:"warning"})}await Promise.all([w(),C()])}};
62
+ </div>`),n=(o,K=T=>T)=>M.computed({read:()=>t.value[o],write:T=>W(o,K(T))}),p={range:n("range"),limit:n("limit",Number),chart:n("chart"),live:n("live"),confirmReset:n("confirmReset")};e.element.appendChild(a);const m=V(a),f=M.applyBindings(p,a,{methods:{done:()=>e.close(),reset:()=>{t.value={...oe}}}});ee(e,()=>{f.dispose(),m()}),e.open()}X(r,{id:"analytics-settings",icon:"settings",title:"Analytics settings",onClick:J});const h=[...s.querySelectorAll(".an-list")];h[0].innerHTML=ve,h[1].innerHTML=A,h[2].innerHTML=A,h[3].innerHTML=A,h[4].innerHTML=be,l.add(V(s,{position:"bottom"}));const z=M.applyBindings(G,s,{methods:{refresh:()=>{w(),C()},showPages:()=>{d.value="pages"},showJourneys:()=>{d.value="journeys"},showSpikes:()=>{d.value="spikes"}}});l.add(()=>z.dispose());const N=e=>new URL(e,location.origin).href,Y=E.contextMenu(s,{cursor:"glyph",match:".an-row",inherit:!1,density:"compact",items:e=>{const a=e.dataset.url;return[{label:"Open on the site",icon:"external-link",action:()=>window.open(N(a),"_blank","noopener")},{label:"Copy address",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(N(a)),E.toast("Address copied",{type:"success"})}catch{E.toast("The browser would not allow copying",{type:"error"})}}},{label:"Show only this page",icon:"filter",action:()=>{d.value="pages",u.value=a}}]}});l.add(()=>Y?.destroy?.());const F=E.contextMenu(s.querySelector(".an-bar-end"),{cursor:"glyph",match:".an-more",inherit:!1,density:"compact",items:()=>[{label:`Export ${B[c.peek()].toLowerCase()} as CSV`,icon:"download",action:async()=>{try{await ye(c.peek())}catch{E.toast("Export failed.",{type:"error"})}}},{type:"divider"},{label:"Reset all statistics\u2026",icon:"trash",danger:!0,action:async()=>{if(!(t.peek().confirmReset&&!await E.confirm("Delete every analytics counter for this site? This cannot be undone.")))try{await v("/analytics/stats",{method:"DELETE"}),E.toast("Analytics reset.",{type:"success"}),w()}catch{E.toast("Reset failed.",{type:"error"})}}}]});l.add(()=>F?.destroy?.()),s.addEventListener("click",e=>{const a=e.target.closest(".an-kebab, .an-more");a&&te(a)}),I.scan(s);try{await he()}catch{E.toast("The chart library could not be loaded; figures still work.",{type:"warning"})}await Promise.all([w(),C()])}};
@@ -171,4 +171,4 @@ import{apiRequest as Ue}from"../api.js";import{addBannerAction as Fe}from"../lib
171
171
  <div class="ct-groups-rows" data-each="list.value key=name"></div>
172
172
  </div>
173
173
  <p class="text-muted" data-if="noGroups.value">No groups yet.</p>
174
- </div>`),a=M.observable(""),s={draft:a,list:M.observable([])},u=M.effect(()=>{const d=ne.value.groups;s.list.value=b.value.map(v=>({name:v,countText:A(d[v]??0,"contact")}))});s.hasGroups=M.computed(()=>s.list.value.length>0),s.noGroups=M.computed(()=>s.list.value.length===0),s.cannotAdd=M.computed(()=>!a.value.trim());async function l(){const d=a.peek().trim();d&&await U(d)&&(a.value="",E.toast("Group added.",{type:"success"}))}e.addEventListener("click",async d=>{const v=d.target.closest("[data-act]"),C=v?.closest("[data-group]")?.dataset.group;if(!(!v||!C)){if(v.dataset.act==="show")j.value={kind:"group",value:C},t.close();else if(v.dataset.act==="rename"){const R=String(await E.prompt("Rename the group to:",{inputValue:C})??"").trim();if(!R||R===C)return;try{await h(`/contacts/groups/${encodeURIComponent(C)}`,"PUT",{newName:R}),await W(),E.toast("Group renamed.",{type:"success"})}catch(_){E.toast(_?.message||"Failed to rename the group.",{type:"error"})}}else if(v.dataset.act==="delete"){if(!await E.confirm("Delete this group? Its contacts stay - they are just no longer in it."))return;try{await h(`/contacts/groups/${encodeURIComponent(C)}`,"DELETE"),await W(),E.toast("Group deleted.",{type:"success"})}catch{E.toast("Failed to delete the group.",{type:"error"})}}}}),t.element.appendChild(e);const $=J(e);e.querySelector(".ct-groups-rows").innerHTML=Xe;const n=M.applyBindings(s,e,{methods:{add:()=>(l(),!1)}});Z(t,()=>{u?.(),n.dispose(),$()}),t.open()}Fe(x,{id:"contacts-settings",icon:"settings",title:"Contacts settings",onClick:Be}),r.querySelector(".ct-rows").innerHTML=_e,r.querySelector(".ct-chips").innerHTML=Ve;const O=r.querySelector(".ct-list"),pe=r.querySelector(".ct-bar"),Ge=r.querySelector(".ct-chips-row"),ue=r.querySelector(".ct-bulk");k.add(J(r,{position:"bottom"}));const Oe=M.applyBindings(Se,r,{methods:{create:()=>F(null),bulkDelete:Ae,clearSelection:()=>{i.value=new Set}}});k.add(()=>Oe.dispose()),k.add(M.effect(()=>{const t=m.value;O.classList.toggle("is-compact",t.density==="compact");const e={company:!t.showCompany,email:!t.showEmail,phone:!t.showPhone,groups:!t.showGroups};for(const[s,u]of Object.entries(e))O.classList.toggle(`hide-${s}`,u);const a=["1.1rem","1.5rem","minmax(10rem, 1.4fr)"];e.company||a.push("minmax(0, 1fr)"),e.email||a.push("minmax(0, 1.2fr)"),e.phone||a.push("minmax(0, .9fr)"),e.groups||a.push("minmax(0, 1fr)"),a.push("1.75rem"),O.style.setProperty("--ct-cols",a.join(" "))}));const De=E.contextMenu(O,{match:".ct-row",inherit:!1,density:"compact",items:t=>{const e=t.dataset.id,a=N(e);if(!a)return[];const s=he(a),u=ge(a),l=a.groups||[];return(n=>n.filter(d=>d.visible!==!1))([{label:"Edit\u2026",icon:"edit",action:()=>F(e)},{type:"divider"},{label:`Email ${s}`,icon:"mail",visible:!!s,action:()=>{location.href=`mailto:${encodeURIComponent(s).replace(/%40/g,"@")}`}},{label:`Call ${u}`,icon:"phone",visible:!!u,action:()=>{location.href=`tel:${u.replace(/[^\d+]/g,"")}`}},{label:"Copy email",icon:"copy",visible:!!s,action:()=>ie(s,"Email")},{label:"Copy phone",icon:"copy",visible:!!u,action:()=>ie(u,"Phone")},{type:"divider",visible:!!(s||u)},{label:a.favourite===!0?"Unstar":"Star",icon:"star",action:()=>q(e,{favourite:a.favourite!==!0})},{label:"Groups",icon:"folder",submenu:[...b.peek().map(n=>({label:`${l.includes(n)?"\u2713 ":""}${n}`,action:()=>Te(e,n,!l.includes(n))})),...b.peek().length?[{type:"divider"}]:[],{label:"New group\u2026",action:()=>re([e])}]},{label:"Duplicate",icon:"copy",action:()=>xe(e)},{label:i.peek().has(e)?"Deselect":"Select",icon:"check-square",action:()=>de(e)},{type:"divider"},{label:"Delete",icon:"trash",danger:!0,action:()=>le(e)}])}});k.add(()=>De?.destroy?.());const Re=E.contextMenu(pe,{match:".ct-more",inherit:!1,density:"compact",items:()=>[{label:"Manage groups\u2026",icon:"folder",action:Pe},{type:"divider"},{label:"Export all (JSON)",icon:"download",action:Le},{label:"Import from file\u2026",icon:"upload",action:()=>Y.click()},{type:"divider"},{label:"Select all shown",icon:"check-square",disabled:!S.peek().length,action:()=>{i.value=new Set(S.peek().map(t=>t.id))}},{label:"Clear selection",icon:"x",disabled:!i.peek().size,action:()=>{i.value=new Set}}]});k.add(()=>Re?.destroy?.());const Ie=E.contextMenu(ue,{match:".ct-bulk-group",inherit:!1,density:"compact",items:()=>[...b.peek().map(t=>({label:t,action:()=>Ne(t)})),...b.peek().length?[{type:"divider"}]:[],{label:"New group\u2026",action:()=>re([...i.peek()])}]});k.add(()=>Ie?.destroy?.()),pe.addEventListener("click",t=>{const e=t.target.closest(".ct-more");e&&ee(e)}),ue.addEventListener("click",t=>{const e=t.target.closest(".ct-bulk-group");e&&ee(e)}),Ge.addEventListener("click",t=>{const e=t.target.closest(".ct-chip");if(!e)return;const a=e.dataset.chip;j.value=a==="fav"?{kind:"fav"}:a.startsWith("group:")?{kind:"group",value:a.slice(6)}:{kind:"all"}}),O.addEventListener("click",t=>{if(t.target.closest(".ct-select-all"))return;const e=t.target.closest(".ct-row");if(!e)return;const a=e.dataset.id;if(t.target.closest(".ct-kebab"))return ee(t.target.closest(".ct-kebab"));if(!t.target.closest(".ct-select")){if(t.target.closest(".ct-fav")){const s=N(a);s&&q(a,{favourite:s.favourite!==!0});return}F(a)}}),O.addEventListener("change",t=>{if(t.target.matches(".ct-select")&&de(t.target.closest(".ct-row").dataset.id),t.target.matches(".ct-select-all")){const e=S.peek().map(a=>a.id);i.value=t.target.checked?new Set(e):new Set}}),O.addEventListener("keydown",t=>{const e=t.target.closest?.(".ct-row");e&&t.key==="Enter"&&t.target===e&&F(e.dataset.id)});const me=t=>{t.ctrlKey||t.metaKey||t.altKey||t.target.closest?.("input, textarea, select, [contenteditable], .dm-slideover")||(t.key==="/"?(t.preventDefault(),se.value=!0):(t.key==="n"||t.key==="N")&&(t.preventDefault(),F(null)))};document.addEventListener("keydown",me),k.add(()=>document.removeEventListener("keydown",me)),I.scan(r),await W()}};
174
+ </div>`),a=M.observable(""),s={draft:a,list:M.observable([])},u=M.effect(()=>{const d=ne.value.groups;s.list.value=b.value.map(v=>({name:v,countText:A(d[v]??0,"contact")}))});s.hasGroups=M.computed(()=>s.list.value.length>0),s.noGroups=M.computed(()=>s.list.value.length===0),s.cannotAdd=M.computed(()=>!a.value.trim());async function l(){const d=a.peek().trim();d&&await U(d)&&(a.value="",E.toast("Group added.",{type:"success"}))}e.addEventListener("click",async d=>{const v=d.target.closest("[data-act]"),C=v?.closest("[data-group]")?.dataset.group;if(!(!v||!C)){if(v.dataset.act==="show")j.value={kind:"group",value:C},t.close();else if(v.dataset.act==="rename"){const R=String(await E.prompt("Rename the group to:",{inputValue:C})??"").trim();if(!R||R===C)return;try{await h(`/contacts/groups/${encodeURIComponent(C)}`,"PUT",{newName:R}),await W(),E.toast("Group renamed.",{type:"success"})}catch(_){E.toast(_?.message||"Failed to rename the group.",{type:"error"})}}else if(v.dataset.act==="delete"){if(!await E.confirm("Delete this group? Its contacts stay - they are just no longer in it."))return;try{await h(`/contacts/groups/${encodeURIComponent(C)}`,"DELETE"),await W(),E.toast("Group deleted.",{type:"success"})}catch{E.toast("Failed to delete the group.",{type:"error"})}}}}),t.element.appendChild(e);const $=J(e);e.querySelector(".ct-groups-rows").innerHTML=Xe;const n=M.applyBindings(s,e,{methods:{add:()=>(l(),!1)}});Z(t,()=>{u?.(),n.dispose(),$()}),t.open()}Fe(x,{id:"contacts-settings",icon:"settings",title:"Contacts settings",onClick:Be}),r.querySelector(".ct-rows").innerHTML=_e,r.querySelector(".ct-chips").innerHTML=Ve;const O=r.querySelector(".ct-list"),pe=r.querySelector(".ct-bar"),Ge=r.querySelector(".ct-chips-row"),ue=r.querySelector(".ct-bulk");k.add(J(r,{position:"bottom"}));const Oe=M.applyBindings(Se,r,{methods:{create:()=>F(null),bulkDelete:Ae,clearSelection:()=>{i.value=new Set}}});k.add(()=>Oe.dispose()),k.add(M.effect(()=>{const t=m.value;O.classList.toggle("is-compact",t.density==="compact");const e={company:!t.showCompany,email:!t.showEmail,phone:!t.showPhone,groups:!t.showGroups};for(const[s,u]of Object.entries(e))O.classList.toggle(`hide-${s}`,u);const a=["1.1rem","1.5rem","minmax(10rem, 1.4fr)"];e.company||a.push("minmax(0, 1fr)"),e.email||a.push("minmax(0, 1.2fr)"),e.phone||a.push("minmax(0, .9fr)"),e.groups||a.push("minmax(0, 1fr)"),a.push("1.75rem"),O.style.setProperty("--ct-cols",a.join(" "))}));const De=E.contextMenu(O,{cursor:"glyph",match:".ct-row",inherit:!1,density:"compact",items:t=>{const e=t.dataset.id,a=N(e);if(!a)return[];const s=he(a),u=ge(a),l=a.groups||[];return(n=>n.filter(d=>d.visible!==!1))([{label:"Edit\u2026",icon:"edit",action:()=>F(e)},{type:"divider"},{label:`Email ${s}`,icon:"mail",visible:!!s,action:()=>{location.href=`mailto:${encodeURIComponent(s).replace(/%40/g,"@")}`}},{label:`Call ${u}`,icon:"phone",visible:!!u,action:()=>{location.href=`tel:${u.replace(/[^\d+]/g,"")}`}},{label:"Copy email",icon:"copy",visible:!!s,action:()=>ie(s,"Email")},{label:"Copy phone",icon:"copy",visible:!!u,action:()=>ie(u,"Phone")},{type:"divider",visible:!!(s||u)},{label:a.favourite===!0?"Unstar":"Star",icon:"star",action:()=>q(e,{favourite:a.favourite!==!0})},{label:"Groups",icon:"folder",submenu:[...b.peek().map(n=>({label:`${l.includes(n)?"\u2713 ":""}${n}`,action:()=>Te(e,n,!l.includes(n))})),...b.peek().length?[{type:"divider"}]:[],{label:"New group\u2026",action:()=>re([e])}]},{label:"Duplicate",icon:"copy",action:()=>xe(e)},{label:i.peek().has(e)?"Deselect":"Select",icon:"check-square",action:()=>de(e)},{type:"divider"},{label:"Delete",icon:"trash",danger:!0,action:()=>le(e)}])}});k.add(()=>De?.destroy?.());const Re=E.contextMenu(pe,{cursor:"glyph",match:".ct-more",inherit:!1,density:"compact",items:()=>[{label:"Manage groups\u2026",icon:"folder",action:Pe},{type:"divider"},{label:"Export all (JSON)",icon:"download",action:Le},{label:"Import from file\u2026",icon:"upload",action:()=>Y.click()},{type:"divider"},{label:"Select all shown",icon:"check-square",disabled:!S.peek().length,action:()=>{i.value=new Set(S.peek().map(t=>t.id))}},{label:"Clear selection",icon:"x",disabled:!i.peek().size,action:()=>{i.value=new Set}}]});k.add(()=>Re?.destroy?.());const Ie=E.contextMenu(ue,{cursor:"glyph",match:".ct-bulk-group",inherit:!1,density:"compact",items:()=>[...b.peek().map(t=>({label:t,action:()=>Ne(t)})),...b.peek().length?[{type:"divider"}]:[],{label:"New group\u2026",action:()=>re([...i.peek()])}]});k.add(()=>Ie?.destroy?.()),pe.addEventListener("click",t=>{const e=t.target.closest(".ct-more");e&&ee(e)}),ue.addEventListener("click",t=>{const e=t.target.closest(".ct-bulk-group");e&&ee(e)}),Ge.addEventListener("click",t=>{const e=t.target.closest(".ct-chip");if(!e)return;const a=e.dataset.chip;j.value=a==="fav"?{kind:"fav"}:a.startsWith("group:")?{kind:"group",value:a.slice(6)}:{kind:"all"}}),O.addEventListener("click",t=>{if(t.target.closest(".ct-select-all"))return;const e=t.target.closest(".ct-row");if(!e)return;const a=e.dataset.id;if(t.target.closest(".ct-kebab"))return ee(t.target.closest(".ct-kebab"));if(!t.target.closest(".ct-select")){if(t.target.closest(".ct-fav")){const s=N(a);s&&q(a,{favourite:s.favourite!==!0});return}F(a)}}),O.addEventListener("change",t=>{if(t.target.matches(".ct-select")&&de(t.target.closest(".ct-row").dataset.id),t.target.matches(".ct-select-all")){const e=S.peek().map(a=>a.id);i.value=t.target.checked?new Set(e):new Set}}),O.addEventListener("keydown",t=>{const e=t.target.closest?.(".ct-row");e&&t.key==="Enter"&&t.target===e&&F(e.dataset.id)});const me=t=>{t.ctrlKey||t.metaKey||t.altKey||t.target.closest?.("input, textarea, select, [contenteditable], .dm-slideover")||(t.key==="/"?(t.preventDefault(),se.value=!0):(t.key==="n"||t.key==="N")&&(t.preventDefault(),F(null)))};document.addEventListener("keydown",me),k.add(()=>document.removeEventListener("keydown",me)),I.scan(r),await W()}};
@@ -52,4 +52,4 @@ import{api as v}from"../api.js";import{makeIconInput as B}from"/admin/js/lib/sho
52
52
  <button class="btn btn-sm btn-ghost ctx-item-down" data-i="${i}" ${i===a.length-1?"disabled":""}>Down</button>
53
53
  <button class="btn btn-sm btn-ghost ctx-item-remove" data-i="${i}">Remove</button>
54
54
  </div>
55
- </div>`;const w=r.querySelector(".ctx-icon-slot");if(w){const l=B("edit",e.icon||"");l.input.className+=" ctx-item-icon",l.input.dataset.i=String(i),w.appendChild(l.el)}const N=r.querySelector(".ctx-url-slot");if(N){const l=L(o.placeholder,e.action?.url||"");l.input.className+=" ctx-item-arg",l.input.dataset.i=String(i),l.input.dataset.key="url",N.appendChild(l.el)}t.appendChild(r)}),Domma.icons.scan("#ctx-items"),p()}c.on("change",".ctx-item-type",function(){const t=Number(this.dataset.i);a[t].type=this.value,this.value!=="item"?delete a[t].action:a[t].action||(a[t].action={type:"navigate"}),u()}),c.on("input",".ctx-item-text",function(){a[Number(this.dataset.i)].text=this.value,p()}),c.on("input",".ctx-item-icon",function(){a[Number(this.dataset.i)].icon=this.value,p()}),c.on("change",".ctx-item-action",function(){const t=Number(this.dataset.i);a[t].action={type:this.value},u()}),c.on("input",".ctx-item-arg",function(){const t=Number(this.dataset.i);a[t].action=a[t].action||{},a[t].action[this.dataset.key]=this.value,p()}),c.on("change",".ctx-item-danger",function(){a[Number(this.dataset.i)].danger=this.checked,p()}),c.on("click",".ctx-item-remove",function(){a.splice(Number(this.dataset.i),1),u()}),c.on("click",".ctx-item-up",function(){const t=Number(this.dataset.i);[a[t-1],a[t]]=[a[t],a[t-1]],u()}),c.on("click",".ctx-item-down",function(){const t=Number(this.dataset.i);[a[t+1],a[t]]=[a[t],a[t+1]],u()}),$("#ctx-add-item").on("click",()=>{a.push({type:"item",text:"New item",action:{type:"navigate",url:""}}),u()});function k(){const t={},s=$("#ctx-accent").val(),e=$("#ctx-transition").val(),i=$("#ctx-density").val(),n=$("#ctx-radius").val(),o=$("#ctx-shadow").val(),r=$("#ctx-opacity").val();return s&&(t.accent=s),e&&(t.transition=e),i&&(t.density=i),n&&(t.radius=n),o&&(t.shadow=o),r!==""&&r!=null&&(t.opacity=Number(r)),t}function A(){const t=$("#ctx-inherit").val(),s=j($("#ctx-pages").val()),e=j($("#ctx-projects").val()),i={slug:$("#ctx-slug").val().trim(),name:$("#ctx-name").val().trim(),enabled:$("#ctx-enabled").is(":checked"),target:{selector:$("#ctx-selector").val().trim(),match:$("#ctx-match").val().trim(),exclude:$("#ctx-exclude").val().trim()},inherit:t==="false"?!1:t,items:a},n=k();return Object.keys(n).length&&(i.theme=n),(s.length||e.length)&&(i.binding={},s.length&&(i.binding.pages=s),e.length&&(i.binding.projects=e)),i}E.tooltip&&E.tooltip(".ctx-help",{trigger:"click",position:"right"});function p(){b();const t=a.map(e=>{const i=e.type||"item";return i==="divider"?{type:"divider"}:i==="header"?{type:"header",header:e.text||""}:{label:e.text||"(no text)",icon:e.icon||void 0,danger:!!e.danger}});if(!t.some(e=>e.label))return;const s=k();g=E.contextMenu("#ctx-preview",{items:t,inherit:!1,accent:s.accent,transition:s.transition,density:s.density,radius:s.radius,shadow:s.shadow,opacity:s.opacity})}c.on("change","#ctx-accent, #ctx-transition, #ctx-density, #ctx-radius, #ctx-shadow",p),c.on("input","#ctx-opacity",p);function h(t){const s=document.getElementById("ctx-errors");if(!t){s.style.display="none";return}s.style.display="",s.innerHTML=String(t).split("; ").map(e=>`<div>${d(e)}</div>`).join(""),s.scrollIntoView({behavior:"smooth",block:"nearest"})}$("#ctx-save").on("click",async function(){h(null);const t=A();if(!t.slug)return h("Slug is required");if(!t.name)return h("Name is required");if(!t.target.selector)return h("Container selector is required");this.disabled=!0;try{x?(await v.contextMenus.create(t),E.toast("Context menu created.",{type:"success"}),location.hash="#/context-menus"):(await v.contextMenus.update(y,t),E.toast("Context menu saved.",{type:"success"}))}catch(s){h(s.message||String(s))}finally{this.disabled=!1}}),u(),Domma.icons.scan(c.get(0))},onUnmount(){E.contextMenu&&E.contextMenu.closeAll&&E.contextMenu.closeAll(),b()}};
55
+ </div>`;const w=r.querySelector(".ctx-icon-slot");if(w){const l=B("edit",e.icon||"");l.input.className+=" ctx-item-icon",l.input.dataset.i=String(i),w.appendChild(l.el)}const N=r.querySelector(".ctx-url-slot");if(N){const l=L(o.placeholder,e.action?.url||"");l.input.className+=" ctx-item-arg",l.input.dataset.i=String(i),l.input.dataset.key="url",N.appendChild(l.el)}t.appendChild(r)}),Domma.icons.scan("#ctx-items"),p()}c.on("change",".ctx-item-type",function(){const t=Number(this.dataset.i);a[t].type=this.value,this.value!=="item"?delete a[t].action:a[t].action||(a[t].action={type:"navigate"}),u()}),c.on("input",".ctx-item-text",function(){a[Number(this.dataset.i)].text=this.value,p()}),c.on("input",".ctx-item-icon",function(){a[Number(this.dataset.i)].icon=this.value,p()}),c.on("change",".ctx-item-action",function(){const t=Number(this.dataset.i);a[t].action={type:this.value},u()}),c.on("input",".ctx-item-arg",function(){const t=Number(this.dataset.i);a[t].action=a[t].action||{},a[t].action[this.dataset.key]=this.value,p()}),c.on("change",".ctx-item-danger",function(){a[Number(this.dataset.i)].danger=this.checked,p()}),c.on("click",".ctx-item-remove",function(){a.splice(Number(this.dataset.i),1),u()}),c.on("click",".ctx-item-up",function(){const t=Number(this.dataset.i);[a[t-1],a[t]]=[a[t],a[t-1]],u()}),c.on("click",".ctx-item-down",function(){const t=Number(this.dataset.i);[a[t+1],a[t]]=[a[t],a[t+1]],u()}),$("#ctx-add-item").on("click",()=>{a.push({type:"item",text:"New item",action:{type:"navigate",url:""}}),u()});function k(){const t={},s=$("#ctx-accent").val(),e=$("#ctx-transition").val(),i=$("#ctx-density").val(),n=$("#ctx-radius").val(),o=$("#ctx-shadow").val(),r=$("#ctx-opacity").val();return s&&(t.accent=s),e&&(t.transition=e),i&&(t.density=i),n&&(t.radius=n),o&&(t.shadow=o),r!==""&&r!=null&&(t.opacity=Number(r)),t}function A(){const t=$("#ctx-inherit").val(),s=j($("#ctx-pages").val()),e=j($("#ctx-projects").val()),i={slug:$("#ctx-slug").val().trim(),name:$("#ctx-name").val().trim(),enabled:$("#ctx-enabled").is(":checked"),target:{selector:$("#ctx-selector").val().trim(),match:$("#ctx-match").val().trim(),exclude:$("#ctx-exclude").val().trim()},inherit:t==="false"?!1:t,items:a},n=k();return Object.keys(n).length&&(i.theme=n),(s.length||e.length)&&(i.binding={},s.length&&(i.binding.pages=s),e.length&&(i.binding.projects=e)),i}E.tooltip&&E.tooltip(".ctx-help",{trigger:"click",position:"right"});function p(){b();const t=a.map(e=>{const i=e.type||"item";return i==="divider"?{type:"divider"}:i==="header"?{type:"header",header:e.text||""}:{label:e.text||"(no text)",icon:e.icon||void 0,danger:!!e.danger}});if(!t.some(e=>e.label))return;const s=k();g=E.contextMenu("#ctx-preview",{cursor:"glyph",items:t,inherit:!1,accent:s.accent,transition:s.transition,density:s.density,radius:s.radius,shadow:s.shadow,opacity:s.opacity})}c.on("change","#ctx-accent, #ctx-transition, #ctx-density, #ctx-radius, #ctx-shadow",p),c.on("input","#ctx-opacity",p);function h(t){const s=document.getElementById("ctx-errors");if(!t){s.style.display="none";return}s.style.display="",s.innerHTML=String(t).split("; ").map(e=>`<div>${d(e)}</div>`).join(""),s.scrollIntoView({behavior:"smooth",block:"nearest"})}$("#ctx-save").on("click",async function(){h(null);const t=A();if(!t.slug)return h("Slug is required");if(!t.name)return h("Name is required");if(!t.target.selector)return h("Container selector is required");this.disabled=!0;try{x?(await v.contextMenus.create(t),E.toast("Context menu created.",{type:"success"}),location.hash="#/context-menus"):(await v.contextMenus.update(y,t),E.toast("Context menu saved.",{type:"success"}))}catch(s){h(s.message||String(s))}finally{this.disabled=!1}}),u(),Domma.icons.scan(c.get(0))},onUnmount(){E.contextMenu&&E.contextMenu.closeAll&&E.contextMenu.closeAll(),b()}};
@@ -101,4 +101,4 @@ import{apiRequest as te}from"../api.js";import{addBannerAction as ae}from"../lib
101
101
  <span class="spacer"></span>
102
102
  <button type="button" class="btn btn-sm btn-primary" data-on-click="done">Done</button>
103
103
  </div>
104
- </div>`),a=(c,f=w=>w)=>M.computed({read:()=>o.value[c],write:w=>x(c,f(w))}),s={layout:a("layout"),sort:a("sort"),previewLength:a("previewLength",Number),showCategories:a("showCategories"),confirmDelete:a("confirmDelete"),previewText:M.computed(()=>o.value.previewLength?`${o.value.previewLength} ch`:"Off")};e.element.appendChild(t);let n;const g=M.applyBindings(s,t,{methods:{done:()=>e.close(),reset:()=>{o.value={...re},n?.setValue([])}}});n=E.pillbox(t.querySelector(".notes-set-defaults"),{data:k.peek().map(c=>({value:c,label:c})),value:o.peek().defaultCategories,creatable:!0,placeholder:"None",onChange:()=>x("defaultCategories",n.getValue())});const p=S(t);B(e,()=>{g.dispose(),p(),n.destroy?.()}),e.open()}ae(v,{id:"notes-settings",icon:"settings",title:"Notes settings",onClick:X});const N=i.querySelector(".notes-list"),Z=E.contextMenu(N,{match:".note-card",inherit:!1,density:"compact",items:e=>{const t=e.dataset.id,s=b(t)?.categories||[];return[{label:"Edit",icon:"edit",action:()=>h(b(t))},{label:"Duplicate",icon:"copy",action:()=>G(t)},{label:"Copy text",icon:"clipboard",action:()=>J(t)},{label:"Show only\u2026",icon:"filter",disabled:s.length===0,submenu:s.map(n=>({label:n,icon:"tag",action:()=>{u.value=n}}))},{type:"divider"},{label:"Delete",icon:"trash",danger:!0,action:()=>q(t)}]}});d.add(()=>Z?.destroy?.()),N.addEventListener("click",e=>{const t=e.target.closest(".note-card");if(!t)return;const a=e.target.closest(".note-cat");if(a){u.value=a.dataset.cat;return}const s=e.target.closest(".note-kebab");if(s){se(s);return}h(b(t.dataset.id))}),N.addEventListener("keydown",e=>{const t=e.target.closest?.(".note-card");t&&e.key==="Enter"&&e.target===t&&h(b(t.dataset.id))});const F=e=>{e.ctrlKey||e.metaKey||e.altKey||e.target.closest?.("input, textarea, select, [contenteditable], .dm-slideover")||(e.key==="/"?(e.preventDefault(),A.value=!0):(e.key==="n"||e.key==="N")&&(e.preventDefault(),h(null)))};document.addEventListener("keydown",F),d.add(()=>document.removeEventListener("keydown",F)),d.add(()=>y?.close()),I.scan(i),await _()}};
104
+ </div>`),a=(c,f=w=>w)=>M.computed({read:()=>o.value[c],write:w=>x(c,f(w))}),s={layout:a("layout"),sort:a("sort"),previewLength:a("previewLength",Number),showCategories:a("showCategories"),confirmDelete:a("confirmDelete"),previewText:M.computed(()=>o.value.previewLength?`${o.value.previewLength} ch`:"Off")};e.element.appendChild(t);let n;const g=M.applyBindings(s,t,{methods:{done:()=>e.close(),reset:()=>{o.value={...re},n?.setValue([])}}});n=E.pillbox(t.querySelector(".notes-set-defaults"),{data:k.peek().map(c=>({value:c,label:c})),value:o.peek().defaultCategories,creatable:!0,placeholder:"None",onChange:()=>x("defaultCategories",n.getValue())});const p=S(t);B(e,()=>{g.dispose(),p(),n.destroy?.()}),e.open()}ae(v,{id:"notes-settings",icon:"settings",title:"Notes settings",onClick:X});const N=i.querySelector(".notes-list"),Z=E.contextMenu(N,{cursor:"glyph",match:".note-card",inherit:!1,density:"compact",items:e=>{const t=e.dataset.id,s=b(t)?.categories||[];return[{label:"Edit",icon:"edit",action:()=>h(b(t))},{label:"Duplicate",icon:"copy",action:()=>G(t)},{label:"Copy text",icon:"clipboard",action:()=>J(t)},{label:"Show only\u2026",icon:"filter",disabled:s.length===0,submenu:s.map(n=>({label:n,icon:"tag",action:()=>{u.value=n}}))},{type:"divider"},{label:"Delete",icon:"trash",danger:!0,action:()=>q(t)}]}});d.add(()=>Z?.destroy?.()),N.addEventListener("click",e=>{const t=e.target.closest(".note-card");if(!t)return;const a=e.target.closest(".note-cat");if(a){u.value=a.dataset.cat;return}const s=e.target.closest(".note-kebab");if(s){se(s);return}h(b(t.dataset.id))}),N.addEventListener("keydown",e=>{const t=e.target.closest?.(".note-card");t&&e.key==="Enter"&&e.target===t&&h(b(t.dataset.id))});const F=e=>{e.ctrlKey||e.metaKey||e.altKey||e.target.closest?.("input, textarea, select, [contenteditable], .dm-slideover")||(e.key==="/"?(e.preventDefault(),A.value=!0):(e.key==="n"||e.key==="N")&&(e.preventDefault(),h(null)))};document.addEventListener("keydown",F),d.add(()=>document.removeEventListener("keydown",F)),d.add(()=>y?.close()),I.scan(i),await _()}};
@@ -93,4 +93,4 @@ import{apiRequest as at}from"../api.js";import{addBannerAction as st}from"../lib
93
93
  <span class="spacer"></span>
94
94
  <button type="button" class="btn btn-sm btn-primary" data-on-click="done">Done</button>
95
95
  </div>
96
- </div>`),s=r=>M.computed({read:()=>n.value[r],write:a=>_(r,a)}),i=Object.fromEntries(Object.keys(H).map(r=>[r,s(r)]));e.element.appendChild(o);const h=O(o),$=M.applyBindings(i,o,{methods:{done:()=>e.close(),reset:()=>{n.value={...H}}}});j(e,()=>{$.dispose(),h()}),e.open()}st(b,{id:"todo-settings",icon:"settings",title:"Todo settings",onClick:tt}),d.querySelector(".todo-rows").innerHTML=vt,p.add(O(d,{position:"bottom"}));const et=M.applyBindings(W,d,{methods:{add:X,clearCompleted:Z}});p.add(()=>et.dispose());const L=d.querySelector(".todo-list"),ot=E.contextMenu(L,{match:".todo-row",inherit:!1,density:"compact",items:e=>{const t=e.dataset.id,o=C(t);return o?[{label:"Edit\u2026",icon:"edit",action:()=>Y(t)},{label:o.status==="completed"?"Reopen":"Complete",icon:"check",action:()=>F(t)},{type:"divider"},{label:"Status",icon:"circle",submenu:B.map(s=>({label:u[s],disabled:o.status===s,action:()=>c(t,{status:s})}))},{label:"Priority",icon:"flag",submenu:P.map(s=>({label:u[s],disabled:o.priority===s,action:()=>c(t,{priority:s})}))},{label:"Due",icon:"calendar",submenu:[{label:"Today",action:()=>c(t,{dueAt:k(0)})},{label:"Tomorrow",action:()=>c(t,{dueAt:k(1)})},{label:"Next week",action:()=>c(t,{dueAt:k(7)})},{type:"divider"},{label:"No due date",disabled:!o.dueAt,action:()=>c(t,{dueAt:null})}]},{type:"divider"},{label:"Delete",icon:"trash",danger:!0,action:()=>q(t)}]:[]}});p.add(()=>ot?.destroy?.()),L.addEventListener("click",e=>{const t=e.target.closest(".todo-row");if(t){if(e.target.closest(".todo-kebab, .todo-pill"))return lt(e.target.closest(".todo-kebab, .todo-pill"));e.target.closest(".todo-text")&&Y(t.dataset.id)}}),L.addEventListener("change",e=>{e.target.matches(".todo-check")&&F(e.target.closest(".todo-row").dataset.id)});const R=e=>{e.ctrlKey||e.metaKey||e.altKey||e.target.closest?.("input, textarea, select, [contenteditable], .dm-slideover")||(e.key==="/"?(e.preventDefault(),N.value=!0):(e.key==="n"||e.key==="N")&&(e.preventDefault(),A.value=!0))};document.addEventListener("keydown",R),p.add(()=>document.removeEventListener("keydown",R)),I.scan(d),await V()}};
96
+ </div>`),s=r=>M.computed({read:()=>n.value[r],write:a=>_(r,a)}),i=Object.fromEntries(Object.keys(H).map(r=>[r,s(r)]));e.element.appendChild(o);const h=O(o),$=M.applyBindings(i,o,{methods:{done:()=>e.close(),reset:()=>{n.value={...H}}}});j(e,()=>{$.dispose(),h()}),e.open()}st(b,{id:"todo-settings",icon:"settings",title:"Todo settings",onClick:tt}),d.querySelector(".todo-rows").innerHTML=vt,p.add(O(d,{position:"bottom"}));const et=M.applyBindings(W,d,{methods:{add:X,clearCompleted:Z}});p.add(()=>et.dispose());const L=d.querySelector(".todo-list"),ot=E.contextMenu(L,{cursor:"glyph",match:".todo-row",inherit:!1,density:"compact",items:e=>{const t=e.dataset.id,o=C(t);return o?[{label:"Edit\u2026",icon:"edit",action:()=>Y(t)},{label:o.status==="completed"?"Reopen":"Complete",icon:"check",action:()=>F(t)},{type:"divider"},{label:"Status",icon:"circle",submenu:B.map(s=>({label:u[s],disabled:o.status===s,action:()=>c(t,{status:s})}))},{label:"Priority",icon:"flag",submenu:P.map(s=>({label:u[s],disabled:o.priority===s,action:()=>c(t,{priority:s})}))},{label:"Due",icon:"calendar",submenu:[{label:"Today",action:()=>c(t,{dueAt:k(0)})},{label:"Tomorrow",action:()=>c(t,{dueAt:k(1)})},{label:"Next week",action:()=>c(t,{dueAt:k(7)})},{type:"divider"},{label:"No due date",disabled:!o.dueAt,action:()=>c(t,{dueAt:null})}]},{type:"divider"},{label:"Delete",icon:"trash",danger:!0,action:()=>q(t)}]:[]}});p.add(()=>ot?.destroy?.()),L.addEventListener("click",e=>{const t=e.target.closest(".todo-row");if(t){if(e.target.closest(".todo-kebab, .todo-pill"))return lt(e.target.closest(".todo-kebab, .todo-pill"));e.target.closest(".todo-text")&&Y(t.dataset.id)}}),L.addEventListener("change",e=>{e.target.matches(".todo-check")&&F(e.target.closest(".todo-row").dataset.id)});const R=e=>{e.ctrlKey||e.metaKey||e.altKey||e.target.closest?.("input, textarea, select, [contenteditable], .dm-slideover")||(e.key==="/"?(e.preventDefault(),N.value=!0):(e.key==="n"||e.key==="N")&&(e.preventDefault(),A.value=!0))};document.addEventListener("keydown",R),p.add(()=>document.removeEventListener("keydown",R)),I.scan(d),await V()}};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.69.4",
3
+ "version": "0.70.0",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -82,7 +82,7 @@
82
82
  "@fastify/rate-limit": "^10.3.0",
83
83
  "@fastify/static": "9.1.1",
84
84
  "bcryptjs": "^3.0.3",
85
- "domma-js": "0.44.0",
85
+ "domma-js": "0.44.1",
86
86
  "dotenv": "^17.2.3",
87
87
  "fastify": "5.8.5",
88
88
  "gray-matter": "^4.0.3",
@@ -16,10 +16,11 @@ Public-facing blog for the CMS. Supports posts with categories, tags, and option
16
16
  - `lib/render.js` - blocks I/O: seed, load (site copy, else shipped), render cards
17
17
  - `lib/page.js` - a post page through its layout; shared by the public route and `POST /preview`
18
18
  - `lib/samples.js` - the sample posts and categories
19
+ - `lib/nav-link.js` - the navbar Blog link added on enable, removed on disable
19
20
  - `blocks/*.html|css` - the shipped layouts, copied into content/blocks/ once
20
21
  - `public/blog.js`, `public/blog.css` - the browser half (listing, right-click menu, post extras)
21
22
  - `public/samples/*.svg` - sample covers
22
- - `tests/layouts.test.js`, `tests/public.test.js`
23
+ - `tests/layouts.test.js`, `tests/public.test.js`, `tests/nav-link.test.js`
23
24
  - `admin/views/blog.js` - post list (KPIs + status/category/search filters)
24
25
  - `admin/views/post-editor.js` - create / edit a post
25
26
  - `admin/views/categories.js`, `admin/views/comments.js`, `admin/views/settings.js`
@@ -85,7 +86,7 @@ Registered at the Fastify root (no `/api/plugins/blog` prefix). Base path defaul
85
86
  | `publishedAt` | ISO string | Set on publish or schedule |
86
87
  | `seo.title` | string | SEO title override |
87
88
  | `seo.description` | string | SEO description override |
88
- | `template` | string | Post layout block (`blog-post-*`); `''` = the site default |
89
+ | `template` | string | Post layout block (`blog-post-*`: classic, feature, minimal, sidebar, split, essay); `''` = the site default |
89
90
  | `sample` | boolean | Added by "Add sample posts"; removed by "Remove" |
90
91
 
91
92
  ### categories
@@ -198,6 +199,26 @@ with `?display=` and swaps the cards, since each display is different block mark
198
199
  `Collection "blog-posts" not found` from every route. Existing sites were
199
200
  unaffected only because the collections were already on disk.
200
201
 
202
+ - **Enabling adds a Blog link to the navbar** (`onEnable` in plugin.js, `lib/nav-link.js`): appended to
203
+ the menu in the `navbar` slot, with an icon only if most of its links have one, and skipped if any item
204
+ already points at the base path. What it added is remembered as `navLink: {menu, url}` in the plugin's
205
+ state - NOT as a marker on the item, because the Menus editor rewrites items with the keys it knows and
206
+ drops the rest. `onDisable` removes a top-level item with that url, and only when `navLink` says the
207
+ plugin added it, so a site's own Blog link is never touched. Sites where the blog was already enabled
208
+ get no link; there is no enable event for them. A later change of `basePath` does not move the link.
209
+
210
+ - **The admin already does Ctrl+S.** app.js clicks the view's primary Save button and calls
211
+ `preventDefault()`. The editor's own handler saved as well, so every Ctrl+S was two PUTs - and on a
212
+ new post, two POSTs and a duplicate post. It now steps aside when `e.defaultPrevented`.
213
+
214
+ - **Categories and tags are Domma Pillboxes** (`mountPills()` in post-editor.js). Pillbox's script
215
+ emits `.dm-pill` / `.dm-pill-remove` / `.dm-pillbox-empty` but Domma's elements.css styles
216
+ `.dm-pillbox-pill` / `-remove` / `-message`, so admin/css/index.css carries the pill styles until
217
+ domma-js agrees with itself. Pills render labels as TEXT (`renderPill` returns a node) - the default
218
+ `pillTemplate` goes through innerHTML. Tags match case-insensitively, reuse an existing spelling,
219
+ and take commas, pastes and text left in the box. A category id with no category left is kept on
220
+ save, not dropped. The Status card's dates are domma-reactive (`mountStatusCard()`).
221
+
201
222
  - **The post editor reads its id off the HASH**: Domma's router hands `onMount`
202
223
  one argument and never the params, and `R.current` is a method, so
203
224
  `R.current?.params?.id` was always undefined - the editor opened blank on
@@ -133,6 +133,32 @@
133
133
  /* The old settings page renders the same panel in a card. */
134
134
  .blog-settings-card { max-width: 600px; }
135
135
 
136
+ /* ── Post editor: status stamps, category / tag pills ─────────────────── */
137
+ .blog-stamps { margin: .85rem 0 0; font-size: var(--dm-text-sm, .875rem); }
138
+ .blog-stamp { display: grid; grid-template-columns: 5.5rem minmax(0, 1fr); gap: .5rem; padding: .3rem 0; border-top: 1px solid var(--dm-border); }
139
+ .blog-stamp dt { color: var(--dm-text-muted); font-weight: 400; }
140
+ .blog-stamp dd { margin: 0; color: var(--dm-text); }
141
+ .blog-stamp-ago { color: var(--dm-text-muted); white-space: nowrap; }
142
+ .blog-stamp-none { margin: .75rem 0 0; font-size: var(--dm-text-sm, .875rem); color: var(--dm-text-muted); }
143
+ .blog-field-note { margin: .4rem 0 0; font-size: var(--dm-text-xs, .75rem); color: var(--dm-text-muted); }
144
+ .blog-editor-side .dm-pillbox { width: 100%; }
145
+ /* The suggestion list hangs below its card; a clipping card cut it off. */
146
+ .blog-editor-side .card:has(.dm-pillbox) { overflow: visible; }
147
+ /* Domma's Pillbox script emits .dm-pill / .dm-pill-remove / .dm-pillbox-empty,
148
+ but elements.css styles .dm-pillbox-pill / -remove / -message - so until the
149
+ two agree the pills arrive unstyled. These mirror Domma's own rules. */
150
+ .blog-editor-side .dm-pill { display: inline-flex; align-items: center; gap: .25rem; padding: .2rem .3rem .2rem .6rem;
151
+ background: var(--dm-primary-light); color: var(--dm-text); border-radius: var(--dm-radius-full); font-size: var(--dm-font-size-sm); line-height: 1.2; }
152
+ .blog-editor-side .dm-pill-remove { display: inline-flex; align-items: center; justify-content: center; width: 1.1rem; height: 1.1rem; padding: 0;
153
+ background: none; border: 0; border-radius: 50%; color: inherit; cursor: pointer; opacity: .65; font-size: 1rem; line-height: 1; }
154
+ .blog-editor-side .dm-pill-remove:hover { opacity: 1; background: color-mix(in srgb, currentColor 15%, transparent); }
155
+ .blog-editor-side .dm-pillbox-container:focus-within { border-color: var(--dm-primary); box-shadow: 0 0 0 3px var(--dm-focus-ring); }
156
+ .blog-editor-side .dm-pillbox-input { min-width: 6rem; font-size: var(--dm-font-size-sm); }
157
+ .blog-editor-side .dm-pillbox-options { list-style: none; margin: 0; padding: .25rem 0; }
158
+ .blog-editor-side .dm-pillbox-option { font-size: var(--dm-font-size-sm); padding: .4rem .75rem; }
159
+ .blog-editor-side .dm-pillbox-option:hover { background-color: var(--dm-hover-bg); }
160
+ .blog-editor-side .dm-pillbox-empty { padding: .6rem .75rem; color: var(--dm-text-muted); font-size: var(--dm-font-size-sm); }
161
+
136
162
  /* ── Post editor: layout picker ─────────────────────────────────────── */
137
163
  .blog-card-head { display: flex; align-items: center; gap: .25rem; }
138
164
  .blog-preview-btn { margin-left: auto; }
@@ -149,6 +175,8 @@
149
175
  .blog-thumb[data-thumb="feature"] { --thumb: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2060%2040'%3E%3Crect%20x='0'%20y='0'%20width='60'%20height='20'%20rx='1'%20opacity='0.45'/%3E%3Crect%20x='6'%20y='11'%20width='30'%20height='3.2'%20rx='1'%20opacity='1'/%3E%3Crect%20x='6'%20y='16'%20width='20'%20height='1.8'%20rx='1'%20opacity='0.8'/%3E%3Crect%20x='14'%20y='24.0'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='27.2'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='30.4'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='33.6'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='36.8'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3C/svg%3E"); }
150
176
  .blog-thumb[data-thumb="minimal"] { --thumb: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2060%2040'%3E%3Crect%20x='20'%20y='5'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='9'%20width='22'%20height='3'%20rx='1'%20opacity='1'/%3E%3Crect%20x='20'%20y='16.0'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='19.2'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='22.4'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='25.6'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='28.8'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='32.0'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='35.2'%20width='20'%20height='1.5'%20rx='1'%20opacity='0.6'/%3E%3C/svg%3E"); }
151
177
  .blog-thumb[data-thumb="sidebar"] { --thumb: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2060%2040'%3E%3Crect%20x='4'%20y='4'%20width='30'%20height='3'%20rx='1'%20opacity='1'/%3E%3Crect%20x='4'%20y='10'%20width='34'%20height='9'%20rx='1'%20opacity='0.45'/%3E%3Crect%20x='4'%20y='22.0'%20width='34'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='4'%20y='25.2'%20width='34'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='4'%20y='28.4'%20width='34'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='4'%20y='31.6'%20width='34'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='4'%20y='34.8'%20width='34'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='42'%20y='10'%20width='14'%20height='10'%20rx='1'%20opacity='0.35'/%3E%3Crect%20x='43'%20y='23'%20width='10'%20height='1.4'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='43'%20y='26'%20width='10'%20height='1.4'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='43'%20y='29'%20width='10'%20height='1.4'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='43'%20y='32'%20width='10'%20height='1.4'%20rx='1'%20opacity='0.6'/%3E%3C/svg%3E"); }
178
+ .blog-thumb[data-thumb="split"] { --thumb: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2060%2040'%3E%3Crect%20x='4'%20y='6'%20width='24'%20height='28'%20rx='1'%20opacity='0.45'/%3E%3Crect%20x='32'%20y='11'%20width='24'%20height='3'%20rx='1'%20opacity='1'/%3E%3Crect%20x='32'%20y='16'%20width='18'%20height='3'%20rx='1'%20opacity='1'/%3E%3Crect%20x='32'%20y='22'%20width='22'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='32'%20y='25'%20width='20'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='32'%20y='30.5'%20width='1.2'%20height='4'%20rx='1'%20opacity='1'/%3E%3Crect%20x='34.5'%20y='31'%20width='12'%20height='1.4'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='34.5'%20y='33.4'%20width='9'%20height='1.4'%20rx='1'%20opacity='0.6'/%3E%3C/svg%3E"); }
179
+ .blog-thumb[data-thumb="essay"] { --thumb: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2060%2040'%3E%3Crect%20x='14'%20y='3'%20width='32'%20height='4'%20rx='1'%20opacity='1'/%3E%3Crect%20x='18'%20y='9.5'%20width='24'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='13.2'%20width='20'%20height='0.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='24'%20y='14.6'%20width='12'%20height='1.2'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='20'%20y='16.6'%20width='20'%20height='0.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='20.5'%20width='4'%20height='4'%20rx='1'%20opacity='0.9'/%3E%3Crect%20x='19.5'%20y='20.5'%20width='26.5'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='19.5'%20y='23.3'%20width='26.5'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='26.5'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='29.5'%20width='32'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='14'%20y='32.5'%20width='23'%20height='1.6'%20rx='1'%20opacity='0.6'/%3E%3Crect%20x='38.5'%20y='32.5'%20width='1.6'%20height='1.6'%20rx='1'%20opacity='1'/%3E%3C/svg%3E"); }
152
180
  .blog-thumb[data-thumb="custom"] { --thumb: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2060%2040'%3E%3Crect%20x='6'%20y='5'%20width='48'%20height='30'%20rx='3'%20fill='none'%20stroke='%23000'%20stroke-width='1.6'%20stroke-dasharray='3%202.4'/%3E%3Crect%20x='28.8'%20y='14'%20width='2.4'%20height='12'%20rx='1'%20opacity='1'/%3E%3Crect%20x='24'%20y='18.8'%20width='12'%20height='2.4'%20rx='1'%20opacity='1'/%3E%3C/svg%3E"); }
153
181
  .blog-layout-desc { margin: .6rem 0 0; font-size: var(--dm-text-xs); color: var(--dm-text-muted); min-height: 2.2em; }
154
182
 
@@ -68,14 +68,29 @@
68
68
 
69
69
  <!-- Sidebar -->
70
70
  <div class="blog-editor-side">
71
- <div class="card mb-3">
72
- <div class="card-header">Status <span data-help="Save Draft keeps the post private. Publish Now puts it live. Schedule publishes it at the date and time you choose."></span></div>
71
+ <div class="card mb-3" id="status-card">
72
+ <div class="card-header">Status <span data-help="Save keeps the post as it is. Publish puts it live now; Schedule publishes it at the date and time you choose. The dates below come from the last save."></span></div>
73
73
  <div class="card-body">
74
74
  <div id="schedule-picker" class="mb-3 blog-hidden">
75
75
  <label class="form-label" for="post-scheduled-at">Scheduled for</label>
76
76
  <input type="datetime-local" id="post-scheduled-at" class="form-input">
77
77
  </div>
78
- <span id="post-status-badge" class="badge badge-secondary">draft</span>
78
+ <span id="post-status-badge" class="badge" data-bind-class="statusClass.value" data-bind-text="status.value">draft</span>
79
+ <dl class="blog-stamps">
80
+ <div class="blog-stamp" data-bind-class="liveWhen.value ? '' : 'blog-hidden'">
81
+ <dt data-bind-text="liveLabel.value"></dt>
82
+ <dd><span data-bind-text="liveWhen.value"></span> <span class="blog-stamp-ago" data-bind-text="liveAgo.value"></span></dd>
83
+ </div>
84
+ <div class="blog-stamp" data-bind-class="savedWhen.value ? '' : 'blog-hidden'">
85
+ <dt>Last saved</dt>
86
+ <dd><span data-bind-text="savedWhen.value"></span> <span class="blog-stamp-ago" data-bind-text="savedAgo.value"></span></dd>
87
+ </div>
88
+ <div class="blog-stamp" data-bind-class="createdWhen.value ? '' : 'blog-hidden'">
89
+ <dt>Created</dt>
90
+ <dd><span data-bind-text="createdWhen.value"></span></dd>
91
+ </div>
92
+ </dl>
93
+ <p class="blog-stamp-none" data-bind-class="createdWhen.value ? 'blog-hidden' : ''">Not saved yet.</p>
79
94
  </div>
80
95
  </div>
81
96
 
@@ -94,17 +109,18 @@
94
109
  </div>
95
110
 
96
111
  <div class="card mb-3">
97
- <div class="card-header">Categories <span data-help="Manage the list under Blog > Categories. A post can be in several."></span></div>
98
- <div class="card-body" id="categories-checklist">
99
- <p class="text-muted text-sm">Loading…</p>
112
+ <div class="card-header">Categories <span data-help="Type to find a category, or click the box to see them all. A post can be in several; the x on a pill takes it out. Manage the list under Blog > Categories."></span></div>
113
+ <div class="card-body">
114
+ <input type="text" id="post-categories" aria-label="Categories">
115
+ <p class="blog-field-note blog-hidden" id="categories-note">No categories yet - add some under Blog &gt; Categories.</p>
100
116
  </div>
101
117
  </div>
102
118
 
103
119
  <div class="card mb-3">
104
- <div class="card-header">Tags <span data-help="Free-form labels, comma-separated. Each tag gets its own page on the site."></span></div>
120
+ <div class="card-header">Tags <span data-help="Free-form labels. Type a tag and press Enter or a comma; tags already used on other posts are suggested as you type. Each tag gets its own page on the site."></span></div>
105
121
  <div class="card-body">
106
- <input type="text" id="post-tags" class="form-input" placeholder="tag1, tag2, …">
107
- <div class="text-muted text-sm mt-1">Comma-separated</div>
122
+ <input type="text" id="post-tags" aria-label="Tags">
123
+ <p class="blog-field-note">Enter or a comma adds a tag.</p>
108
124
  </div>
109
125
  </div>
110
126
 
@@ -237,6 +237,7 @@ export const blogView = {
237
237
 
238
238
  // ---- Context menu: every action on a post ----
239
239
  const menu = E.contextMenu(list, {
240
+ cursor: 'glyph',
240
241
  match: '.blog-row',
241
242
  inherit: false,
242
243
  density: 'compact',
@@ -269,6 +270,7 @@ export const blogView = {
269
270
  .catch(() => { /* not shown, then */ });
270
271
  const samples = () => posts.peek().filter(p => p.sample === true).length;
271
272
  const barMenu = E.contextMenu(root.querySelector('.blog-bar-end'), {
273
+ cursor: 'glyph',
272
274
  match: '.blog-more',
273
275
  inherit: false,
274
276
  density: 'compact',
@@ -204,6 +204,7 @@ export const categoriesView = {
204
204
  add(() => handle.dispose());
205
205
 
206
206
  const menu = E.contextMenu(list, {
207
+ cursor: 'glyph',
207
208
  match: '.blog-row',
208
209
  inherit: false,
209
210
  density: 'compact',
@@ -191,6 +191,7 @@ export const commentsView = {
191
191
  });
192
192
 
193
193
  const menu = E.contextMenu(list, {
194
+ cursor: 'glyph',
194
195
  match: '.blog-row',
195
196
  inherit: false,
196
197
  density: 'compact',
@@ -219,7 +219,7 @@ const SETTINGS_PANEL = `
219
219
  </h4>
220
220
  <div class="blog-field">
221
221
  <label class="form-label" for="blog-set-layout">Post layout
222
- <span data-help="How a post looks unless it picks its own in the editor. Classic: title, image, story. Feature: a full-width hero. Minimal: just the words. Sidebar: contents list and details beside the story."></span>
222
+ <span data-help="How a post looks unless it picks its own in the editor. Classic: title, image, story. Feature: a full-width hero. Minimal: just the words. Sidebar: contents list and details beside the story. Split: picture and headline side by side. Essay: a long read set like print."></span>
223
223
  </label>
224
224
  <select id="blog-set-layout" class="form-select" data-model="postLayout.value"
225
225
  data-options="layoutOptions.value" data-options-text="label" data-options-value="id"></select>