domma-cms 1.10.14 → 1.10.16

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,4 +1,4 @@
1
- import{api as z}from"../api.js";import{addBannerAction as Be}from"../lib/plugin-chrome.js";import{SHAPES as He,whileLoading as Oe}from"../lib/loading.js";import{helpPopovers as $e}from"../lib/help-popover.js";import{onceClosed as Ke,openMenuAt as Me,settingsStore as _e,sidebarCount as ee,viewLifetime as je}from"../lib/tool-kit.js";import{openImageEditor as Ve}from"../lib/image-editor.js";import{KIND_LABEL as De,arrangeMedia as Ye,clashes as Ge,formatBytes as Le,isEditable as te,isPreviewable as Je,kindCounts as Qe,kindOf as We,markdownFor as ae,mediaTile as Xe,normalisePrefs as Ze,safeName as B}from"../lib/media-arrange.js";import{fmtDateTime as et}from"../lib/dates.js";const Se=/^#\/media\/?(\?.*)?$/,ne="#/media",H="media.draft",Ae=_e("media.prefs",Ze),tt=`
1
+ import{api as N,getToken as ot,refreshAccessToken as it}from"../api.js";import{addBannerAction as rt}from"../lib/plugin-chrome.js";import{SHAPES as lt,whileLoading as dt}from"../lib/loading.js";import{helpPopovers as Ie}from"../lib/help-popover.js";import{onceClosed as ct,openMenuAt as Fe,settingsStore as mt,sidebarCount as le,viewLifetime as ut}from"../lib/tool-kit.js";import{openImageEditor as pt}from"../lib/image-editor.js";import{KIND_LABEL as Ue,arrangeMedia as ft,formatBytes as Pe,isEditable as de,isPreviewable as vt,kindCounts as ht,kindOf as bt,markdownFor as ce,mediaTile as gt,nameRefusal as yt,normaliseLimits as _e,normalisePrefs as wt,dropzoneRules as Et,rejectText as kt,safeName as w,shortName as Be,uploadErrorText as xt,uploadHint as $t}from"../lib/media-arrange.js";import{fmtDateTime as Mt}from"../lib/dates.js";const He=/^#\/media\/?(\?.*)?$/,me="#/media",K="media.draft",Oe=mt("media.prefs",wt),St=`
2
2
  <div class="mdx-tile" data-name="{{name}}" tabindex="0" title="{{name}}">
3
3
  <div class="mdx-thumb">
4
4
  <img data-bind-hidden="!preview" data-bind-src="thumb" alt="" loading="lazy" decoding="async">
@@ -7,7 +7,7 @@ import{api as z}from"../api.js";import{addBannerAction as Be}from"../lib/plugin-
7
7
  <span class="mdx-chip">{{kindLabel}}</span>
8
8
  <button type="button" class="mdx-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
9
9
  <div class="mdx-info"><span class="mdx-name">{{name}}</span><span class="mdx-meta">{{meta}}</span></div>
