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.
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/lib/scribe-composer.js +1 -1
- package/package.json +2 -2
- package/plugins/CLAUDE.md +128 -0
- package/plugins/_template/CLAUDE.md +23 -0
- package/plugins/_template/docs/guide.md +37 -0
- package/plugins/_template/plugin.js +4 -3
- package/scripts/build.js +8 -0
- package/server/services/plugins.js +26 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Domma Tools CSS v0.51.
|
|
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-
|
|
6
|
-
* Commit:
|
|
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.
|
|
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-
|
|
6
|
-
* Commit:
|
|
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,"&").replace(/"/g,""")}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,"&").replace(/"/g,""")}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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
/**
|