domma-cms 0.101.0 → 0.101.2

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.49.0
2
+ * Domma Tools CSS v0.50.0
3
3
  * Dynamic Object Manipulation & Modeling API
4
4
  * (c) 2026 Darryl Waterhouse & DCBW-IT
5
- * Built: 2026-09-27T22:31:48.349Z
6
- * Commit: 5bc521e
5
+ * Built: 2026-09-28T07:51:39.095Z
6
+ * Commit: d400172
7
7
  */
8
8
 
9
9
  /*!
@@ -1,9 +1,9 @@
1
1
  /*!
2
- * Domma Tools v0.49.0
2
+ * Domma Tools v0.50.0
3
3
  * Developer tools: Theme Roller & Page Roller
4
4
  * (c) 2026 Darryl Waterhouse & DCBW-IT
5
- * Built: 2026-09-27T22:31:33.360Z
6
- * Commit: 5bc521e
5
+ * Built: 2026-09-28T07:51:27.470Z
6
+ * Commit: d400172
7
7
  *
8
8
  * Requires: domma.min.js
9
9
  */
@@ -51,9 +51,13 @@
51
51
  .usx-row:last-child { border-bottom: 0; }
52
52
  .usx-row:hover, .usx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
53
53
  .usx-row.is-locked { cursor: default; }