10
- </div>`,q=u=>String(u??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function at(u){const d=document.createElement("template");return d.innerHTML=u.trim(),d.content.firstElementChild}const l=je(Se);async function O(u,d){try{await navigator.clipboard.writeText(u),E.toast(`${d} copied.`,{type:"success"})}catch{E.toast("Copy failed - the browser refused the clipboard.",{type:"error"})}}export const mediaView={templateUrl:"/admin/js/templates/media.html",async onMount(u){l.reset();const d=u.get(0).querySelector(".mdx")||u.get(0),b=()=>Se.test(location.hash)&&d.isConnected,oe=Ae.read(),p=M.observable([]),se=M.observable(!0),K=M.observable(""),y=M.observable(oe.kind),_=M.observable(oe.sort),ie=M.observable(""),D=M.observable(""),R=M.observable(!1);let j=String(Date.now());l.add(M.effect(()=>Ae.write({kind:y.value,sort:_.value})));const U=M.observable([]);l.add(M.effect(()=>{const e=Date.now();U.value=Ye(p.value,{kind:y.value,query:ie.value,sort:_.value}).map(t=>Xe(t,{now:e,stamp:j}))}));const re=M.computed(()=>Qe(p.value));l.add(M.effect(()=>{y.value!=="all"&&!re.value[y.value]&&p.value.length&&(y.value="all")}));const Ce=M.computed(()=>{const e=p.value,t=U.value.length,o=Le(e.reduce((n,i)=>n+(Number(i.size)||0),0));return t===e.length?`${e.length} file${e.length===1?"":"s"} \xB7 ${o}`:`${t} of ${e.length} \xB7 ${o} in all`}),Te=M.computed(()=>p.value.length?"No files match.":"Nothing uploaded yet - drop files here, or Upload in the banner."),L=e=>p.peek().find(t=>t.name===e);async function A(){try{const e=await Oe(d.querySelector(".dm-loading-slot"),z.media.list(),He.tiles);if(!b())return;j=String(Date.now()),p.value=Array.isArray(e)?e:[],K.value=""}catch(e){K.value=`Could not load the media: ${e.message||e}`}finally{se.value=!1}}async function le(e){const t=[...e||[]].filter(s=>s&&s.size!==void 0);if(!t.length)return;if(D.peek()){E.toast("An upload is still running.",{type:"warning"});return}const o=Ge(t.map(s=>s.name),p.peek().map(s=>s.name));if(o.length){const s=o.slice(0,5).map(w=>`"${q(w)}"`).join(", ")+(o.length>5?` and ${o.length-5} more`:"");if(!await E.confirm(`${o.length===1?"A file":`${o.length} files`} with the same name ${o.length===1?"is":"are"} already here: ${s}. Replace ${o.length===1?"it":"them"}?`))return}const n=[];let i=0;try{for(const s of t){D.value=`Uploading ${i+1} of ${t.length} - ${s.name}`;const w=new FormData;w.append("file",s);try{await z.media.upload(w),i++}catch(J){n.push(`${s.name}: ${J.message||"failed"}`)}}}finally{D.value=""}i&&E.toast(`Uploaded ${i} file${i===1?"":"s"}.`,{type:"success"}),n.length&&E.toast(`Not uploaded - ${n.join("; ")}`,{type:"error",duration:9e3}),ee(ne),b()&&await A()}async function V(e){if(!await E.confirm(`Delete "${q(e.name)}"? Pages that show it will show a broken image or link. This cannot be undone.`))return!1;try{return await z.media.delete(e.name),E.toast(`Deleted "${e.name}".`,{type:"success"}),ee(ne),b()&&await A(),!0}catch(t){return E.toast(`Not deleted: ${t.message||t}`,{type:"error"}),!1}}async function de(e){const t=await Ve(e);return t&&b()&&(ee(ne),await A()),t}const Y=e=>window.location.origin+e.url;let h=null,ce=!1;const G=new Set;function k(e,{focusRename:t=!1,draftName:o=null}={}){const n=L(e);if(!n){E.toast(`"${e}" is not here any more.`,{type:"warning"});return}const i=M.observable(o??n.name),s=M.observable(!1),w=M.computed(()=>{const a=i.value.trim();if(!a)return"A file needs a name.";const r=B(a);return r!==n.name&&L(r)?`"${r}" is already here.`:""}),J=M.computed(()=>{const a=i.value.trim(),r=B(a);return a&&r!==a?`Saved as "${r}" - only letters, numbers, dots, hyphens and underscores are kept.`:""}),ge=()=>i.peek().trim()!==n.name,we={newName:i,saving:s,renameErr:w,renameHint:J,cannotRename:M.computed(()=>s.value||!!w.value||B(i.value.trim())===n.name),editable:te(n.name)},m=E.slideover({title:q(n.name),size:"md",position:"right"}),c=at(`
10
+ </div>`,q=p=>String(p??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function Dt(p){const r=document.createElement("template");return r.innerHTML=p.trim(),r.content.firstElementChild}const d=ut(He);let ue=null;async function j(p,r){try{await navigator.clipboard.writeText(p),E.toast(`${r} copied.`,{type:"success"})}catch{E.toast("Copy failed - the browser refused the clipboard.",{type:"error"})}}export const mediaView={templateUrl:"/admin/js/templates/media.html",async onMount(p){d.reset();const r=p.get(0).querySelector(".mdx")||p.get(0),m=()=>He.test(location.hash)&&r.isConnected,pe=Oe.read(),v=M.observable([]),fe=M.observable(!0),V=M.observable(""),b=M.observable(pe.kind),Y=M.observable(pe.sort),ve=M.observable(""),F=M.observable(!1),he=M.observable("");let k=_e(null),G=String(Date.now());d.add(M.effect(()=>Oe.write({kind:b.value,sort:Y.value})));const U=M.observable([]);d.add(M.effect(()=>{const e=Date.now();U.value=ft(v.value,{kind:b.value,query:ve.value,sort:Y.value}).map(t=>gt(t,{now:e,stamp:G}))}));const be=M.computed(()=>ht(v.value));d.add(M.effect(()=>{b.value!=="all"&&!be.value[b.value]&&v.value.length&&(b.value="all")}));const Ke=M.computed(()=>{const e=v.value,t=U.value.length,s=Pe(e.reduce((n,o)=>n+(Number(o.size)||0),0));return t===e.length?`${e.length} file${e.length===1?"":"s"} \xB7 ${s}`:`${t} of ${e.length} \xB7 ${s} in all`}),je=M.computed(()=>v.value.length?"No files match.":"Nothing uploaded yet - drop files here, or Upload in the banner."),x=e=>v.peek().find(t=>t.name===e);async function $(){try{const e=await dt(r.querySelector(".dm-loading-slot"),N.media.list(),lt.tiles);if(!m())return;G=String(Date.now()),v.value=Array.isArray(e)?e:[],V.value=""}catch(e){V.value=`Could not load the media: ${e.message||e}`}finally{fe.value=!1}}async function W(e){if(!await E.confirm(`Delete "${q(e.name)}"? Pages that show it will show a broken image or link. This cannot be undone.`))return!1;try{return await N.media.delete(e.name),E.toast(`Deleted "${e.name}".`,{type:"success"}),le(me),m()&&await $(),!0}catch(t){return E.toast(`Not deleted: ${t.message||t}`,{type:"error"}),!1}}async function ge(e){const t=await pt(e);return t&&m()&&(le(me),await $()),t}const J=e=>window.location.origin+e.url;let g=null,ye=!1;const Q=new Set;function D(e,{focusRename:t=!1,draftName:s=null}={}){const n=x(e);if(!n){E.toast(`"${e}" is not here any more.`,{type:"warning"});return}const o=M.observable(s??n.name),H=M.observable(!1),Ce=M.computed(()=>{const a=o.value.trim();if(!a)return"A file needs a name.";const i=w(a);return i!==n.name&&x(i)?`"${i}" is already here.`:""}),Ze=M.computed(()=>{const a=o.value.trim(),i=w(a);return a&&i!==a?`Saved as "${i}" - only letters, numbers, dots, hyphens and underscores are kept.`:""}),Te=()=>o.peek().trim()!==n.name,ze={newName:o,saving:H,renameErr:Ce,renameHint:Ze,cannotRename:M.computed(()=>H.value||!!Ce.value||w(o.value.trim())===n.name),editable:de(n.name)},u=E.slideover({title:q(n.name),size:"md",position:"right"}),c=Dt(`
11
11
  <div class="mdx-panel">
12
12
  <div class="mdx-preview"></div>
13
13
  <dl class="mdx-facts"></dl>
@@ -37,4 +37,4 @@ import{api as z}from"../api.js";import{addBannerAction as Be}from"../lib/plugin-
37
37
  <a class="btn btn-sm btn-ghost mdx-open" target="_blank" rel="noopener"><span data-icon="external-link" data-icon-size="13"></span> Open</a>
38
38
  <button type="button" class="btn btn-sm btn-outline" data-on-click="edit" data-bind-hidden="!editable"><span data-icon="edit" data-icon-size="13"></span> Edit image</button>
