domma-cms 1.6.20 → 1.6.22

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.51.10
2
+ * Domma Tools CSS v0.51.11
3
3
  * Dynamic Object Manipulation & Modeling API
4
4
  * (c) 2026 Darryl Waterhouse & DCBW-IT
5
- * Built: 2026-10-11T12:22:34.379Z
6
- * Commit: b8fe5ce
5
+ * Built: 2026-10-12T06:05:33.864Z
6
+ * Commit: a53de2f
7
7
  */
8
8
 
9
9
  /*!
@@ -1,9 +1,9 @@
1
1
  /*!
2
- * Domma Tools v0.51.10
2
+ * Domma Tools v0.51.11
3
3
  * Developer tools: Theme Roller & Page Roller
4
4
  * (c) 2026 Darryl Waterhouse & DCBW-IT
5
- * Built: 2026-10-11T12:22:24.023Z
6
- * Commit: b8fe5ce
5
+ * Built: 2026-10-12T06:05:20.912Z
6
+ * Commit: a53de2f
7
7
  *
8
8
  * Requires: domma.min.js
9
9
  */
@@ -1 +1 @@
1
- import{insertAtCursor as re}from"./markdown-toolbar.js";import{SCRIBE_FIELDS as ae,scribeDefaults as se,scribeLoop as ce,stateToScribeActions as ie,buildScribeShortcode as le}from"./scribe-model.js";const de=20,Q={render:{label:"Render",icon:"edit-3",color:"var(--dm-info,#3b82f6)"},wait:{label:"Wait",icon:"clock",color:"var(--dm-warning,#f59e0b)"},undo:{label:"Undo",icon:"corner-up-left",color:"var(--dm-secondary,#a855f7)"},redraw:{label:"Redraw",icon:"refresh-cw",color:"var(--dm-success,#22c55e)"}},L="padding:.4rem .6rem;background:var(--dm-input-bg,#1a1a1a);border:1px solid var(--dm-border,#333);border-radius:4px;color:var(--dm-text,#eee);font-size:.9em;box-sizing:border-box;width:100%;",pe="display:block;font-size:.7rem;font-weight:600;color:var(--dm-text-muted,#aaa);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem;",ue="display:flex;align-items:center;gap:.5rem;cursor:pointer;font-size:.85em;";export function openScribeComposer(s){const r=me(),n=[];let t=null,p=null;function c(){clearTimeout(t),p?.destroy(),p=null}const h=E.slideover({title:"Scribe Composer",size:"lg",position:"right",onClose:c});Ce();const b=document.createElement("div");b.className="scribe-composer",b.style.cssText="display:flex;flex-direction:column;height:100%;";const y=document.createElement("div");y.style.cssText="padding:1rem 1.25rem;border-bottom:1px solid var(--dm-border,#333);background:var(--dm-background-alt,#0f0f17);";const k=document.createElement("div");k.style.cssText="min-height:3.5rem;padding:.85rem 1rem;background:var(--dm-surface,#1a1a1a);border:1px dashed var(--dm-border,#333);border-radius:6px;font-size:1.25rem;line-height:1.5;overflow-wrap:anywhere;",y.appendChild(k);const B=document.createElement("div");B.style.cssText="display:flex;gap:.5rem;margin-top:.75rem;align-items:center;";const Y=V("rotate-cw","Replay"),q=V("code","Show code"),N=document.createElement("span");N.style.cssText="font-size:.8em;color:var(--dm-text-muted,#aaa);flex:1;text-align:right;margin-right:.5rem;",B.appendChild(Y),B.appendChild(N),B.appendChild(q),y.appendChild(B);const M=document.createElement("pre");M.style.cssText="display:none;margin:.75rem 0 0;padding:.6rem .75rem;background:var(--dm-surface,#0a0a0a);border:1px solid var(--dm-border,#333);border-radius:4px;font-family:monospace;font-size:.78em;white-space:pre-wrap;word-break:break-all;color:var(--dm-text-muted,#bbb);max-height:200px;overflow:auto;",y.appendChild(M),q.addEventListener("click",()=>{const e=M.style.display!=="none";M.style.display=e?"none":"block"}),Y.addEventListener("click",()=>{J(),p?.resume(),p&&(N.textContent="Playing preview")}),b.appendChild(y);const R=document.createElement("div");R.style.cssText="display:flex;flex:1;min-height:0;overflow:hidden;";const F=fe(r,()=>{C(),x()});R.appendChild(F.el);const z=document.createElement("main");z.style.cssText="flex:1;overflow-y:auto;padding:1rem 1.25rem;display:flex;flex-direction:column;";const D=document.createElement("h4");D.textContent="Actions",D.style.cssText="margin:0 0 .75rem;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--dm-text-muted,#aaa);",z.appendChild(D);const A=document.createElement("div");A.style.cssText="display:flex;flex-direction:column;gap:.6rem;",z.appendChild(A);const P=document.createElement("div");P.style.cssText="display:flex;gap:.4rem;margin-top:.9rem;flex-wrap:wrap;",Object.entries(Q).forEach(([e,a])=>{const i=document.createElement("button");i.type="button",i.style.cssText=`padding:.4rem .8rem;background:transparent;border:1px dashed color-mix(in srgb, ${a.color} 40%, transparent);border-radius:4px;color:${a.color};cursor:pointer;font-size:.85em;display:inline-flex;align-items:center;gap:.35rem;`;const l=document.createElement("span");l.setAttribute("data-icon",a.icon),i.appendChild(l),i.appendChild(document.createTextNode(` + ${a.label}`)),i.addEventListener("click",()=>{C(),r.actions.push(he(e)),$(),x()}),P.appendChild(i)}),z.appendChild(P),R.appendChild(z),b.appendChild(R);const T=document.createElement("div");T.style.cssText="display:flex;padding:.75rem 1rem;border-top:1px solid var(--dm-border,#333);background:var(--dm-background-alt,#0f0f17);gap:.5rem;align-items:center;";const S=document.createElement("button");S.type="button",S.className="btn btn-ghost btn-sm",S.textContent="Undo (Ctrl+Z)",S.addEventListener("click",()=>H()),T.appendChild(S);const U=document.createElement("span");U.style.cssText="font-size:.75em;color:var(--dm-text-muted,#aaa);",T.appendChild(U);const G=document.createElement("div");G.style.flex="1",T.appendChild(G);const W=document.createElement("button");W.type="button",W.className="btn btn-ghost",W.textContent="Cancel",W.addEventListener("click",()=>h.close()),T.appendChild(W);const j=document.createElement("button");j.type="button",j.className="btn btn-primary",j.textContent="Insert Scribe",j.addEventListener("click",()=>{re(s,Z()),h.close()}),T.appendChild(j),b.appendChild(T),h.setContent(b),b.addEventListener("keydown",e=>{const a=(e.key||"").toLowerCase();(e.ctrlKey||e.metaKey)&&a==="z"&&!e.shiftKey&&n.length>0&&(e.preventDefault(),e.stopPropagation(),H())}),$(),x(),O(),requestAnimationFrame(()=>{Domma.icons.scan()}),h.open();function C(){n.push(JSON.stringify(r)),n.length>de&&n.shift(),O()}function H(){if(n.length===0){E.toast("Nothing to undo",{type:"info",duration:700});return}const e=JSON.parse(n.pop());Object.assign(r.globals,e.globals),r.actions.length=0,e.actions.forEach(a=>r.actions.push(a)),F.sync(),$(),x(),O(),E.toast("Undone",{type:"info",duration:500})}function O(){U.textContent=n.length===0?"":`${n.length} step${n.length===1?"":"s"} stored`,S.disabled=n.length===0,S.style.opacity=n.length===0?".4":"1"}function x(e=150){clearTimeout(t),t=setTimeout(J,e)}function J(){c(),ee(k),k.style.color="",k.style.fontStyle="",M.textContent=Z();const e=r.actions.filter(o=>o.type==="render"||o.type==="redraw"?(o.text||"").length>0:!0);if(e.length===0){k.textContent="(add an action to preview)",k.style.color="var(--dm-text-muted,#666)",k.style.fontStyle="italic",N.textContent="";return}const a=document.createElement("span");a.className="dm-fx-scribe",k.appendChild(a);const i=r.globals,{effect:l,controls:g,...w}=i;w.actions=ie(e,l);try{p=Domma.effects.scribe(a,w),N.textContent=i.autoStart?`${e.length} action${e.length===1?"":"s"}`:"Press Replay to start"}catch{N.textContent="Preview error"}}function $(){if(ee(A),r.actions.length===0){const e=document.createElement("p");e.textContent="No actions yet - add one from the buttons below.",e.style.cssText="font-size:.85em;color:var(--dm-text-muted,#888);font-style:italic;margin:.5rem 0;",A.appendChild(e);return}r.actions.forEach((e,a)=>{A.appendChild(ne(e,a))}),requestAnimationFrame(()=>Domma.icons.scan())}function ne(e,a){const i=Q[e.type],l=document.createElement("div");l.style.cssText=`border:1px solid var(--dm-border,#333);border-left:3px solid ${i.color};border-radius:4px;padding:.6rem .75rem;background:var(--dm-surface,#15151a);`;const g=document.createElement("div");g.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;";const w=document.createElement("span");w.style.cssText=`display:inline-flex;align-items:center;gap:.3rem;font-size:.72rem;font-weight:700;color:${i.color};text-transform:uppercase;letter-spacing:.06em;`;const o=document.createElement("span");o.setAttribute("data-icon",i.icon),w.appendChild(o),w.appendChild(document.createTextNode(i.label)),g.appendChild(w);const m=document.createElement("span");m.textContent=`#${a+1}`,m.style.cssText="font-size:.7rem;color:var(--dm-text-muted,#888);",g.appendChild(m);const u=document.createElement("div");u.style.flex="1",g.appendChild(u);const d=K("chevron-up","Move up");a===0&&(d.disabled=!0,d.style.opacity=".3"),d.addEventListener("click",()=>X(a,-1)),g.appendChild(d);const f=K("chevron-down","Move down");a===r.actions.length-1&&(f.disabled=!0,f.style.opacity=".3"),f.addEventListener("click",()=>X(a,1)),g.appendChild(f);const v=K("x","Remove");return v.addEventListener("click",()=>{C(),r.actions.splice(a,1),$(),x()}),g.appendChild(v),l.appendChild(g),l.appendChild(oe(e)),l}function X(e,a){const i=e+a;if(i<0||i>=r.actions.length)return;C();const[l]=r.actions.splice(e,1);r.actions.splice(i,0,l),$(),x()}function oe(e){const a=document.createElement("div");if(a.style.cssText="display:flex;flex-direction:column;gap:.5rem;",e.type==="render"||e.type==="redraw"){const o=document.createElement("textarea");o.value=e.text||"",o.placeholder=e.type==="redraw"?"New text - clears everything first, then types this":"Text to type out",o.rows=2,o.style.cssText=L+"resize:vertical;font-family:inherit;min-height:2.2rem;";let m=o.value;o.addEventListener("input",()=>{e.text=o.value,x()}),o.addEventListener("blur",()=>{o.value!==m&&(C(),m=o.value)}),a.appendChild(o);const u=document.createElement("div");u.style.cssText="display:flex;align-items:center;gap:.5rem;font-size:.8em;";const d=document.createElement("label");d.textContent="Effect:",d.style.color="var(--dm-text-muted,#aaa)";const f=document.createElement("select");return f.style.cssText=L+"width:auto;padding:.25rem .4rem;",["inherit","none","fade","bounce","glow"].forEach(v=>{const _=document.createElement("option");_.value=v,_.textContent=v==="inherit"?"Default effect":v.charAt(0).toUpperCase()+v.slice(1),f.appendChild(_)}),f.value=e.effect||"none",f.addEventListener("change",()=>{C(),e.effect=f.value,x()}),u.appendChild(d),u.appendChild(f),a.appendChild(u),a}if(e.type==="wait"){const o=document.createElement("div");o.style.cssText="display:flex;gap:.4rem;align-items:center;";const m=document.createElement("input");m.type="number",m.min="0",m.step="any",m.value=String(e.duration??1e3),m.style.cssText=L+"flex:1;",m.addEventListener("input",()=>{const d=Number(m.value);!Number.isNaN(d)&&d>=0&&(e.duration=d,x())}),m.addEventListener("blur",()=>C()),o.appendChild(m);const u=document.createElement("select");return u.style.cssText=L+"width:auto;",[["ms","ms"],["s","seconds"]].forEach(([d,f])=>{const v=document.createElement("option");v.value=d,v.textContent=f,u.appendChild(v)}),u.value=e.unit||"ms",u.addEventListener("change",()=>{C(),e.unit=u.value,x()}),o.appendChild(u),a.appendChild(o),a}const i=document.createElement("div");i.style.cssText="display:flex;gap:.75rem;align-items:center;flex-wrap:wrap;font-size:.85em;";const l=document.createElement("input");l.type="number",l.min="1",l.value=String(e.count||5),l.style.cssText=L+"width:5rem;",l.addEventListener("input",()=>{const o=parseInt(l.value,10);!Number.isNaN(o)&&o>0&&(e.count=o,x())}),l.addEventListener("blur",()=>C());const g=`undo-mode-${Math.random().toString(36).slice(2)}`;return[["last","Last render"],["all","All text"],["count","Exactly N units"]].forEach(([o,m])=>{const u=document.createElement("label");u.style.cssText="display:inline-flex;align-items:center;gap:.3rem;cursor:pointer;";const d=document.createElement("input");d.type="radio",d.name=g,d.value=o,d.checked=(e.mode||"last")===o,d.addEventListener("change",()=>{d.checked&&(C(),e.mode=o,l.style.display=o==="count"?"":"none",x())}),u.appendChild(d),u.appendChild(document.createTextNode(m)),i.appendChild(u),o==="count"&&(i.appendChild(l),l.style.display=(e.mode||"last")==="count"?"":"none")}),a.appendChild(i),a}function Z(){return le(r.globals,r.actions)}}function me(){return{globals:se(),actions:[{type:"render",text:"We build things that matter.",effect:"inherit"},{type:"wait",duration:1500,unit:"ms"},{type:"redraw",text:"We create solutions that last.",effect:"inherit"},{type:"wait",duration:1500,unit:"ms"}]}}function he(s){switch(s){case"render":return{type:"render",text:"",effect:"inherit"};case"wait":return{type:"wait",duration:1e3,unit:"ms"};case"undo":return{type:"undo",mode:"last",count:5};case"redraw":return{type:"redraw",text:"",effect:"inherit"}}return{type:"render",text:"",effect:"inherit"}}function fe(s,r){const n=document.createElement("aside");n.style.cssText="width:240px;flex-shrink:0;padding:1rem 1.25rem;border-right:1px solid var(--dm-border,#333);overflow-y:auto;display:flex;flex-direction:column;gap:.75rem;background:var(--dm-background-alt,#0c0c12);";const t=document.createElement("h4");t.textContent="Global settings",t.style.cssText="margin:0 0 .25rem;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--dm-text-muted,#aaa);",n.appendChild(t);const p=ae.map(c=>{const h=y=>{r(),s.globals[c.key]=c.key==="loop"?ce(y):y};let b;if(c.type==="checkbox"){const y=ve(s.globals[c.key],h,c.label);n.appendChild(y.wrap),b=y.input}else b=c.type==="select"?ge(c.options,s.globals[c.key],h):c.type==="number"?xe(s.globals[c.key],h,c.min):ye(s.globals[c.key],"",h),n.appendChild(be(c.label,b));return{field:c,input:b}});return{el:n,sync(){p.forEach(({field:c,input:h})=>{c.type==="checkbox"?h.checked=s.globals[c.key]:h.value=String(s.globals[c.key])})}}}function be(s,r){const n=document.createElement("div"),t=document.createElement("label");return t.style.cssText=pe,t.textContent=s,n.appendChild(t),n.appendChild(r),n}function xe(s,r,n=1){const t=document.createElement("input");return t.type="number",t.min=String(n),t.value=String(s),t.style.cssText=L,t.addEventListener("input",()=>{const p=Number(t.value);!Number.isNaN(p)&&p>=n&&r(p)}),t}function ye(s,r,n){const t=document.createElement("input");return t.type="text",t.value=s,t.placeholder=r,t.style.cssText=L,t.addEventListener("input",()=>n(t.value)),t}function ge(s,r,n){const t=document.createElement("select");return t.style.cssText=L,s.forEach(([p,c])=>{const h=document.createElement("option");h.value=p,h.textContent=c,t.appendChild(h)}),t.value=r,t.addEventListener("change",()=>n(t.value)),t}function ve(s,r,n){const t=document.createElement("label");t.style.cssText=ue;const p=document.createElement("input");return p.type="checkbox",p.checked=s,p.addEventListener("change",()=>r(p.checked)),t.appendChild(p),t.appendChild(document.createTextNode(n)),{wrap:t,input:p}}function V(s,r){const n=document.createElement("button");n.type="button",n.className="btn btn-ghost btn-sm",n.style.cssText="display:inline-flex;align-items:center;gap:.35rem;";const t=document.createElement("span");return t.setAttribute("data-icon",s),n.appendChild(t),n.appendChild(document.createTextNode(r)),n}function K(s,r){const n=document.createElement("button");n.type="button",n.title=r,n.style.cssText="padding:.25rem .4rem;background:transparent;border:1px solid var(--dm-border,#333);border-radius:3px;color:var(--dm-text-muted,#aaa);cursor:pointer;display:inline-flex;align-items:center;";const t=document.createElement("span");return t.setAttribute("data-icon",s),n.appendChild(t),n}function ee(s){for(;s.firstChild;)s.removeChild(s.firstChild)}function we(s){return String(s).replace(/&/g,"&amp;").replace(/"/g,"&quot;")}let te=!1;function Ce(){te||(te=!0,I.register("edit-3",{viewBox:"0 0 24 24",paths:["M12 20h9","M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("clock",{viewBox:"0 0 24 24",paths:["M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z","M12 6v6l4 2"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("corner-up-left",{viewBox:"0 0 24 24",paths:["M9 14 4 9l5-5","M20 20v-7a4 4 0 0 0-4-4H4"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("refresh-cw",{viewBox:"0 0 24 24",paths:["M23 4v6h-6","M1 20v-6h6","M3.51 9a9 9 0 0 1 14.85-3.36L23 10","M20.49 15a9 9 0 0 1-14.85 3.36L1 14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("rotate-cw",{viewBox:"0 0 24 24",paths:["M23 4v6h-6","M20.49 15a9 9 0 1 1-2.12-9.36L23 10"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("code",{viewBox:"0 0 24 24",paths:["M16 18l6-6-6-6","M8 6l-6 6 6 6"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("chevron-up",{viewBox:"0 0 24 24",path:"M18 15l-6-6-6 6",stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("chevron-down",{viewBox:"0 0 24 24",path:"M6 9l6 6 6-6",stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("x",{viewBox:"0 0 24 24",paths:["M18 6L6 18","M6 6l12 12"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}))}
1
+ import{insertAtCursor as re}from"./markdown-toolbar.js";import{SCRIBE_FIELDS as ae,scribeDefaults as se,scribeLoop as ce,stateToScribeActions as ie,buildScribeShortcode as le}from"./scribe-model.js";const de=20,Q={render:{label:"Render",icon:"edit-3",color:"var(--dm-info,#3b82f6)"},wait:{label:"Wait",icon:"clock",color:"var(--dm-warning,#f59e0b)"},undo:{label:"Undo",icon:"corner-up-left",color:"var(--dm-secondary,#a855f7)"},redraw:{label:"Redraw",icon:"refresh-cw",color:"var(--dm-success,#22c55e)"}},L="padding:.4rem .6rem;background:var(--dm-input-bg,#1a1a1a);border:1px solid var(--dm-border,#333);border-radius:4px;color:var(--dm-text,#eee);font-size:.9em;box-sizing:border-box;width:100%;",pe="display:block;font-size:.7rem;font-weight:600;color:var(--dm-text-muted,#aaa);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem;",me="display:flex;align-items:center;gap:.5rem;cursor:pointer;font-size:.85em;";export function openScribeComposer(s){const r=ue(),n=[];let t=null,p=null;function c(){clearTimeout(t),p?.destroy(),p=null}const h=E.slideover({title:"Scribe Composer",size:"lg",position:"right",onClose:c});Ce();const b=document.createElement("div");b.className="scribe-composer",b.style.cssText="display:flex;flex-direction:column;height:100%;";const y=document.createElement("div");y.style.cssText="padding:1rem 1.25rem;border-bottom:1px solid var(--dm-border,#333);background:var(--dm-background-alt,#0f0f17);";const k=document.createElement("div");k.style.cssText="min-height:3.5rem;padding:.85rem 1rem;background:var(--dm-surface,#1a1a1a);border:1px dashed var(--dm-border,#333);border-radius:6px;font-size:1.25rem;line-height:1.5;overflow-wrap:normal;word-break:normal;",y.appendChild(k);const B=document.createElement("div");B.style.cssText="display:flex;gap:.5rem;margin-top:.75rem;align-items:center;";const Y=V("rotate-cw","Replay"),q=V("code","Show code"),N=document.createElement("span");N.style.cssText="font-size:.8em;color:var(--dm-text-muted,#aaa);flex:1;text-align:right;margin-right:.5rem;",B.appendChild(Y),B.appendChild(N),B.appendChild(q),y.appendChild(B);const M=document.createElement("pre");M.style.cssText="display:none;margin:.75rem 0 0;padding:.6rem .75rem;background:var(--dm-surface,#0a0a0a);border:1px solid var(--dm-border,#333);border-radius:4px;font-family:monospace;font-size:.78em;white-space:pre-wrap;word-break:break-all;color:var(--dm-text-muted,#bbb);max-height:200px;overflow:auto;",y.appendChild(M),q.addEventListener("click",()=>{const e=M.style.display!=="none";M.style.display=e?"none":"block"}),Y.addEventListener("click",()=>{J(),p?.resume(),p&&(N.textContent="Playing preview")}),b.appendChild(y);const R=document.createElement("div");R.style.cssText="display:flex;flex:1;min-height:0;overflow:hidden;";const F=fe(r,()=>{C(),x()});R.appendChild(F.el);const z=document.createElement("main");z.style.cssText="flex:1;overflow-y:auto;padding:1rem 1.25rem;display:flex;flex-direction:column;";const D=document.createElement("h4");D.textContent="Actions",D.style.cssText="margin:0 0 .75rem;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--dm-text-muted,#aaa);",z.appendChild(D);const A=document.createElement("div");A.style.cssText="display:flex;flex-direction:column;gap:.6rem;",z.appendChild(A);const P=document.createElement("div");P.style.cssText="display:flex;gap:.4rem;margin-top:.9rem;flex-wrap:wrap;",Object.entries(Q).forEach(([e,a])=>{const i=document.createElement("button");i.type="button",i.style.cssText=`padding:.4rem .8rem;background:transparent;border:1px dashed color-mix(in srgb, ${a.color} 40%, transparent);border-radius:4px;color:${a.color};cursor:pointer;font-size:.85em;display:inline-flex;align-items:center;gap:.35rem;`;const l=document.createElement("span");l.setAttribute("data-icon",a.icon),i.appendChild(l),i.appendChild(document.createTextNode(` + ${a.label}`)),i.addEventListener("click",()=>{C(),r.actions.push(he(e)),$(),x()}),P.appendChild(i)}),z.appendChild(P),R.appendChild(z),b.appendChild(R);const T=document.createElement("div");T.style.cssText="display:flex;padding:.75rem 1rem;border-top:1px solid var(--dm-border,#333);background:var(--dm-background-alt,#0f0f17);gap:.5rem;align-items:center;";const S=document.createElement("button");S.type="button",S.className="btn btn-ghost btn-sm",S.textContent="Undo (Ctrl+Z)",S.addEventListener("click",()=>H()),T.appendChild(S);const U=document.createElement("span");U.style.cssText="font-size:.75em;color:var(--dm-text-muted,#aaa);",T.appendChild(U);const G=document.createElement("div");G.style.flex="1",T.appendChild(G);const W=document.createElement("button");W.type="button",W.className="btn btn-ghost",W.textContent="Cancel",W.addEventListener("click",()=>h.close()),T.appendChild(W);const j=document.createElement("button");j.type="button",j.className="btn btn-primary",j.textContent="Insert Scribe",j.addEventListener("click",()=>{re(s,Z()),h.close()}),T.appendChild(j),b.appendChild(T),h.setContent(b),b.addEventListener("keydown",e=>{const a=(e.key||"").toLowerCase();(e.ctrlKey||e.metaKey)&&a==="z"&&!e.shiftKey&&n.length>0&&(e.preventDefault(),e.stopPropagation(),H())}),$(),x(),O(),requestAnimationFrame(()=>{Domma.icons.scan()}),h.open();function C(){n.push(JSON.stringify(r)),n.length>de&&n.shift(),O()}function H(){if(n.length===0){E.toast("Nothing to undo",{type:"info",duration:700});return}const e=JSON.parse(n.pop());Object.assign(r.globals,e.globals),r.actions.length=0,e.actions.forEach(a=>r.actions.push(a)),F.sync(),$(),x(),O(),E.toast("Undone",{type:"info",duration:500})}function O(){U.textContent=n.length===0?"":`${n.length} step${n.length===1?"":"s"} stored`,S.disabled=n.length===0,S.style.opacity=n.length===0?".4":"1"}function x(e=150){clearTimeout(t),t=setTimeout(J,e)}function J(){c(),ee(k),k.style.color="",k.style.fontStyle="",M.textContent=Z();const e=r.actions.filter(o=>o.type==="render"||o.type==="redraw"?(o.text||"").length>0:!0);if(e.length===0){k.textContent="(add an action to preview)",k.style.color="var(--dm-text-muted,#666)",k.style.fontStyle="italic",N.textContent="";return}const a=document.createElement("span");a.className="dm-fx-scribe",k.appendChild(a);const i=r.globals,{effect:l,controls:g,...w}=i;w.actions=ie(e,l);try{p=Domma.effects.scribe(a,w),N.textContent=i.autoStart?`${e.length} action${e.length===1?"":"s"}`:"Press Replay to start"}catch{N.textContent="Preview error"}}function $(){if(ee(A),r.actions.length===0){const e=document.createElement("p");e.textContent="No actions yet - add one from the buttons below.",e.style.cssText="font-size:.85em;color:var(--dm-text-muted,#888);font-style:italic;margin:.5rem 0;",A.appendChild(e);return}r.actions.forEach((e,a)=>{A.appendChild(ne(e,a))}),requestAnimationFrame(()=>Domma.icons.scan())}function ne(e,a){const i=Q[e.type],l=document.createElement("div");l.style.cssText=`border:1px solid var(--dm-border,#333);border-left:3px solid ${i.color};border-radius:4px;padding:.6rem .75rem;background:var(--dm-surface,#15151a);`;const g=document.createElement("div");g.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;";const w=document.createElement("span");w.style.cssText=`display:inline-flex;align-items:center;gap:.3rem;font-size:.72rem;font-weight:700;color:${i.color};text-transform:uppercase;letter-spacing:.06em;`;const o=document.createElement("span");o.setAttribute("data-icon",i.icon),w.appendChild(o),w.appendChild(document.createTextNode(i.label)),g.appendChild(w);const u=document.createElement("span");u.textContent=`#${a+1}`,u.style.cssText="font-size:.7rem;color:var(--dm-text-muted,#888);",g.appendChild(u);const m=document.createElement("div");m.style.flex="1",g.appendChild(m);const d=K("chevron-up","Move up");a===0&&(d.disabled=!0,d.style.opacity=".3"),d.addEventListener("click",()=>X(a,-1)),g.appendChild(d);const f=K("chevron-down","Move down");a===r.actions.length-1&&(f.disabled=!0,f.style.opacity=".3"),f.addEventListener("click",()=>X(a,1)),g.appendChild(f);const v=K("x","Remove");return v.addEventListener("click",()=>{C(),r.actions.splice(a,1),$(),x()}),g.appendChild(v),l.appendChild(g),l.appendChild(oe(e)),l}function X(e,a){const i=e+a;if(i<0||i>=r.actions.length)return;C();const[l]=r.actions.splice(e,1);r.actions.splice(i,0,l),$(),x()}function oe(e){const a=document.createElement("div");if(a.style.cssText="display:flex;flex-direction:column;gap:.5rem;",e.type==="render"||e.type==="redraw"){const o=document.createElement("textarea");o.value=e.text||"",o.placeholder=e.type==="redraw"?"New text - clears everything first, then types this":"Text to type out",o.rows=2,o.style.cssText=L+"resize:vertical;font-family:inherit;min-height:2.2rem;";let u=o.value;o.addEventListener("input",()=>{e.text=o.value,x()}),o.addEventListener("blur",()=>{o.value!==u&&(C(),u=o.value)}),a.appendChild(o);const m=document.createElement("div");m.style.cssText="display:flex;align-items:center;gap:.5rem;font-size:.8em;";const d=document.createElement("label");d.textContent="Effect:",d.style.color="var(--dm-text-muted,#aaa)";const f=document.createElement("select");return f.style.cssText=L+"width:auto;padding:.25rem .4rem;",["inherit","none","fade","bounce","glow"].forEach(v=>{const _=document.createElement("option");_.value=v,_.textContent=v==="inherit"?"Default effect":v.charAt(0).toUpperCase()+v.slice(1),f.appendChild(_)}),f.value=e.effect||"none",f.addEventListener("change",()=>{C(),e.effect=f.value,x()}),m.appendChild(d),m.appendChild(f),a.appendChild(m),a}if(e.type==="wait"){const o=document.createElement("div");o.style.cssText="display:flex;gap:.4rem;align-items:center;";const u=document.createElement("input");u.type="number",u.min="0",u.step="any",u.value=String(e.duration??1e3),u.style.cssText=L+"flex:1;",u.addEventListener("input",()=>{const d=Number(u.value);!Number.isNaN(d)&&d>=0&&(e.duration=d,x())}),u.addEventListener("blur",()=>C()),o.appendChild(u);const m=document.createElement("select");return m.style.cssText=L+"width:auto;",[["ms","ms"],["s","seconds"]].forEach(([d,f])=>{const v=document.createElement("option");v.value=d,v.textContent=f,m.appendChild(v)}),m.value=e.unit||"ms",m.addEventListener("change",()=>{C(),e.unit=m.value,x()}),o.appendChild(m),a.appendChild(o),a}const i=document.createElement("div");i.style.cssText="display:flex;gap:.75rem;align-items:center;flex-wrap:wrap;font-size:.85em;";const l=document.createElement("input");l.type="number",l.min="1",l.value=String(e.count||5),l.style.cssText=L+"width:5rem;",l.addEventListener("input",()=>{const o=parseInt(l.value,10);!Number.isNaN(o)&&o>0&&(e.count=o,x())}),l.addEventListener("blur",()=>C());const g=`undo-mode-${Math.random().toString(36).slice(2)}`;return[["last","Last render"],["all","All text"],["count","Exactly N units"]].forEach(([o,u])=>{const m=document.createElement("label");m.style.cssText="display:inline-flex;align-items:center;gap:.3rem;cursor:pointer;";const d=document.createElement("input");d.type="radio",d.name=g,d.value=o,d.checked=(e.mode||"last")===o,d.addEventListener("change",()=>{d.checked&&(C(),e.mode=o,l.style.display=o==="count"?"":"none",x())}),m.appendChild(d),m.appendChild(document.createTextNode(u)),i.appendChild(m),o==="count"&&(i.appendChild(l),l.style.display=(e.mode||"last")==="count"?"":"none")}),a.appendChild(i),a}function Z(){return le(r.globals,r.actions)}}function ue(){return{globals:se(),actions:[{type:"render",text:"We build things that matter.",effect:"inherit"},{type:"wait",duration:1500,unit:"ms"},{type:"redraw",text:"We create solutions that last.",effect:"inherit"},{type:"wait",duration:1500,unit:"ms"}]}}function he(s){switch(s){case"render":return{type:"render",text:"",effect:"inherit"};case"wait":return{type:"wait",duration:1e3,unit:"ms"};case"undo":return{type:"undo",mode:"last",count:5};case"redraw":return{type:"redraw",text:"",effect:"inherit"}}return{type:"render",text:"",effect:"inherit"}}function fe(s,r){const n=document.createElement("aside");n.style.cssText="width:240px;flex-shrink:0;padding:1rem 1.25rem;border-right:1px solid var(--dm-border,#333);overflow-y:auto;display:flex;flex-direction:column;gap:.75rem;background:var(--dm-background-alt,#0c0c12);";const t=document.createElement("h4");t.textContent="Global settings",t.style.cssText="margin:0 0 .25rem;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--dm-text-muted,#aaa);",n.appendChild(t);const p=ae.map(c=>{const h=y=>{r(),s.globals[c.key]=c.key==="loop"?ce(y):y};let b;if(c.type==="checkbox"){const y=ve(s.globals[c.key],h,c.label);n.appendChild(y.wrap),b=y.input}else b=c.type==="select"?ge(c.options,s.globals[c.key],h):c.type==="number"?xe(s.globals[c.key],h,c.min):ye(s.globals[c.key],"",h),n.appendChild(be(c.label,b));return{field:c,input:b}});return{el:n,sync(){p.forEach(({field:c,input:h})=>{c.type==="checkbox"?h.checked=s.globals[c.key]:h.value=String(s.globals[c.key])})}}}function be(s,r){const n=document.createElement("div"),t=document.createElement("label");return t.style.cssText=pe,t.textContent=s,n.appendChild(t),n.appendChild(r),n}function xe(s,r,n=1){const t=document.createElement("input");return t.type="number",t.min=String(n),t.value=String(s),t.style.cssText=L,t.addEventListener("input",()=>{const p=Number(t.value);!Number.isNaN(p)&&p>=n&&r(p)}),t}function ye(s,r,n){const t=document.createElement("input");return t.type="text",t.value=s,t.placeholder=r,t.style.cssText=L,t.addEventListener("input",()=>n(t.value)),t}function ge(s,r,n){const t=document.createElement("select");return t.style.cssText=L,s.forEach(([p,c])=>{const h=document.createElement("option");h.value=p,h.textContent=c,t.appendChild(h)}),t.value=r,t.addEventListener("change",()=>n(t.value)),t}function ve(s,r,n){const t=document.createElement("label");t.style.cssText=me;const p=document.createElement("input");return p.type="checkbox",p.checked=s,p.addEventListener("change",()=>r(p.checked)),t.appendChild(p),t.appendChild(document.createTextNode(n)),{wrap:t,input:p}}function V(s,r){const n=document.createElement("button");n.type="button",n.className="btn btn-ghost btn-sm",n.style.cssText="display:inline-flex;align-items:center;gap:.35rem;";const t=document.createElement("span");return t.setAttribute("data-icon",s),n.appendChild(t),n.appendChild(document.createTextNode(r)),n}function K(s,r){const n=document.createElement("button");n.type="button",n.title=r,n.style.cssText="padding:.25rem .4rem;background:transparent;border:1px solid var(--dm-border,#333);border-radius:3px;color:var(--dm-text-muted,#aaa);cursor:pointer;display:inline-flex;align-items:center;";const t=document.createElement("span");return t.setAttribute("data-icon",s),n.appendChild(t),n}function ee(s){for(;s.firstChild;)s.removeChild(s.firstChild)}function we(s){return String(s).replace(/&/g,"&amp;").replace(/"/g,"&quot;")}let te=!1;function Ce(){te||(te=!0,I.register("edit-3",{viewBox:"0 0 24 24",paths:["M12 20h9","M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("clock",{viewBox:"0 0 24 24",paths:["M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z","M12 6v6l4 2"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("corner-up-left",{viewBox:"0 0 24 24",paths:["M9 14 4 9l5-5","M20 20v-7a4 4 0 0 0-4-4H4"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("refresh-cw",{viewBox:"0 0 24 24",paths:["M23 4v6h-6","M1 20v-6h6","M3.51 9a9 9 0 0 1 14.85-3.36L23 10","M20.49 15a9 9 0 0 1-14.85 3.36L1 14"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("rotate-cw",{viewBox:"0 0 24 24",paths:["M23 4v6h-6","M20.49 15a9 9 0 1 1-2.12-9.36L23 10"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("code",{viewBox:"0 0 24 24",paths:["M16 18l6-6-6-6","M8 6l-6 6 6 6"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("chevron-up",{viewBox:"0 0 24 24",path:"M18 15l-6-6-6 6",stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("chevron-down",{viewBox:"0 0 24 24",path:"M6 9l6 6 6-6",stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}),I.register("x",{viewBox:"0 0 24 24",paths:["M18 6L6 18","M6 6l12 12"],stroke:"currentColor",fill:"none",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"}))}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.6.20",
3
+ "version": "1.6.22",
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",
@@ -84,7 +84,7 @@
84
84
  "@fastify/rate-limit": "^10.3.0",
85
85
  "@fastify/static": "9.1.1",
86
86
  "bcryptjs": "^3.0.3",
87
- "domma-js": "0.51.10",
87
+ "domma-js": "0.51.11",
88
88
  "dotenv": "^17.2.3",
89
89
  "fastify": "5.8.5",
90
90
  "gray-matter": "^4.0.3",
@@ -0,0 +1,128 @@
1
+ # Plugin Development
2
+
3
+ Each plugin needs exactly 3 files:
4
+
5
+ ```
6
+ plugins/my-plugin/
7
+ plugin.json Required manifest (name, displayName, version, description, author, date, icon)
8
+ plugin.js Default export: Fastify plugin function
9
+ config.js Default export: plain object of config defaults
10
+ ```
11
+
12
+ See `docs/plugin-development.md` for full plugin API, hooks, and injection points.
13
+ Each plugin directory has its own `CLAUDE.md` with routes, storage, and gotchas.
14
+
15
+ **User docs ship with the plugin:** `docs/*.md` (Markdown, optional `title`/`order` frontmatter, no H1,
16
+ shortcodes shown literally) appear under Documentation > Plugins for users who can use the plugin
17
+ (`docs.permission`, else the first sidebar permission). Update them in the same change as the feature -
18
+ see "User guides in the Documentation folder" in `docs/plugin-development.md`.
19
+
20
+ ## Contracts every plugin gets wrong
21
+
22
+ These five faults were each found in more than one shipped plugin, and each one
23
+ is silent - nothing logs, nothing throws where you can see it, and the screen
24
+ just does less than it should. Copy `_template/`, not another plugin.
25
+
26
+ **Collection schemas.** `createCollection()` destructures `title` and calls
27
+ `.trim()`, and `validateEntryData()` reads `data[field.name]`. So a collection is
28
+ named by `title` (not `label`) and a field by `name` (not `key`). Get either
29
+ wrong and every schema throws; `setupPlugin()` catches that and says nothing, so
30
+ the collections are left as empty directories and every route answers
31
+ `Collection "…" not found`. Delete those directories before retrying - the mkdir
32
+ happens before the validation, so a second attempt reports "already exists"
33
+ instead of the real fault. Note this only bites a FRESH install: on a box where
34
+ the collection already exists, `setupPlugin` skips it and the broken schema is
35
+ invisible.
36
+
37
+ **Route params.** Domma's router calls `view.onMount($container)` with ONE
38
+ argument and never the params, and `R.current` is a METHOD - so
39
+ `R.current?.params?.id` reads `.params` off the function object and is always
40
+ `undefined`. An editor view that does this opens blank on every edit and Save
41
+ creates a duplicate. Read the hash, as every core editor view does:
42
+
43
+ ```js
44
+ const m = location.hash.match(/^#\/plugins\/my-plugin\/edit\/([^?#]+)/);
45
+ const id = m ? decodeURIComponent(m[1]) : null;
46
+ ```
47
+
48
+ **Class names are Domma's, not Bootstrap's.** `form-control`, `d-flex`,
49
+ `justify-content-between`, `align-items-center`, `h2`, `h3`, `small`, `fw-bold`,
50
+ `font-monospace`, `btn-outline-secondary`, `col-sm-*` and `badge-outline` are
51
+ defined NOWHERE in this admin and style nothing. Domma's are `form-input` /
52
+ `form-textarea` / `form-select` / `form-checkbox`, `flex`, `justify-between`,
53
+ `items-center`, `text-2xl`, `text-sm`, `font-bold`, `font-mono`, `btn-outline`,
54
+ `col-*`, `badge-secondary`. For a stat tile the admin ships `.stat-card` /
55
+ `.stat-icon` / `.stat-value` / `.stat-label`. Put your own rules in
56
+ `admin/css/index.css` and declare it in the manifest's `admin.css` - not inline
57
+ on the element.
58
+
59
+ **`F.create` takes a schema, not a container.** The signature is
60
+ `F.create(schema, data, options).renderTo(el)`, values come back from
61
+ `form.getData()` and `form.validate()`, and `placeholder` / `hint` live under
62
+ `formConfig`. Passing an element first makes it the schema and the form renders
63
+ nothing at all. `admin/js/views/plugins.js` is the house pattern.
64
+
65
+ **Reading an H response.** Some routes send a bare array and some send
66
+ `{data: […]}`. `res.data ?? []` silently empties a screen that the API is
67
+ filling correctly. Write `res.data ?? (Array.isArray(res) ? res : [])`.
68
+
69
+ ## The plugin banner is the admin's, and it has a door
70
+
71
+ The admin wraps every plugin view in `.dm-plugin-shell` at the point views are
72
+ registered (`admin/js/lib/plugin-chrome.js`), so the banner carrying the name,
73
+ the version and the licence is not yours to draw - which is why a template that
74
+ opens with its own `<h1>` shows the same words twice and has one stripped.
75
+
76
+ The facts read version, release date, licence and - for a paid plugin with an
77
+ entitlement - its expiry: "Expires" for a rental, "Support to" for a perpetual
78
+ licence, whose plugin never stops. That comes from `pluginExpiry()` in
79
+ `server/services/plugins.js`; a plugin does nothing to get it.
80
+
81
+ A screen-level control belongs IN that banner, and `addBannerAction(container,
82
+ {icon, title, onClick, id})` puts it there, to the LEFT of those facts. Settings
83
+ in particular goes there as a cog on the plugin's main screens - not as a
84
+ sidebar item, a tab, or a button in a pane of your own - so every plugin keeps
85
+ it in the same place. Import it at RUN
86
+ time and survive its absence:
87
+
88
+ ```js
89
+ try {
90
+ const chrome = await import('/admin/js/lib/plugin-chrome.js');
91
+ if (!chrome.addBannerAction?.($container, {...})) throw new Error('no banner');
92
+ } catch {
93
+ // older admin, or no chrome: put the control in your own toolbar
94
+ }
95
+ ```
96
+
97
+ A static import would take the whole view down on a CMS whose `admin/` predates
98
+ the export - and `admin/` is replaced wholesale by the updater, so that is a
99
+ version skew you do not control.
100
+
101
+ ## Smaller ones, same flavour
102
+
103
+ - `E.slideover().close()` only hides the panel and `destroy()` does not detach
104
+ its element, so every open leaves another one in the document. Do both, via
105
+ `onClose`, after the slide-out has run.
106
+ - `requireAdmin` reads `request.user`, which only `authenticate` populates - as a
107
+ lone preHandler it answers 401 to everyone, admin included. Always
108
+ `[authenticate, requireAdmin]`.
109
+ - Plugin settings live in `config/plugins.json`; read them from `options.settings`
110
+ or `getPluginStates()[name].settings` and write them with
111
+ `savePluginState(name, {settings})`. `getConfig`/`saveConfig` in
112
+ `server/config.js` take a FILE NAME, not a config object.
113
+ - `authenticate` is Bearer-only, so an `<a href target="_blank">` to any API
114
+ route arrives with no Authorization header and comes back 401. Fetch through
115
+ `H` and write the result into a window you opened on the click.
116
+ - An unknown `data-icon` renders an empty span and says nothing. Check the name
117
+ against the shipped bundle before using it. An icon inside a `data-if` needs
118
+ scanning AFTER the element comes back, or the span is scanned while detached
119
+ and stays empty - set the observable, `M.flush()`, then scan.
120
+ - **`type: 'date'` in an `F.create` schema cannot be submitted.** Forma renders
121
+ `<input type="date">`, which writes a string, and `M.types.date` accepts only
122
+ a real `Date`, so `validate()` fails with "Expected type date" every time and
123
+ the save button appears to do nothing. Declare the field with no `type` (the
124
+ model only type-checks when one is set) and attach **`E.datePicker`** to the
125
+ rendered input: it is one themed control in every browser, it writes ISO
126
+ `YYYY-MM-DD` into the input and it fires `input`/`change`, so the model and
127
+ `form.getData()` hear it exactly as they hear typing. `plugins/invoice` does
128
+ this in its editor, its list filters and its payment dialog.
@@ -0,0 +1,23 @@
1
+ # PLUGIN_DISPLAY_NAME - AI Assistant Guide
2
+
3
+ ## Purpose
4
+ TODO: Describe what this plugin does.
5
+
6
+ ## Files
7
+ - `plugin.json`, `plugin.js`, `config.js`
8
+ - `admin/views/index.js` - main admin view (`indexView`)
9
+ - `admin/templates/index.html` - view template
10
+ - `admin/css/index.css` - admin styles (auto-injected via plugin.json `admin.css`)
11
+
12
+ ## Routes
13
+ All API routes are under `/api/plugins/PLUGIN_SLUG` and require authentication.
14
+
15
+ | Method | Path | Purpose | Location |
16
+ |---|---|---|---|
17
+
18
+ ## Gotchas
19
+ - Use `S.get/set` not `localStorage`
20
+ - Use `H.get/post/put/delete` not `fetch()`
21
+ - Use `F.create()` for forms, `T.create()` for tables
22
+ - For HTML with inline styles or data-* attrs: `import { safeSetHTML } from '../../admin/js/lib/safe-html.js'`
23
+ - For interactive elements (buttons) inside dynamic HTML: build via DOM (`createElement`/`appendChild`)
@@ -0,0 +1,37 @@
1
+ ---
2
+ title: Guide
3
+ ---
4
+ One or two sentences on what this plugin does for a site owner. This file appears under
5
+ Documentation > Plugins in the admin while the plugin is enabled (see "User guides in the
6
+ Documentation folder" in docs/plugin-development.md). Do not start with a top-level heading -
7
+ the page header already shows the plugin's name.
8
+
9
+ ## Getting started
10
+
11
+ 1. Open **Tools > PLUGIN_DISPLAY_NAME**.
12
+ 2. Click **New** and fill in the form.
13
+ 3. Click **Save**.
14
+
15
+ ## Screens
16
+
17
+ ### PLUGIN_DISPLAY_NAME
18
+
19
+ What the list shows, what clicking a row does, and what the right-click menu offers.
20
+
21
+ ## Settings
22
+
23
+ Click the cog on the plugin banner. List each setting and what it changes.
24
+
25
+ ## Shortcodes
26
+
27
+ ```text
28
+ [PLUGIN_SLUG limit="5"]
29
+ ```
30
+
31
+ ## Permissions and roles
32
+
33
+ Which permissions in **System > Roles** open the screen, and who holds them by default.
34
+
35
+ ## Limitations
36
+
37
+ What it does not do, so nobody looks for it.
@@ -4,11 +4,12 @@
4
4
 
5
5
  export default async function plugin(fastify, options) {
6
6
  const { authenticate, requireAdmin } = options.auth;
7
- const BASE = `/api/plugins/PLUGIN_SLUG`;
8
7
 
9
- // Add your API routes here.
8
+ // Add your API routes here. Paths are RELATIVE: the CMS mounts every route
9
+ // in this file under /api/plugins/PLUGIN_SLUG, so '/items' answers at
10
+ // /api/plugins/PLUGIN_SLUG/items. A full path here would end up doubled.
10
11
  // Example:
11
- // fastify.get(`${BASE}/items`, { preHandler: [authenticate] }, async (request, reply) => {
12
+ // fastify.get('/items', { preHandler: [authenticate] }, async (request, reply) => {
12
13
  // return { items: [] };
13
14
  // });
14
15
  }
package/scripts/build.js CHANGED
@@ -93,6 +93,14 @@ const PLUGIN_AS_IS_PATTERNS = [
93
93
  'plugins/*/blocks/**/*.css',
94
94
  // Images a plugin serves from public/ (blog's sample-post covers).
95
95
  'plugins/*/public/**/*.svg',
96
+ // The scaffold template's Markdown: its CLAUDE.md and the docs/guide.md a new
97
+ // Tool starts its Documentation page from. The free-tier plugins ship whole
98
+ // from their lock, but _template goes through these patterns, which carried no
99
+ // .md - so every Tool scaffolded on an installed site lacked both, while the
100
+ // New plugin panel promised the CLAUDE.md.
101
+ 'plugins/_template/**/*.md',
102
+ // The plugin-authoring guide the CMS's own CLAUDE.md points Tool authors to.
103
+ 'plugins/CLAUDE.md',
96
104
  ];
97
105
 
98
106
  /**
@@ -11,6 +11,7 @@
11
11
  * plugins/{name}/data/ - optional plugin-specific data store
12
12
  */
13
13
  import fs from 'fs/promises';
14
+ import {readFileSync} from 'fs';
14
15
  import path from 'path';
15
16
  import matter from 'gray-matter';
16
17
  import {getConfig, saveConfig} from '../config.js';
@@ -1253,6 +1254,25 @@ async function newestAdminMtime(root) {
1253
1254
  return newest;
1254
1255
  }
1255
1256
 
1257
+ /**
1258
+ * The plugins the engine ships with (the free tier): the names in
1259
+ * plugins/free-tier.lock.json. Read once; an install without the lock ships none.
1260
+ *
1261
+ * @returns {Set<string>}
1262
+ */
1263
+ let shippedNames = null;
1264
+ export function shippedPluginNames() {
1265
+ if (!shippedNames) {
1266
+ try {
1267
+ const lock = JSON.parse(readFileSync(path.join(PLUGINS_DIR, 'free-tier.lock.json'), 'utf8'));
1268
+ shippedNames = new Set(Object.keys(lock.plugins || {}));
1269
+ } catch {
1270
+ shippedNames = new Set();
1271
+ }
1272
+ }
1273
+ return shippedNames;
1274
+ }
1275
+
1256
1276
  /**
1257
1277
  * The licence line shown on a plugin's admin banner.
1258
1278
  *
@@ -1266,12 +1286,16 @@ async function newestAdminMtime(root) {
1266
1286
  * @param {object} manifest
1267
1287
  * @returns {string}
1268
1288
  */
1269
- export function pluginLicence(manifest) {
1289
+ export function pluginLicence(manifest, shipped = shippedPluginNames()) {
1270
1290
  const stated = manifest.licence || manifest.license;
1271
1291
  if (typeof stated === 'string' && stated.trim()) return stated.trim();
1272
1292
  if (CORE_PLUGINS.has(manifest.name)) return 'Built in';
1273
1293
  if (manifest.closedSource) return 'Commercial licence';
1274
- return 'Included with Domma CMS';
1294
+ // Only the plugins the engine ships (the free tier, listed in its lock) came with
1295
+ // the CMS. Anything else that is neither bought nor licensed is one the site made:
1296
+ // a Tool scaffolded from the Marketplace used to be labelled as part of the CMS.
1297
+ if (shipped.has(manifest.name)) return 'Included with Domma CMS';
1298
+ return 'Your own Tool';
1275
1299
  }
1276
1300
 
1277
1301
  /**