54
- .usx-avatar { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; flex-shrink: 0; border-radius: 50%;
54
+ /* The row's avatar is Domma's (.avatar, colour from the name); the plain circle is the look on a core without it. */
55
+ .usx-avatar:not(.avatar) { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; flex-shrink: 0; border-radius: 50%;
55
56
  font-size: var(--dm-text-xs); font-weight: 700; color: var(--dm-primary); background: color-mix(in srgb, var(--dm-primary) 14%, transparent); }
56
- .usx-row.is-off .usx-avatar { color: var(--dm-text-muted); background: color-mix(in srgb, var(--dm-text-muted) 14%, transparent); }
57
+ .usx-avatar:not(.avatar) > .avatar-img { display: none; }
58
+ .usx-row.is-off .usx-avatar:not(.avatar) { color: var(--dm-text-muted); background: color-mix(in srgb, var(--dm-text-muted) 14%, transparent); }
59
+ /* An inactive user cannot sign in: their avatar fades with the rest of the row. */
60
+ .usx-row.is-off .usx-avatar.avatar { opacity: .5; filter: grayscale(1); }
57
61
  .usx-row.is-off .usx-main strong { color: var(--dm-text-muted); }
58
62
  .usx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
59
63
  .usx-main strong, .usx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -1,6 +1,6 @@
1
- import{api as B,apiRequest as O,getUser as oe}from"../api.js";import{addBannerAction as ae}from"../lib/plugin-chrome.js";import{SHAPES as ne,whileLoading as ie}from"../lib/loading.js";import{helpPopovers as re}from"../lib/help-popover.js";import{openMenuAt as F,settingsStore as le,sidebarCount as v,viewLifetime as ce}from"../lib/tool-kit.js";import{resetPassword as de,withdrawReset as ue}from"../lib/password-reset-actions.js";import{filterByProject as pe}from"../lib/project-context.js";import{FILTERS as me,SORTS as he,arrangeUsers as fe,canManage as W,resetAllowed as ve,roleIndex as be,userRow as ye}from"../lib/users-arrange.js";import{fmtDateTime as we}from"../lib/dates.js";const H=/^#\/(projects\/[^/?#]+\/)?users\/?(\?.*)?$/,b="#/users",ge=`
1
+ import{api as B,apiRequest as O,getUser as se}from"../api.js";import{addBannerAction as ne}from"../lib/plugin-chrome.js";import{SHAPES as oe,whileLoading as ie}from"../lib/loading.js";import{helpPopovers as re}from"../lib/help-popover.js";import{openMenuAt as F,settingsStore as le,sidebarCount as f,viewLifetime as ce}from"../lib/tool-kit.js";import{resetPassword as de,withdrawReset as ue}from"../lib/password-reset-actions.js";import{filterByProject as pe}from"../lib/project-context.js";import{FILTERS as me,SORTS as he,arrangeUsers as ve,canManage as W,resetAllowed as fe,roleIndex as be,userRow as ye}from"../lib/users-arrange.js";import{fmtDateTime as we}from"../lib/dates.js";import{avatarUrl as ge}from"../lib/avatar.js";const H=/^#\/(projects\/[^/?#]+\/)?users\/?(\?.*)?$/,b="#/users",ke=`
2
2
  <div class="usx-row" data-id="{{id}}" tabindex="0" data-bind-class="rowCls">
3
- <span class="usx-avatar" aria-hidden="true">{{initials}}</span>
3
+ <span class="usx-avatar {{avatarCls}}" aria-hidden="true"><img class="avatar-img" alt="" data-bind-src="avatarSrc" data-bind-hidden="!avatarSrc"><span class="avatar-initials" data-bind-hidden="avatarSrc">{{initials}}</span></span>
4
4
  <span class="usx-main"><strong>{{name}}</strong><small>{{meta}}</small></span>
5
5
  <span class="usx-chips">
6
6
  <span class="usx-chip is-ok" data-bind-hidden="!self">You</span>
@@ -11,4 +11,4 @@ import{api as B,apiRequest as O,getUser as oe}from"../api.js";import{addBannerAc
11
11
  <span class="usx-chip is-outline" data-bind-hidden="!resetOpen" title="A password reset link was sent and has not been used yet">Reset link open</span>
12
12
  </span>
13
13
  <button type="button" class="usx-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
14
- </div>`,K=o=>String(o??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),Y=le("users.list",o=>({filter:me.includes(o?.filter)?o.filter:"all",sort:he.includes(o?.sort)?o.sort:"name"})),r=ce(H);function ke(){const o=location.hash.match(/^#\/projects\/([^/?#]+)\/users/);if(!o)return null;try{return decodeURIComponent(o[1])}catch{return o[1]}}export const usersView={templateUrl:"/admin/js/templates/users.html",async onMount(o){r.reset();const i=o.get(0).querySelector(".usx")||o.get(0),m=()=>H.test(location.hash)&&i.isConnected,f=oe(),l=ke(),x=Y.read(),c=M.observable([]),d=M.observable({}),R=M.observable(!0),y=M.observable(""),u=M.observable(x.filter),w=M.observable(x.sort),g=M.observable(""),$=M.observable(""),k=M.observable(0);r.add(M.effect(()=>{Y.write({filter:u.value,sort:w.value})}));const A=M.observable([]);r.add(M.effect(()=>{const e=d.value,t=s=>{try{return D(s).fromNow?D(s).fromNow():we(s)}catch{return s}};A.value=fe(c.value,{filter:u.value,role:g.value,query:$.value,sort:w.value}).map(s=>ye(s,{roles:e,me:f,when:t}))}));const _=M.computed(()=>{const e=c.value,t=e.filter(s=>s.isActive===!1).length;return{all:e.length,active:e.length-t,inactive:t}}),G=M.computed(()=>c.value.length?"Nobody matches.":"No users yet - New user in the banner adds one."),J=M.computed(()=>l?`Showing only users in the project "${l}"${k.value?` - ${k.value} more elsewhere`:""}.`:""),S=e=>c.peek().find(t=>t.id===e);async function h(){try{const[e,t]=await ie(i.querySelector(".dm-loading-slot"),Promise.all([B.users.list(),O("/collections/roles/entries?limit=100",{method:"GET"}).catch(()=>({entries:[]}))]),ne.iconRows);if(!m())return;let s=Array.isArray(e)?e:[];if(l){const a=pe(s,l);k.value=s.length-a.length,s=a}d.value=be(t?.entries||[]),V(),c.value=s,y.value=""}catch(e){y.value=`Could not load the users: ${e.message||e}`}finally{R.value=!1}}function V(){const e=i.querySelector('.usx-select[aria-label="Role"]');if(!e)return;const t=g.peek();e.querySelectorAll('option:not([value=""])').forEach(s=>s.remove()),Object.entries(d.peek()).sort((s,a)=>s[1].level-a[1].level).forEach(([s,a])=>{const n=document.createElement("option");n.value=s,n.textContent=a.label,e.appendChild(n)}),e.value=t}const C=e=>{location.hash=`#/users/edit/${encodeURIComponent(e)}`};async function T(e,t){if(!(!t&&!await E.confirm(`Make ${K(e.name||e.email)} inactive? They will not be able to sign in until the account is made active again. Their work stays.`)))try{await O(`/users/${encodeURIComponent(e.id)}`,{method:"PUT",body:JSON.stringify({isActive:t})}),E.toast(`${e.name||e.email} is ${t?"active":"inactive"}.`,{type:"success"}),v(b),m()&&await h()}catch(s){E.toast(`Not changed: ${s.message||s}`,{type:"error"})}}async function z(e){if(await E.confirm(`Delete ${K(e.name||e.email)}? They lose access at once and this cannot be undone.`))try{await B.delete(`/users/${encodeURIComponent(e.id)}`),E.toast(`Deleted ${e.name||e.email}.`,{type:"success"}),v(b),m()&&await h()}catch(t){E.toast(`Not deleted: ${t.message||t}`,{type:"error"})}}async function L(e,t){await de(e,t)&&(v(b),m()&&await h())}async function Q(e){await ue(e)&&(v(b),m()&&await h())}async function X(e){try{await navigator.clipboard.writeText(e.email||""),E.toast("Email copied.",{type:"success"})}catch{E.toast(e.email||"No email",{type:"info",duration:6e3})}}function Z(e){const t=S(e);if(!t)return[];const s=W(f,t,d.peek()),a=s.self?"not on your own account":"your role is not senior to theirs",n=ve(f,t,d.peek()),se=!!t.resetLinkExpires&&Date.parse(t.resetLinkExpires)>Date.now(),P=q=>n.ok?q:`${q.replace(/…$/,"")} - ${n.why}`;return[{header:t.name||t.email},{label:s.self?"Edit my account":s.edit?"Edit":`Edit - ${a}`,icon:"edit",disabled:!s.edit,action:()=>C(t.id)},{label:"Copy email",icon:"copy",disabled:!t.email,action:()=>X(t)},{type:"divider"},{label:P("Send password reset email\u2026"),icon:"mail",disabled:!n.ok,action:()=>L(t,"email")},{label:P("Copy a reset link"),icon:"link",disabled:!n.ok,action:()=>L(t,"link")},...se?[{label:s.other?"Withdraw the reset link":`Withdraw the reset link - ${a}`,icon:"x-circle",disabled:!s.other,action:()=>Q(t)}]:[],{type:"divider"},t.isActive===!1?{label:s.other?"Make active":`Make active - ${a}`,icon:"play",disabled:!s.other,action:()=>T(t,!0)}:{label:s.other?"Make inactive\u2026":`Make inactive - ${a}`,icon:"pause",disabled:!s.other,action:()=>T(t,!1)},{label:s.other?"Delete\u2026":`Delete - ${a}`,icon:"trash",danger:s.other,disabled:!s.other,action:()=>z(t)}]}const p=i.querySelector(".usx-list");p.innerHTML=ge;const ee=M.applyBindings({users:c,loading:R,loadError:y,filter:u,sort:w,role:g,query:$,rows:A,counts:_,emptyText:G,projectNote:J},i,{methods:{showAll:()=>{u.value="all"},showActive:()=>{u.value="active"},showInactive:()=>{u.value="inactive"}}});r.add(()=>ee.dispose()),r.add(re(i,{position:"bottom"}));const te=E.contextMenu(p,{cursor:"glyph",match:".usx-row",inherit:!1,density:"compact",items:e=>Z(e.dataset.id)});r.add(()=>te?.destroy?.());const j=e=>{const t=S(e);t&&(W(f,t,d.peek()).edit?C(e):E.toast("Your role is not senior to theirs, so you cannot edit this account.",{type:"info"}))},N=e=>{const t=e.target.closest(".usx-row");if(!t)return;const s=e.target.closest(".usx-kebab");if(s){F(s);return}j(t.dataset.id)},U=e=>{const t=e.target.closest(".usx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),F(t)):e.key==="Enter"&&e.target===t&&j(t.dataset.id))};p.addEventListener("click",N),p.addEventListener("keydown",U),r.add(()=>{p.removeEventListener("click",N),p.removeEventListener("keydown",U)}),ae(o,{id:"users-new",icon:"plus",title:"New user",onClick:()=>{location.hash=l?`#/users/new?project=${encodeURIComponent(l)}`:"#/users/new"}}),I.scan(i),await h()}};
14
+ </div>`;function Ee(s){const o=typeof E.avatar?.tone=="function"?E.avatar.tone(s.name||s.email||""):null;return{avatarCls:o==null?"":`avatar avatar-sm avatar-tone-${o}`,avatarSrc:o==null?"":ge(s)}}const K=s=>String(s??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),Y=le("users.list",s=>({filter:me.includes(s?.filter)?s.filter:"all",sort:he.includes(s?.sort)?s.sort:"name"})),r=ce(H);function xe(){const s=location.hash.match(/^#\/projects\/([^/?#]+)\/users/);if(!s)return null;try{return decodeURIComponent(s[1])}catch{return s[1]}}export const usersView={templateUrl:"/admin/js/templates/users.html",async onMount(s){r.reset();const o=s.get(0).querySelector(".usx")||s.get(0),m=()=>H.test(location.hash)&&o.isConnected,v=se(),l=xe(),x=Y.read(),c=M.observable([]),d=M.observable({}),R=M.observable(!0),y=M.observable(""),u=M.observable(x.filter),w=M.observable(x.sort),g=M.observable(""),$=M.observable(""),k=M.observable(0);r.add(M.effect(()=>{Y.write({filter:u.value,sort:w.value})}));const S=M.observable([]);r.add(M.effect(()=>{const e=d.value,t=a=>{try{return D(a).fromNow?D(a).fromNow():we(a)}catch{return a}};S.value=ve(c.value,{filter:u.value,role:g.value,query:$.value,sort:w.value}).map(a=>({...ye(a,{roles:e,me:v,when:t}),...Ee(a)}))}));const _=M.computed(()=>{const e=c.value,t=e.filter(a=>a.isActive===!1).length;return{all:e.length,active:e.length-t,inactive:t}}),G=M.computed(()=>c.value.length?"Nobody matches.":"No users yet - New user in the banner adds one."),J=M.computed(()=>l?`Showing only users in the project "${l}"${k.value?` - ${k.value} more elsewhere`:""}.`:""),A=e=>c.peek().find(t=>t.id===e);async function h(){try{const[e,t]=await ie(o.querySelector(".dm-loading-slot"),Promise.all([B.users.list(),O("/collections/roles/entries?limit=100",{method:"GET"}).catch(()=>({entries:[]}))]),oe.iconRows);if(!m())return;let a=Array.isArray(e)?e:[];if(l){const n=pe(a,l);k.value=a.length-n.length,a=n}d.value=be(t?.entries||[]),V(),c.value=a,y.value=""}catch(e){y.value=`Could not load the users: ${e.message||e}`}finally{R.value=!1}}function V(){const e=o.querySelector('.usx-select[aria-label="Role"]');if(!e)return;const t=g.peek();e.querySelectorAll('option:not([value=""])').forEach(a=>a.remove()),Object.entries(d.peek()).sort((a,n)=>a[1].level-n[1].level).forEach(([a,n])=>{const i=document.createElement("option");i.value=a,i.textContent=n.label,e.appendChild(i)}),e.value=t}const C=e=>{location.hash=`#/users/edit/${encodeURIComponent(e)}`};async function T(e,t){if(!(!t&&!await E.confirm(`Make ${K(e.name||e.email)} inactive? They will not be able to sign in until the account is made active again. Their work stays.`)))try{await O(`/users/${encodeURIComponent(e.id)}`,{method:"PUT",body:JSON.stringify({isActive:t})}),E.toast(`${e.name||e.email} is ${t?"active":"inactive"}.`,{type:"success"}),f(b),m()&&await h()}catch(a){E.toast(`Not changed: ${a.message||a}`,{type:"error"})}}async function z(e){if(await E.confirm(`Delete ${K(e.name||e.email)}? They lose access at once and this cannot be undone.`))try{await B.delete(`/users/${encodeURIComponent(e.id)}`),E.toast(`Deleted ${e.name||e.email}.`,{type:"success"}),f(b),m()&&await h()}catch(t){E.toast(`Not deleted: ${t.message||t}`,{type:"error"})}}async function L(e,t){await de(e,t)&&(f(b),m()&&await h())}async function Q(e){await ue(e)&&(f(b),m()&&await h())}async function X(e){try{await navigator.clipboard.writeText(e.email||""),E.toast("Email copied.",{type:"success"})}catch{E.toast(e.email||"No email",{type:"info",duration:6e3})}}function Z(e){const t=A(e);if(!t)return[];const a=W(v,t,d.peek()),n=a.self?"not on your own account":"your role is not senior to theirs",i=fe(v,t,d.peek()),ae=!!t.resetLinkExpires&&Date.parse(t.resetLinkExpires)>Date.now(),P=q=>i.ok?q:`${q.replace(/…$/,"")} - ${i.why}`;return[{header:t.name||t.email},{label:a.self?"Edit my account":a.edit?"Edit":`Edit - ${n}`,icon:"edit",disabled:!a.edit,action:()=>C(t.id)},{label:"Copy email",icon:"copy",disabled:!t.email,action:()=>X(t)},{type:"divider"},{label:P("Send password reset email\u2026"),icon:"mail",disabled:!i.ok,action:()=>L(t,"email")},{label:P("Copy a reset link"),icon:"link",disabled:!i.ok,action:()=>L(t,"link")},...ae?[{label:a.other?"Withdraw the reset link":`Withdraw the reset link - ${n}`,icon:"x-circle",disabled:!a.other,action:()=>Q(t)}]:[],{type:"divider"},t.isActive===!1?{label:a.other?"Make active":`Make active - ${n}`,icon:"play",disabled:!a.other,action:()=>T(t,!0)}:{label:a.other?"Make inactive\u2026":`Make inactive - ${n}`,icon:"pause",disabled:!a.other,action:()=>T(t,!1)},{label:a.other?"Delete\u2026":`Delete - ${n}`,icon:"trash",danger:a.other,disabled:!a.other,action:()=>z(t)}]}const p=o.querySelector(".usx-list");p.innerHTML=ke;const ee=M.applyBindings({users:c,loading:R,loadError:y,filter:u,sort:w,role:g,query:$,rows:S,counts:_,emptyText:G,projectNote:J},o,{methods:{showAll:()=>{u.value="all"},showActive:()=>{u.value="active"},showInactive:()=>{u.value="inactive"}}});r.add(()=>ee.dispose()),r.add(re(o,{position:"bottom"}));const te=E.contextMenu(p,{cursor:"glyph",match:".usx-row",inherit:!1,density:"compact",items:e=>Z(e.dataset.id)});r.add(()=>te?.destroy?.());const j=e=>{const t=A(e);t&&(W(v,t,d.peek()).edit?C(e):E.toast("Your role is not senior to theirs, so you cannot edit this account.",{type:"info"}))},U=e=>{const t=e.target.closest(".usx-row");if(!t)return;const a=e.target.closest(".usx-kebab");if(a){F(a);return}j(t.dataset.id)},N=e=>{const t=e.target.closest(".usx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),F(t)):e.key==="Enter"&&e.target===t&&j(t.dataset.id))};p.addEventListener("click",U),p.addEventListener("keydown",N),r.add(()=>{p.removeEventListener("click",U),p.removeEventListener("keydown",N)}),ne(s,{id:"users-new",icon:"plus",title:"New user",onClick:()=>{location.hash=l?`#/users/new?project=${encodeURIComponent(l)}`:"#/users/new"}}),I.scan(o),await h()}};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.101.0",
3
+ "version": "0.101.2",
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",
@@ -83,7 +83,7 @@
83
83
  "@fastify/rate-limit": "^10.3.0",
84
84
  "@fastify/static": "9.1.1",
85
85
  "bcryptjs": "^3.0.3",
86
- "domma-js": "0.49.0",
86
+ "domma-js": "0.50.0",
87
87
  "dotenv": "^17.2.3",
88
88
  "fastify": "5.8.5",
89
89
  "gray-matter": "^4.0.3",