39
39
  </div>
40
- </div>`),Q=c.querySelector(".mdx-preview"),$=We(n.name);if(Je(n.name)){const a=document.createElement("img");a.src=`${n.url}?t=${encodeURIComponent(j)}`,a.alt=n.name,Q.appendChild(a)}else if($==="video"||$==="audio"){const a=document.createElement($);a.src=n.url,a.controls=!0,a.preload="metadata",Q.appendChild(a)}else{const a=document.createElement("span");a.setAttribute("data-icon",$==="document"?"file-text":"paperclip"),a.setAttribute("data-icon-size","40"),Q.appendChild(a)}const Re=c.querySelector(".mdx-facts"),P=(a,r)=>{const xe=document.createElement("dt");xe.textContent=a;const Z=document.createElement("dd");return Z.textContent=r,Re.append(xe,Z),Z};if(P("Type",`${De[$]==="Images"?"Image":De[$].replace(/s$/,"")} (${(n.name.split(".").pop()||"").toUpperCase()})`),P("Size",Le(n.size)),P("Uploaded",n.createdAt?et(n.createdAt):"-"),te(n.name)){const a=P("Dimensions","\u2026");z.media.info(n.name).then(r=>{a.textContent=r?.width?`${r.width} \xD7 ${r.height} px`:"-"}).catch(()=>{a.textContent="-"})}c.querySelector(".mdx-url").value=Y(n),c.querySelector(".mdx-md").value=ae(n),c.querySelector(".mdx-open").href=n.url;let N=!1;async function ke(){if(!we.cannotRename.peek()){s.value=!0;try{const a=await z.media.rename(n.name,i.peek().trim());E.toast(`Renamed to "${a?.name||B(i.peek().trim())}".`,{type:"success"}),N=!0,m.close(),b()&&await A()}catch(a){E.toast(`Not renamed: ${a.message||a}`,{type:"error"})}finally{s.value=!1}}}m.element.appendChild(c);const Ie=$e(c),Ue=M.applyBindings(we,c,{methods:{rename:ke,copyUrl:()=>O(Y(n),"Address"),copyMd:()=>O(ae(n),"Markdown"),async del(){await V(n)&&(N=!0,m.close())},async edit(){N=!0,m.close(),await de(n)}}}),W=c.querySelector("#mdx-rename"),Fe=a=>{a.key==="Enter"&&a.target===W&&(a.preventDefault(),ke())};c.addEventListener("keydown",Fe);const Ee={name:n.name,dirty:ge,newName:()=>i.peek()};h=Ee,G.add(m),Ke(m,()=>{Ue.dispose(),Ie(),c.querySelectorAll("video, audio").forEach(a=>{try{a.pause()}catch{}}),G.delete(m),h===Ee&&(h=null)});const Pe=m.close.bind(m);let X=!1;m.close=async a=>{if(!N&&!ce&&a!==!0&&ge()){if(X)return;X=!0;const r=await E.confirm("Discard the new name you typed?");if(X=!1,!r)return}return N=!0,Pe()},I.scan(c),m.open(),(t||o)&&(W.focus(),W.select())}function Ne(e){const t=L(e);return t?[{header:t.name},{label:"Details\u2026",icon:"info",action:()=>k(t.name)},{label:"Copy address",icon:"link",action:()=>O(Y(t),"Address")},{label:"Copy for a page",icon:"copy",action:()=>O(ae(t),"Markdown")},{label:"Open in a new tab",icon:"external-link",action:()=>window.open(t.url,"_blank","noopener")},{type:"divider"},{label:"Rename\u2026",icon:"type",action:()=>k(t.name,{focusRename:!0})},...te(t.name)?[{label:"Edit image\u2026",icon:"edit",action:()=>de(t)}]:[],{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>V(t)}]:[]}const f=d.querySelector(".mdx-grid");f.innerHTML=tt;const x=e=>()=>{y.value=e},ze=M.applyBindings({files:p,loading:se,loadError:K,kind:y,sort:_,query:ie,tiles:U,counts:re,totalText:Ce,emptyText:Te,busyText:D,dragging:R},d,{methods:{kindAll:x("all"),kindImage:x("image"),kindVideo:x("video"),kindAudio:x("audio"),kindDocument:x("document"),kindOther:x("other")}});l.add(()=>ze.dispose()),l.add($e(d,{position:"bottom"})),l.add(M.effect(()=>{U.value,requestAnimationFrame(()=>{f.isConnected&&I.scan(f)})}));const qe=E.contextMenu(f,{cursor:"glyph",match:".mdx-tile",inherit:!1,density:"compact",items:e=>Ne(e.dataset.name)});l.add(()=>qe?.destroy?.());const me=e=>{const t=e.target.closest(".mdx-tile");if(!t)return;const o=e.target.closest(".mdx-kebab");if(o){Me(o);return}k(t.dataset.name)},ue=e=>{const t=e.target.closest(".mdx-tile");if(!t||e.target!==t)return;const o=L(t.dataset.name);o&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Me(t)):e.key==="Enter"?k(o.name):e.key==="F2"?(e.preventDefault(),k(o.name,{focusRename:!0})):e.key==="Delete"&&(e.preventDefault(),V(o)))};f.addEventListener("click",me),f.addEventListener("keydown",ue),l.add(()=>{f.removeEventListener("click",me),f.removeEventListener("keydown",ue)});const C=d.querySelector(".mdx-file-input"),pe=()=>{const e=[...C.files];C.value="",le(e)};C.addEventListener("change",pe),l.add(()=>C.removeEventListener("change",pe)),Be(u,{id:"media-upload",icon:"upload",title:"Upload files",onClick:()=>C.click()});const F=e=>[...e.dataTransfer?.types||[]].includes("Files");let T=0;const fe=e=>{F(e)&&(e.preventDefault(),T++,R.value=!0)},ve=e=>{F(e)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")},be=e=>{F(e)&&(T=Math.max(0,T-1),T||(R.value=!1))},ye=e=>{F(e)&&(e.preventDefault(),T=0,R.value=!1,le(e.dataTransfer.files))},v=u.get(0);v.addEventListener("dragenter",fe),v.addEventListener("dragover",ve),v.addEventListener("dragleave",be),v.addEventListener("drop",ye),l.add(()=>{v.removeEventListener("dragenter",fe),v.removeEventListener("dragover",ve),v.removeEventListener("dragleave",be),v.removeEventListener("drop",ye)});const he=e=>{(D.peek()||h?.dirty())&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",he),l.add(()=>window.removeEventListener("beforeunload",he)),l.add(()=>{if(h?.dirty())try{S.set(H,{name:h.name,newName:h.newName(),at:new Date().toISOString()})}catch{}ce=!0,[...G].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(d),await A(),!b())return;let g=null;try{g=S.get(H)}catch{}if(g?.name){try{S.remove?S.remove(H):S.set(H,null)}catch{}L(g.name)&&await E.confirm(`You were renaming "${q(g.name)}" to "${q(g.newName)}" and left before saving. Pick up where you left off?`)&&b()&&k(g.name,{draftName:g.newName})}}};
40
+ </div>`),se=c.querySelector(".mdx-preview"),A=bt(n.name);if(vt(n.name)){const a=document.createElement("img");a.src=`${n.url}?t=${encodeURIComponent(G)}`,a.alt=n.name,se.appendChild(a)}else if(A==="video"||A==="audio"){const a=document.createElement(A);a.src=n.url,a.controls=!0,a.preload="metadata",se.appendChild(a)}else{const a=document.createElement("span");a.setAttribute("data-icon",A==="document"?"file-text":"paperclip"),a.setAttribute("data-icon-size","40"),se.appendChild(a)}const et=c.querySelector(".mdx-facts"),O=(a,i)=>{const qe=document.createElement("dt");qe.textContent=a;const re=document.createElement("dd");return re.textContent=i,et.append(qe,re),re};if(O("Type",`${Ue[A]==="Images"?"Image":Ue[A].replace(/s$/,"")} (${(n.name.split(".").pop()||"").toUpperCase()})`),O("Size",Pe(n.size)),O("Uploaded",n.createdAt?Mt(n.createdAt):"-"),de(n.name)){const a=O("Dimensions","\u2026");N.media.info(n.name).then(i=>{a.textContent=i?.width?`${i.width} \xD7 ${i.height} px`:"-"}).catch(()=>{a.textContent="-"})}c.querySelector(".mdx-url").value=J(n),c.querySelector(".mdx-md").value=ce(n),c.querySelector(".mdx-open").href=n.url;let R=!1;async function Re(){if(!ze.cannotRename.peek()){H.value=!0;try{const a=await N.media.rename(n.name,o.peek().trim());E.toast(`Renamed to "${a?.name||w(o.peek().trim())}".`,{type:"success"}),R=!0,u.close(),m()&&await $()}catch(a){E.toast(`Not renamed: ${a.message||a}`,{type:"error"})}finally{H.value=!1}}}u.element.appendChild(c);const tt=Ie(c),nt=M.applyBindings(ze,c,{methods:{rename:Re,copyUrl:()=>j(J(n),"Address"),copyMd:()=>j(ce(n),"Markdown"),async del(){await W(n)&&(R=!0,u.close())},async edit(){R=!0,u.close(),await ge(n)}}}),oe=c.querySelector("#mdx-rename"),at=a=>{a.key==="Enter"&&a.target===oe&&(a.preventDefault(),Re())};c.addEventListener("keydown",at);const Ne={name:n.name,dirty:Te,newName:()=>o.peek()};g=Ne,Q.add(u),ct(u,()=>{nt.dispose(),tt(),c.querySelectorAll("video, audio").forEach(a=>{try{a.pause()}catch{}}),Q.delete(u),g===Ne&&(g=null)});const st=u.close.bind(u);let ie=!1;u.close=async a=>{if(!R&&!ye&&a!==!0&&Te()){if(ie)return;ie=!0;const i=await E.confirm("Discard the new name you typed?");if(ie=!1,!i)return}return R=!0,st()},I.scan(c),u.open(),(t||s)&&(oe.focus(),oe.select())}function Ve(e){const t=x(e);return t?[{header:t.name},{label:"Details\u2026",icon:"info",action:()=>D(t.name)},{label:"Copy address",icon:"link",action:()=>j(J(t),"Address")},{label:"Copy for a page",icon:"copy",action:()=>j(ce(t),"Markdown")},{label:"Open in a new tab",icon:"external-link",action:()=>window.open(t.url,"_blank","noopener")},{type:"divider"},{label:"Rename\u2026",icon:"type",action:()=>D(t.name,{focusRename:!0})},...de(t.name)?[{label:"Edit image\u2026",icon:"edit",action:()=>ge(t)}]:[],{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>W(t)}]:[]}const h=r.querySelector(".mdx-grid");h.innerHTML=St;const L=e=>()=>{b.value=e},Ye=M.applyBindings({files:v,loading:fe,loadError:V,kind:b,sort:Y,query:ve,tiles:U,counts:be,totalText:Ke,emptyText:je,hintText:he,dragging:F},r,{methods:{kindAll:L("all"),kindImage:L("image"),kindVideo:L("video"),kindAudio:L("audio"),kindDocument:L("document"),kindOther:L("other")}});d.add(()=>Ye.dispose()),d.add(Ie(r,{position:"bottom"})),d.add(M.effect(()=>{U.value,requestAnimationFrame(()=>{h.isConnected&&I.scan(h)})}));const Ge=E.contextMenu(h,{cursor:"glyph",match:".mdx-tile",inherit:!1,density:"compact",items:e=>Ve(e.dataset.name)});d.add(()=>Ge?.destroy?.());const we=e=>{const t=e.target.closest(".mdx-tile");if(!t)return;const s=e.target.closest(".mdx-kebab");if(s){Fe(s);return}D(t.dataset.name)},Ee=e=>{const t=e.target.closest(".mdx-tile");if(!t||e.target!==t)return;const s=x(t.dataset.name);s&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Fe(t)):e.key==="Enter"?D(s.name):e.key==="F2"?(e.preventDefault(),D(s.name,{focusRename:!0})):e.key==="Delete"&&(e.preventDefault(),W(s)))};h.addEventListener("click",we),h.addEventListener("keydown",Ee),d.add(()=>{h.removeEventListener("click",we),h.removeEventListener("keydown",Ee)});const X=new WeakSet;let P=[],C=[],Z=0,ee=0;const ke=e=>setTimeout(e,0),We=()=>{if(!C.length)return;const e=C;C=[],E.toast(`Not added - ${e.slice(0,4).join("; ")}${e.length>4?` and ${e.length-4} more`:""}`,{type:"error",duration:9e3})},te=e=>{C.length||ke(We),C.push(e)};async function Je(){const e=P;if(P=[],!e.length||!m())return;const t=[...new Set(e.map(o=>w(o.name)))],s=t.slice(0,5).map(o=>`"${q(o)}"`).join(", ")+(t.length>5?` and ${t.length-5} more`:""),n=t.length===1;await E.confirm(`${n?"A file":`${t.length} files`} with the same name ${n?"is":"are"} already here: ${s}. Replace ${n?"it":"them"}?`)&&(T||(e.forEach(o=>X.add(o)),l.addFiles(e)))}const Qe=r.querySelector(".mdx-file-input"),Xe=r.querySelector(".mdx-zone");let T=!1,_=0,ne=null;const l=E.dropzone(Xe,{input:Qe,multiple:!0,previews:!0,upload:{url:"/api/media",field:"file",concurrency:3,headers:()=>{const e=ot();return e?{Authorization:`Bearer ${e}`}:{}}},labels:{cancel:"Cancel the upload of {name}",remove:"Remove {name} from the list"},onBeforeAdd(e){const t=yt(e.name,k);if(t)return te(`${Be(e.name)}: ${t}`),!1;if(X.has(e.file))return X.delete(e.file),!0;const s=w(e.name),n=l.getEntries().filter(o=>w(o.name)===s);return n.some(o=>o.status==="uploading"||o.status==="queued")?(te(`${Be(e.name)}: a file of that name is uploading already`),!1):(n.forEach(o=>{o.status!=="done"&&l.removeFile(o.id)}),x(s)?(P.length||ke(Je),P.push(e.file),!1):!0)},onReject(e,t){te(kt(e.name,t,k))},onSuccess(){Z++},onError(e,t){if(t.status===401&&!e._mdxRefreshed&&S.get("auth_refresh_token")){e._mdxRefreshed=!0,t.message="signing in again\u2026",_++,it().then(()=>{T||l.retry(e.id)}).catch(()=>{}).finally(()=>{_--,ne?.()});return}ee++,t.message=xt(t,k)},onComplete(){const e=Z,t=ee;if(Z=0,ee=0,e&&E.toast(`Uploaded ${e} file${e===1?"":"s"}.`,{type:"success"}),t){const s=`${t} file${t===1?"":"s"} not uploaded`;E.toast(m()?`${s} - Retry or remove ${t===1?"it":"them"} in the list.`:`${s}.`,{type:"error",duration:9e3})}l.getEntries().filter(s=>s.status==="done").forEach(s=>l.removeFile(s.id)),e&&(le(me),m()?$():ue?.())}}),ae=()=>l.getEntries().some(e=>e.status==="uploading"||e.status==="queued");ue=()=>{m()&&$()};const xe=()=>{T||(T=!0,l.destroy())};d.add(()=>{ue=null,ne=()=>{!ae()&&!_&&xe()},ae()||_?l.on("complete",ne):xe()}),N.media.limits().then(e=>{T||!m()||(k=_e(e),l.setOptions(Et(k)),he.value=$t(k))}).catch(()=>{}),rt(p,{id:"media-upload",icon:"upload",title:"Upload files",onClick:()=>l.browse()});const B=e=>[...e.dataTransfer?.types||[]].includes("Files");let z=0;const $e=e=>{B(e)&&(e.preventDefault(),z++,F.value=!0)},Me=e=>{B(e)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")},Se=e=>{B(e)&&(z=Math.max(0,z-1),z||(F.value=!1))},De=e=>{B(e)&&(z=0,F.value=!1,!e.defaultPrevented&&(e.preventDefault(),l.addFiles(e.dataTransfer.files)))},Le=e=>{const t=[...e.clipboardData?.files||[]];e.defaultPrevented||!t.length||e.target?.closest?.('input, textarea, [contenteditable=""], [contenteditable="true"]')&&[...e.clipboardData.types||[]].includes("text/plain")||(e.preventDefault(),l.addFiles(t))},f=p.get(0);f.addEventListener("dragenter",$e),f.addEventListener("dragover",Me),f.addEventListener("dragleave",Se),f.addEventListener("drop",De),f.addEventListener("paste",Le),d.add(()=>{f.removeEventListener("dragenter",$e),f.removeEventListener("dragover",Me),f.removeEventListener("dragleave",Se),f.removeEventListener("drop",De),f.removeEventListener("paste",Le)});const Ae=e=>{(ae()||g?.dirty())&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",Ae),d.add(()=>window.removeEventListener("beforeunload",Ae)),d.add(()=>{if(g?.dirty())try{S.set(K,{name:g.name,newName:g.newName(),at:new Date().toISOString()})}catch{}ye=!0,[...Q].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(r),await $(),!m())return;let y=null;try{y=S.get(K)}catch{}if(y?.name){try{S.remove?S.remove(K):S.set(K,null)}catch{}x(y.name)&&await E.confirm(`You were renaming "${q(y.name)}" to "${q(y.newName)}" and left before saving. Pick up where you left off?`)&&m()&&D(y.name,{draftName:y.newName})}}};
@@ -1702,9 +1702,13 @@ uploads of the same name don't collide).
1702
1702
  | `accept` | no | Comma-separated mime types or wildcards (`image/*`, `application/pdf,image/jpeg`) |
1703
1703
  | `maxSize` | no | Maximum bytes (default 5 MB) |
1704
1704
 
1705
- **Form rendering** - `<input type="file">` with the `accept` hint. Setting
1706
- `multiple: true` on the field renders a multi-file picker (one file per entry
1707
- update; the form takes the last selection).
1705
+ **Form rendering** - a drop area (`E.dropzone`) over the field's file input: drop,
1706
+ browse (click, Enter or Space) or paste, with image previews. `accept`, `maxSize`
1707
+ and the count (one file unless `multiple: true`) are checked as files are added,
1708
+ and a refused file is named under the area and announced. The rules are
1709
+ `public/js/form-files.mjs`, which the server imports for its own check. Setting
1710
+ `multiple: true` lets several files be added (the server keeps the last one per
1711
+ field).
1708
1712
 
1709
1713
  **Submission** - the form switches automatically to `multipart/form-data`
1710
1714
  when any file input has a selected file. The server-side handler:
@@ -1115,8 +1115,9 @@ all entries inline.
1115
1115
  Subsequent pages append rather than replace; filter/sort changes reset to page 1 normally.
1116
1116
  - **Server-mode debounce + abort** - in-flight fetches are cancelled when a new state change supersedes them, so
1117
1117
  fast typing in the search box doesn't fight for response order.
1118
- - **Polished controls** - multiselect renders as toggleable chips with counts; numeric ranges add a dual-handle
1119
- slider above the min/max inputs (when bounds can be derived from the dataset).
1118
+ - **Polished controls** - multiselect renders as toggleable chips with counts; numeric ranges add a two-thumb
1119
+ slider (Domma's `E.rangeSlider`: arrow keys, Page Up/Down, Home/End, read out by screen readers, the field's
1120
+ prefix/suffix text in its labels) under the min/max inputs, in client mode when the loaded entries give the bounds.
1120
1121
 
1121
1122
  ##### State-machine transitions
1122
1123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.10.14",
3
+ "version": "1.10.16",
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",
@@ -86,7 +86,7 @@
86
86
  "bcryptjs": "^3.0.3",
87
87
  "css-select": "^7.0.0",
88
88
  "dom-serializer": "^3.1.1",
89
- "domma-js": "0.57.0",
89
+ "domma-js": "0.58.1",
90
90
  "dotenv": "^17.2.3",
91
91
  "fastify": "5.12.7",
92
92
  "gray-matter": "^4.0.3",
@@ -1 +1 @@
1
- .fb-form-wrapper{max-width:600px;margin:2rem auto}.fb-form-honeypot{position:absolute;left:-9999px;top:-9999px;width:0;height:0;overflow:hidden;opacity:0;z-index:-1}.fb-form-success{padding:1rem 1.25rem;border-radius:var(--dm-radius-md, 6px);background:color-mix(in srgb,var(--dm-success) 12%,transparent);border:1px solid color-mix(in srgb,var(--dm-success) 30%,transparent);color:var(--dm-success);font-size:.95rem;margin-top:.5rem}.fb-form-error{padding:.75rem 1rem;border-radius:var(--dm-radius-md, 6px);background:color-mix(in srgb,var(--dm-danger) 10%,transparent);border:1px solid color-mix(in srgb,var(--dm-danger) 25%,transparent);color:var(--dm-danger);font-size:.875rem;margin-top:.75rem}.fb-form-loading{opacity:.6;pointer-events:none}.wizard-progress{height:4px;background:var(--dm-border);border-radius:2px;margin-bottom:1rem}.wizard-progress-bar{height:100%;background:var(--dm-primary);border-radius:2px;transition:width .3s ease}.wizard-step-title{font-size:1.1rem;font-weight:600;margin-bottom:.25rem}.wizard-step-description{font-size:.875rem;color:var(--dm-text-muted);margin-bottom:1rem}.wizard-nav{display:flex;justify-content:space-between;gap:.5rem;margin-top:1rem}.fb-field-hidden{display:none!important}.fb-validation-error{color:var(--dm-danger);font-size:.8rem;margin-top:.25rem}.fb-field-trigger-hidden{display:none!important}.fb-field-disabled{opacity:.55;pointer-events:none}.fb-trigger-banner{display:flex;align-items:flex-start;gap:.75rem;margin:.75rem 0;padding:.7rem .9rem;border-radius:var(--dm-radius-md);border-left:3px solid var(--dm-info);background:var(--dm-surface-raised);color:var(--dm-text);font-size:.9rem}.fb-trigger-banner--success{border-left-color:var(--dm-success)}.fb-trigger-banner--warning{border-left-color:var(--dm-warning)}.fb-trigger-banner--error{border-left-color:var(--dm-danger)}.fb-trigger-banner-text{flex:1 1 auto}.fb-trigger-banner-close{flex:0 0 auto;background:none;border:0;color:var(--dm-text-muted);font-size:1.1rem;line-height:1;cursor:pointer;padding:0 .15rem}.fb-trigger-banner-close:hover{color:var(--dm-text)}.fb-trigger-block-note{color:var(--dm-danger);font-size:.85rem;margin:.5rem 0 0}.fb-field-trigger-masked{display:none!important}.fb-trigger-end{margin:1rem 0;padding:1rem 1.1rem;border-radius:var(--dm-radius-md);border:1px solid var(--dm-border);border-left:3px solid var(--dm-warning);background:var(--dm-surface-raised);color:var(--dm-text)}.fb-trigger-end-text{margin:0}.fb-form-wrapper .grid>.fb-trigger-banner,.cms-form-embed .grid>.fb-trigger-banner,.fb-form-wrapper .grid>.fb-trigger-end,.cms-form-embed .grid>.fb-trigger-end{grid-column:1 / -1}.fb-form-wrapper .domma-form-field{margin-bottom:2rem}.fb-form-wrapper .form-group:not(.form-buttons){margin-bottom:2rem}.fb-spacer{height:2rem}.fb-field-required .picker-label:after,.fb-field-required>.form-label:after{content:" *";color:var(--dm-danger)}.fb-field-required .picker-label .picker-required{display:none}.fb-field-invalid>.form-label,.fb-field-invalid .picker-label{color:var(--dm-danger)}.fb-field-invalid .form-input,.fb-field-invalid .form-select,.fb-field-invalid textarea,.fb-field-invalid .picker-options{border-color:var(--dm-danger)}@media(max-width:700px){.fb-form-wrapper form.grid,[data-form-inline] form.grid,.fb-form-wrapper .wizard form.grid,[data-form-inline] .wizard form.grid{grid-template-columns:1fr!important}.fb-form-wrapper form.grid>[class*=col-span-],[data-form-inline] form.grid>[class*=col-span-]{grid-column:span 1 / span 1!important}}.wizard-nav{gap:1rem;flex-wrap:wrap}.wizard-nav .btn{min-width:7.5rem}@media(max-width:480px){.wizard-nav{flex-direction:column-reverse}.wizard-nav .btn{width:100%}}.fb-form-wrapper .form-buttons,[data-form-inline] .form-buttons,#fb-preview-form .form-buttons{display:flex;gap:1rem;flex-wrap:wrap;margin-top:1.25rem}.form-buttons.fb-submit{display:flex;gap:1rem;flex-wrap:wrap}.form-buttons.fb-submit--left{justify-content:flex-start;text-align:left}.form-buttons.fb-submit--center{justify-content:center;text-align:center}.form-buttons.fb-submit--right{justify-content:flex-end;text-align:right}.form-buttons.fb-submit--full>button[type=submit],.form-buttons.fb-submit--full>input[type=submit]{flex:1 1 100%;width:100%}.form-buttons.fb-submit>.fb-trigger-block-note{flex-basis:100%}.btn.fb-btn{gap:.45em}.btn.fb-btn>.fb-btn-icon{flex-shrink:0}.form-buttons.fb-submit--pair{display:flex;gap:1rem;flex-wrap:wrap;align-items:center}.form-buttons.fb-submit--full.fb-submit--pair:not(.fb-submit--stack)>.btn{flex:1 1 0;width:auto}.form-buttons.fb-submit--stack{flex-direction:column;flex-wrap:nowrap;align-items:flex-start;gap:.75rem}.form-buttons.fb-submit--stack.fb-submit--center{align-items:center}.form-buttons.fb-submit--stack.fb-submit--right{align-items:flex-end}.form-buttons.fb-submit--stack.fb-submit--full{align-items:stretch}.form-buttons.fb-submit--stack>.btn,.form-buttons.fb-submit--stack>button[type=submit]{flex:0 0 auto}.form-buttons.fb-submit--stack>.fb-trigger-block-note{flex-basis:auto;align-self:stretch}a.btn.fb-secondary-btn.btn-primary:not(:hover){color:var(--dm-primary-text, var(--dm-white))}a.btn.fb-secondary-btn.btn-secondary:not(:hover){color:var(--dm-secondary-text, var(--dm-white))}a.btn.fb-secondary-btn.btn-success:not(:hover){color:var(--dm-success-text, var(--dm-white))}a.btn.fb-secondary-btn.btn-danger:not(:hover){color:var(--dm-danger-text, var(--dm-white))}a.btn.fb-secondary-btn.btn-warning:not(:hover){color:var(--dm-warning-text)}a.btn.fb-secondary-btn.btn-info:not(:hover){color:var(--dm-info-text, var(--dm-white))}a.btn.fb-secondary-btn.btn-outline:not(:hover){color:var(--dm-text)}a.btn.fb-secondary-btn.btn-outline-success:not(:hover){color:var(--dm-success)}a.btn.fb-secondary-btn.btn-outline-danger:not(:hover){color:var(--dm-danger)}a.btn.fb-secondary-btn.btn-outline-warning:not(:hover){color:var(--dm-warning-text)}a.btn.fb-secondary-btn.btn-outline-info:not(:hover){color:var(--dm-info)}a.btn.fb-secondary-btn.btn-ghost:not(:hover){color:var(--dm-text-secondary)}a.btn.fb-secondary-btn:hover{text-decoration:none}
1
+ .fb-form-wrapper{max-width:600px;margin:2rem auto}.fb-form-honeypot{position:absolute;left:-9999px;top:-9999px;width:0;height:0;overflow:hidden;opacity:0;z-index:-1}.fb-form-success{padding:1rem 1.25rem;border-radius:var(--dm-radius-md, 6px);background:color-mix(in srgb,var(--dm-success) 12%,transparent);border:1px solid color-mix(in srgb,var(--dm-success) 30%,transparent);color:var(--dm-success);font-size:.95rem;margin-top:.5rem}.fb-form-error{padding:.75rem 1rem;border-radius:var(--dm-radius-md, 6px);background:color-mix(in srgb,var(--dm-danger) 10%,transparent);border:1px solid color-mix(in srgb,var(--dm-danger) 25%,transparent);color:var(--dm-danger);font-size:.875rem;margin-top:.75rem}.fb-form-loading{opacity:.6;pointer-events:none}.wizard-progress{height:4px;background:var(--dm-border);border-radius:2px;margin-bottom:1rem}.wizard-progress-bar{height:100%;background:var(--dm-primary);border-radius:2px;transition:width .3s ease}.wizard-step-title{font-size:1.1rem;font-weight:600;margin-bottom:.25rem}.wizard-step-description{font-size:.875rem;color:var(--dm-text-muted);margin-bottom:1rem}.wizard-nav{display:flex;justify-content:space-between;gap:.5rem;margin-top:1rem}.fb-field-hidden{display:none!important}.fb-validation-error{color:var(--dm-danger);font-size:.8rem;margin-top:.25rem}.fb-field-trigger-hidden{display:none!important}.fb-field-disabled{opacity:.55;pointer-events:none}.fb-trigger-banner{display:flex;align-items:flex-start;gap:.75rem;margin:.75rem 0;padding:.7rem .9rem;border-radius:var(--dm-radius-md);border-left:3px solid var(--dm-info);background:var(--dm-surface-raised);color:var(--dm-text);font-size:.9rem}.fb-trigger-banner--success{border-left-color:var(--dm-success)}.fb-trigger-banner--warning{border-left-color:var(--dm-warning)}.fb-trigger-banner--error{border-left-color:var(--dm-danger)}.fb-trigger-banner-text{flex:1 1 auto}.fb-trigger-banner-close{flex:0 0 auto;background:none;border:0;color:var(--dm-text-muted);font-size:1.1rem;line-height:1;cursor:pointer;padding:0 .15rem}.fb-trigger-banner-close:hover{color:var(--dm-text)}.fb-trigger-block-note{color:var(--dm-danger);font-size:.85rem;margin:.5rem 0 0}.fb-field-trigger-masked{display:none!important}.fb-trigger-end{margin:1rem 0;padding:1rem 1.1rem;border-radius:var(--dm-radius-md);border:1px solid var(--dm-border);border-left:3px solid var(--dm-warning);background:var(--dm-surface-raised);color:var(--dm-text)}.fb-trigger-end-text{margin:0}.fb-form-wrapper .grid>.fb-trigger-banner,.cms-form-embed .grid>.fb-trigger-banner,.fb-form-wrapper .grid>.fb-trigger-end,.cms-form-embed .grid>.fb-trigger-end{grid-column:1 / -1}.fb-form-wrapper .domma-form-field{margin-bottom:2rem}.fb-form-wrapper .form-group:not(.form-buttons){margin-bottom:2rem}.fb-spacer{height:2rem}.fb-field-required .picker-label:after,.fb-field-required>.form-label:after{content:" *";color:var(--dm-danger)}.fb-field-required .picker-label .picker-required{display:none}.fb-field-invalid>.form-label,.fb-field-invalid .picker-label{color:var(--dm-danger)}.fb-field-invalid .form-input,.fb-field-invalid .form-select,.fb-field-invalid textarea,.fb-field-invalid .picker-options{border-color:var(--dm-danger)}.fb-file-refusal{margin:.4rem 0 0;font-size:.875rem;color:var(--dm-danger)}.fb-file-refusal[hidden]{display:none}.fb-field-invalid .dm-dropzone{border-color:var(--dm-danger)}.page-body ul.dm-dropzone-files{margin:var(--dm-space-3, .75rem) 0;padding-left:0}@media(max-width:700px){.fb-form-wrapper form.grid,[data-form-inline] form.grid,.fb-form-wrapper .wizard form.grid,[data-form-inline] .wizard form.grid{grid-template-columns:1fr!important}.fb-form-wrapper form.grid>[class*=col-span-],[data-form-inline] form.grid>[class*=col-span-]{grid-column:span 1 / span 1!important}}.wizard-nav{gap:1rem;flex-wrap:wrap}.wizard-nav .btn{min-width:7.5rem}@media(max-width:480px){.wizard-nav{flex-direction:column-reverse}.wizard-nav .btn{width:100%}}.fb-form-wrapper .form-buttons,[data-form-inline] .form-buttons,#fb-preview-form .form-buttons{display:flex;gap:1rem;flex-wrap:wrap;margin-top:1.25rem}.form-buttons.fb-submit{display:flex;gap:1rem;flex-wrap:wrap}.form-buttons.fb-submit--left{justify-content:flex-start;text-align:left}.form-buttons.fb-submit--center{justify-content:center;text-align:center}.form-buttons.fb-submit--right{justify-content:flex-end;text-align:right}.form-buttons.fb-submit--full>button[type=submit],.form-buttons.fb-submit--full>input[type=submit]{flex:1 1 100%;width:100%}.form-buttons.fb-submit>.fb-trigger-block-note{flex-basis:100%}.btn.fb-btn{gap:.45em}.btn.fb-btn>.fb-btn-icon{flex-shrink:0}.form-buttons.fb-submit--pair{display:flex;gap:1rem;flex-wrap:wrap;align-items:center}.form-buttons.fb-submit--full.fb-submit--pair:not(.fb-submit--stack)>.btn{flex:1 1 0;width:auto}.form-buttons.fb-submit--stack{flex-direction:column;flex-wrap:nowrap;align-items:flex-start;gap:.75rem}.form-buttons.fb-submit--stack.fb-submit--center{align-items:center}.form-buttons.fb-submit--stack.fb-submit--right{align-items:flex-end}.form-buttons.fb-submit--stack.fb-submit--full{align-items:stretch}.form-buttons.fb-submit--stack>.btn,.form-buttons.fb-submit--stack>button[type=submit]{flex:0 0 auto}.form-buttons.fb-submit--stack>.fb-trigger-block-note{flex-basis:auto;align-self:stretch}a.btn.fb-secondary-btn.btn-primary:not(:hover){color:var(--dm-primary-text, var(--dm-white))}a.btn.fb-secondary-btn.btn-secondary:not(:hover){color:var(--dm-secondary-text, var(--dm-white))}a.btn.fb-secondary-btn.btn-success:not(:hover){color:var(--dm-success-text, var(--dm-white))}a.btn.fb-secondary-btn.btn-danger:not(:hover){color:var(--dm-danger-text, var(--dm-white))}a.btn.fb-secondary-btn.btn-warning:not(:hover){color:var(--dm-warning-text)}a.btn.fb-secondary-btn.btn-info:not(:hover){color:var(--dm-info-text, var(--dm-white))}a.btn.fb-secondary-btn.btn-outline:not(:hover){color:var(--dm-text)}a.btn.fb-secondary-btn.btn-outline-success:not(:hover){color:var(--dm-success)}a.btn.fb-secondary-btn.btn-outline-danger:not(:hover){color:var(--dm-danger)}a.btn.fb-secondary-btn.btn-outline-warning:not(:hover){color:var(--dm-warning-text)}a.btn.fb-secondary-btn.btn-outline-info:not(:hover){color:var(--dm-info)}a.btn.fb-secondary-btn.btn-ghost:not(:hover){color:var(--dm-text-secondary)}a.btn.fb-secondary-btn:hover{text-decoration:none}