domma-cms 0.66.0 → 0.69.3
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/CLAUDE.md +64 -1
- package/README.md +14 -10
- package/admin/css/admin.css +1 -1
- package/admin/index.html +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/analytics-shape.js +1 -0
- package/admin/js/lib/contacts-arrange.js +1 -0
- package/admin/js/lib/help-popover.js +1 -0
- package/admin/js/lib/notes-arrange.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -1
- package/admin/js/lib/plugin-chrome.js +1 -1
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-grouping.test.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +3 -3
- package/admin/js/lib/todo-arrange.js +1 -0
- package/admin/js/lib/tool-kit.js +1 -0
- package/admin/js/templates/analytics.html +138 -0
- package/admin/js/templates/contacts.html +156 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/admin/js/templates/theme.html +38 -0
- package/admin/js/templates/todo.html +113 -0
- package/admin/js/views/analytics.js +62 -0
- package/admin/js/views/contacts.js +174 -0
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/notes.js +104 -0
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +24 -22
- package/admin/js/views/theme.js +1 -1
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +4 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +14 -0
- package/config/menus/admin-sidebar.json +1 -1
- package/config/server.json +3 -1
- package/package.json +3 -7
- package/plugins/_lib/admin/mail/reader-view.js +20 -0
- package/plugins/_lib/mail/store.js +21 -1
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -1
- package/plugins/blog/admin/templates/blog.html +35 -53
- package/plugins/blog/admin/templates/categories.html +19 -6
- package/plugins/blog/admin/templates/comments.html +26 -9
- package/plugins/blog/admin/templates/post-editor.html +36 -13
- package/plugins/blog/admin/templates/settings.html +2 -11
- package/plugins/blog/admin/views/blog.js +288 -162
- package/plugins/blog/admin/views/categories.js +212 -201
- package/plugins/blog/admin/views/comments.js +207 -158
- package/plugins/blog/admin/views/kit.js +372 -0
- package/plugins/blog/admin/views/post-editor.js +269 -21
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -86
- package/plugins/blog/blocks/blog-card-compact.css +17 -0
- package/plugins/blog/blocks/blog-card-compact.html +8 -0
- package/plugins/blog/blocks/blog-card-row.css +22 -0
- package/plugins/blog/blocks/blog-card-row.html +11 -0
- package/plugins/blog/blocks/blog-card.css +35 -0
- package/plugins/blog/blocks/blog-card.html +16 -0
- package/plugins/blog/blocks/blog-post-classic.css +15 -0
- package/plugins/blog/blocks/blog-post-classic.html +28 -0
- package/plugins/blog/blocks/blog-post-feature.css +20 -0
- package/plugins/blog/blocks/blog-post-feature.html +30 -0
- package/plugins/blog/blocks/blog-post-minimal.css +10 -0
- package/plugins/blog/blocks/blog-post-minimal.html +23 -0
- package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
- package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
- package/plugins/blog/collections/posts/schema.json +3 -1
- package/plugins/blog/config.js +6 -0
- package/plugins/blog/lib/layouts.js +304 -0
- package/plugins/blog/lib/page.js +158 -0
- package/plugins/blog/lib/render.js +119 -0
- package/plugins/blog/lib/samples.js +234 -0
- package/plugins/blog/plugin.js +225 -15
- package/plugins/blog/plugin.json +21 -81
- package/plugins/blog/plugin.public.js +115 -210
- package/plugins/blog/public/blog.css +177 -0
- package/plugins/blog/public/blog.js +586 -0
- package/plugins/blog/public/samples/aurora.svg +7 -0
- package/plugins/blog/public/samples/dusk.svg +7 -0
- package/plugins/blog/public/samples/ember.svg +7 -0
- package/plugins/blog/public/samples/harbour.svg +7 -0
- package/plugins/blog/public/samples/meadow.svg +7 -0
- package/plugins/blog/public/samples/rosewood.svg +7 -0
- package/plugins/blog/templates/index.html +9 -8
- package/plugins/blog/templates/post.html +4 -17
- package/plugins/blog/tests/layouts.test.js +97 -0
- package/plugins/blog/tests/public.test.js +71 -0
- package/plugins/free-tier.lock.json +74 -0
- package/plugins/mail-reader/CLAUDE.md +78 -0
- package/plugins/mail-reader/plugin.json +2 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +41 -2
- package/scripts/run-tests.mjs +92 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/{plugins/contacts/plugin.js → server/routes/api/contacts.js} +71 -43
- package/server/routes/api/dashboard.js +8 -2
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +148 -3
- package/server/routes/api/todo.js +178 -0
- package/server/server.js +61 -1
- package/server/services/collections.js +13 -3
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +54 -1
- package/server/services/permissionRegistry.js +50 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +117 -7
- package/server/services/presetCollections.js +344 -0
- package/server/services/promoted-plugins-migration.js +254 -0
- package/server/services/publicCors.js +46 -0
- package/server/services/renderer.js +83 -3
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +307 -0
- package/server/services/themeSettings.js +39 -0
- package/config/theme.json +0 -18
- package/plugins/analytics/admin/css/index.css +0 -1
- package/plugins/analytics/admin/templates/analytics.html +0 -70
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -60
- package/plugins/analytics/public/inject-body.html +0 -60
- package/plugins/analytics/public/inject-head.html +0 -1
- package/plugins/blog/templates/author.html +0 -9
- package/plugins/blog/templates/category.html +0 -9
- package/plugins/blog/templates/tag.html +0 -9
- package/plugins/contacts/admin/templates/contacts.html +0 -254
- package/plugins/contacts/admin/views/contacts.js +0 -943
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -179
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/plugin.json +0 -36
- package/plugins/demo-viewer/config.js +0 -4
- package/plugins/demo-viewer/plugin.js +0 -36
- package/plugins/demo-viewer/plugin.json +0 -9
- package/plugins/invoice/admin/css/index.css +0 -1
- package/plugins/invoice/admin/templates/editor.html +0 -220
- package/plugins/invoice/admin/templates/index.html +0 -173
- package/plugins/invoice/admin/templates/issuers.html +0 -2
- package/plugins/invoice/admin/templates/receivers.html +0 -2
- package/plugins/invoice/admin/views/contacts-source.js +0 -266
- package/plugins/invoice/admin/views/editor.js +0 -434
- package/plugins/invoice/admin/views/export.js +0 -199
- package/plugins/invoice/admin/views/help-content.js +0 -61
- package/plugins/invoice/admin/views/index.js +0 -643
- package/plugins/invoice/admin/views/issuers.js +0 -30
- package/plugins/invoice/admin/views/media.js +0 -172
- package/plugins/invoice/admin/views/party-view.js +0 -386
- package/plugins/invoice/admin/views/payments.js +0 -127
- package/plugins/invoice/admin/views/print.js +0 -130
- package/plugins/invoice/admin/views/receivers.js +0 -55
- package/plugins/invoice/admin/views/send.js +0 -212
- package/plugins/invoice/admin/views/settings.js +0 -594
- package/plugins/invoice/admin/views/view-lifecycle.js +0 -33
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -82
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -16
- package/plugins/invoice/collections/invoices/schema.json +0 -33
- package/plugins/invoice/config.js +0 -37
- package/plugins/invoice/pdf.js +0 -164
- package/plugins/invoice/plugin.js +0 -1456
- package/plugins/invoice/plugin.json +0 -86
- package/plugins/invoice/templates/_base.css +0 -1
- package/plugins/invoice/templates/classic-nologo.html +0 -100
- package/plugins/invoice/templates/classic.html +0 -91
- package/plugins/invoice/templates/invoice-print.html +0 -237
- package/plugins/invoice/templates/minimal.html +0 -99
- package/plugins/invoice/templates/modern-nologo.html +0 -114
- package/plugins/invoice/templates/modern.html +0 -113
- package/plugins/invoice/templates/templates.json +0 -11
- package/plugins/notes/admin/templates/notes.html +0 -83
- package/plugins/notes/admin/views/notes.js +0 -281
- package/plugins/notes/collections/user-notes/schema.json +0 -54
- package/plugins/notes/config.js +0 -6
- package/plugins/notes/plugin.json +0 -33
- package/plugins/surveys/admin/templates/audience.html +0 -47
- package/plugins/surveys/admin/templates/results.html +0 -58
- package/plugins/surveys/admin/templates/survey-editor.html +0 -141
- package/plugins/surveys/admin/templates/surveys.html +0 -25
- package/plugins/surveys/admin/views/audience.js +0 -301
- package/plugins/surveys/admin/views/results.js +0 -172
- package/plugins/surveys/admin/views/survey-editor.js +0 -211
- package/plugins/surveys/admin/views/surveys.js +0 -161
- package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
- package/plugins/surveys/collections/survey-groups/schema.json +0 -11
- package/plugins/surveys/collections/survey-invites/schema.json +0 -16
- package/plugins/surveys/collections/surveys/schema.json +0 -23
- package/plugins/surveys/config.js +0 -8
- package/plugins/surveys/lib/audience.js +0 -37
- package/plugins/surveys/lib/campaigns.js +0 -43
- package/plugins/surveys/lib/ledger.js +0 -110
- package/plugins/surveys/lib/sending.js +0 -106
- package/plugins/surveys/lib/stats.js +0 -62
- package/plugins/surveys/lib/submit.js +0 -95
- package/plugins/surveys/lib/tokens.js +0 -28
- package/plugins/surveys/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- package/plugins/surveys/plugin.public.js +0 -149
- package/plugins/surveys/public/dist-shared.mjs +0 -1
- package/plugins/surveys/public/survey.css +0 -1
- package/plugins/surveys/public/survey.mjs +0 -1
- package/plugins/surveys/templates/survey-page.html +0 -63
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
- package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
- package/plugins/theme-switcher/config.js +0 -10
- package/plugins/theme-switcher/plugin.js +0 -26
- package/plugins/theme-switcher/plugin.json +0 -74
- package/plugins/theme-switcher/public/inject-body.html +0 -153
- package/plugins/theme-switcher/public/inject-head.html +0 -260
- package/plugins/todo/admin/templates/todo.html +0 -158
- package/plugins/todo/admin/views/todo.js +0 -343
- package/plugins/todo/collections/todos/schema.json +0 -60
- package/plugins/todo/config.js +0 -7
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
- /package/{plugins/contacts/admin/css/index.css → admin/css/contacts.css} +0 -0
package/admin/js/views/theme.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as T}from"../api.js";import{populateThemeSelect as w}from"../lib/themes.js";const se=[{value:"#site-navbar",label:"Navigation bar"},{value:"#site-footer",label:"Footer"},{value:".site-content",label:"Page content area"},{value:".page-body",label:"Page body (inside the container)"},{value:".site-sidebar",label:"Sidebar"},{value:".card",label:"Every card"},{value:".cms-form-embed",label:"Embedded forms"},{value:".cms-collection",label:"Collection displays"}];export const themeView={templateUrl:"/admin/js/templates/theme.html",async onMount(r){E.tabs(r.find("#theme-tabs").get(0));const F=E.loader(r.get(0),{type:"dots"});let d,R;try{[d,R]=await Promise.all([T.theme.get(),T.theme.catalog().catch(()=>({themes:[],tokens:[]}))])}catch(e){F.destroy(),E.toast(`Could not load theme settings: ${e.message||e}`,{type:"error"});return}F.destroy();const z=R.tokens||[],A=new Map;async function K(e){if(!e)return null;if(A.has(e))return A.get(e);const t=await T.theme.themeTokens(e).catch(()=>null),n=t&&t.tokens?t.tokens:null;return A.set(e,n),n}function $(){const e=r.find("#theme-auto-enabled").prop("checked");return d.baseTheme||(e?S.val():f.val())||d.theme}const V=["--dm-card-bg","--dm-surface"],_=[...V,"--dm-text","--dm-primary","--dm-background"];function H(e){const t=document.createElement("span");return t.style.cssText="display:inline-block;width:.7rem;height:.7rem;border-radius:2px;border:1px solid var(--dm-border);vertical-align:middle;margin-right:.25rem;",/^#[0-9a-f]{3,8}$/i.test(e)?t.style.background=e:t.style.background="transparent",t}async function I(e){const t=e.querySelector(".ov-theme-diff");if(!t)return;for(;t.firstChild;)t.removeChild(t.firstChild);const n=e.querySelector(".ov-theme")?.value;if(!n)return;const o=$();if(n===o){const l=document.createElement("p");l.className="form-hint text-muted",l.style.cssText="margin:.4rem 0 0;font-size:.8rem;",l.textContent="That is already the site theme, so this changes nothing on its own. Set a token below instead.",t.appendChild(l);return}const[a,s]=await Promise.all([K(o),K(n)]);if(!a||!s)return;const c=Object.keys(s).filter(l=>String(a[l]??"").toLowerCase()!==String(s[l]).toLowerCase()),i=V.filter(l=>c.includes(l)),m=document.createElement("div");m.style.cssText="margin-top:.5rem;padding:.5rem .6rem;border-radius:var(--dm-radius-md);background:var(--dm-surface-raised);font-size:.8rem;";const h=document.createElement("p");if(h.style.cssText="margin:0 0 .35rem;",h.textContent=`Changes ${c.length} of ${Object.keys(s).length} tokens here.`,m.appendChild(h),!i.length){const l=document.createElement("p");l.style.cssText="margin:0 0 .35rem;color:var(--dm-warning);",l.textContent="Both themes use the same surface colour, so this region keeps the fill it has now. Set --dm-card-bg (a card) or --dm-surface (most other regions) below to change it.",m.appendChild(l)}const u=document.createElement("div");u.style.cssText="display:flex;flex-wrap:wrap;gap:.15rem .75rem;";const v=[..._.filter(l=>c.includes(l)),...c.filter(l=>!_.includes(l))];for(const l of v.slice(0,6)){const p=document.createElement("span");p.style.cssText="white-space:nowrap;color:var(--dm-text-muted);",p.appendChild(H(a[l]||""));const C=document.createElement("span");C.textContent=`${l} `,p.appendChild(C),p.appendChild(H(s[l])),u.appendChild(p)}if(v.length>6){const l=document.createElement("span");l.style.cssText="color:var(--dm-text-muted);",l.textContent=`+${v.length-6} more`,u.appendChild(l)}m.appendChild(u),t.appendChild(m)}let b=Array.isArray(d.overrides)?d.overrides.map(G):[];const f=r.find("#theme-frontend"),B=r.find("#theme-admin");w(f.get(0)),w(B.get(0),{includeAdmin:!0}),f.val(d.baseTheme||d.theme||"charcoal-dark"),B.val(d.adminTheme||"charcoal-dark"),d.baseTheme&&(f.prop("disabled",!0),r.find("#theme-auto-enabled").prop("disabled",!0),r.find("#theme-auto-roller-hint").show());const S=r.find("#theme-day"),D=r.find("#theme-night");w(S.get(0)),w(D.get(0));const g=d.autoTheme||{};S.val(g.dayTheme||"charcoal-light"),D.val(g.nightTheme||"charcoal-dark"),r.find("#theme-day-start").val(g.dayStart||"07:00"),r.find("#theme-night-start").val(g.nightStart||"19:00");const U=r.find("#theme-auto-enabled"),W=r.find("#theme-auto-fields");d.baseTheme||(U.prop("checked",!!g.enabled),g.enabled&&(W.show(),f.prop("disabled",!0)),U.on("change",function(){W.toggle(this.checked),f.prop("disabled",this.checked)})),r.find("#theme-font-family").val(d.font?.family||"Roboto"),r.find("#theme-font-size").val(String(d.font?.size??16));function G(e){return{id:e.id||`ov-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,label:e.label||"",enabled:e.enabled!==!1,scope:e.scope||"",theme:e.theme||"",tokens:{...e.tokens||{}},css:e.css||""}}const y=document.getElementById("theme-overrides"),ee=document.getElementById("theme-overrides-empty");function L(){for(;y.firstChild;)y.removeChild(y.firstChild);ee.style.display=b.length?"none":"",b.forEach((e,t)=>y.appendChild(te(e,t))),Domma.icons.scan("#theme-overrides")}function te(e,t){const n=document.createElement("div");n.className="card mb-3",n.dataset.i=String(t);const o=document.createElement("div");o.className="card-header";const a=document.createElement("h3");a.className="m-0",a.style.fontSize="1rem",a.textContent=e.label||e.scope||`Override ${t+1}`,o.appendChild(a);const s=document.createElement("div");s.style.cssText="display:flex;gap:.5rem;align-items:center;";const c=document.createElement("label");c.style.cssText="display:flex;align-items:center;gap:.35rem;font-size:.85rem;cursor:pointer;";const i=document.createElement("input");i.type="checkbox",i.className="form-check ov-enabled",i.checked=e.enabled,c.appendChild(i),c.appendChild(document.createTextNode("Enabled")),s.appendChild(c);const m=document.createElement("button");m.type="button",m.className="btn btn-ghost btn-sm ov-remove",m.textContent="Remove",s.appendChild(m),o.appendChild(s),n.appendChild(o);const h=document.createElement("div");h.className="card-body";const u=document.createElement("div");u.className="row mb-3",u.appendChild(P("col-4","Name",X("ov-label",e.label,"Navbar island"))),u.appendChild(P("col-8","Target selector",ne(e.scope),"The region to re-theme. Everything inside it follows.")),h.appendChild(u);const v=document.createElement("div");v.className="row mb-3";const l=document.createElement("select");l.className="form-select ov-theme",w(l,{includeDefault:!0,defaultLabel:"Keep the site theme"}),l.value=e.theme||"",v.appendChild(P("col-6","Adopt a theme here",l,"Copies every token of that theme onto this region.")),h.appendChild(v);const p=document.createElement("div");p.className="ov-theme-diff",h.appendChild(p);const C=document.createElement("div");C.className="ov-tokens mb-2",Object.entries(e.tokens||{}).forEach(([oe,ae])=>{C.appendChild(J(oe,ae))}),h.appendChild(Q("Token overrides",C));const q=document.createElement("button");q.type="button",q.className="btn btn-ghost btn-sm ov-add-token",q.textContent="+ Add token",h.appendChild(q);const x=document.createElement("details");x.style.marginTop="1rem";const j=document.createElement("summary");j.style.cssText="cursor:pointer;font-size:.85rem;",j.textContent="Custom CSS for this override",x.appendChild(j);const k=document.createElement("textarea");k.className="form-input ov-css",k.rows=4,k.placeholder="#site-navbar .brand { letter-spacing: .04em; }",k.value=e.css||"",k.style.marginTop=".5rem",x.appendChild(k);const O=document.createElement("p");return O.className="form-hint text-muted",O.style.cssText="font-size:.8rem;margin-top:.35rem;",O.textContent="Appended verbatim after the generated rule - you own these selectors. Prefer tokens: they survive a Domma upgrade, hand-written rules fight the cascade.",x.appendChild(O),h.appendChild(x),n.appendChild(h),setTimeout(()=>I(n),0),n}function J(e,t){const n=document.createElement("div");n.className="ov-token-row",n.style.cssText="display:flex;gap:.5rem;align-items:center;margin-bottom:.5rem;";const o=document.createElement("select");if(o.className="form-select ov-token-name",o.style.flex="1 1 auto",z.length){for(const i of z){const m=document.createElement("option");m.value=i,m.textContent=i,o.appendChild(m)}o.value=e||z[0]}else{const i=document.createElement("option");i.value=e||"",i.textContent=e||"(no tokens found)",o.appendChild(i)}const a=document.createElement("input");a.type="text",a.className="form-input ov-token-value",a.style.flex="0 0 150px",a.placeholder="#0096c7",a.value=t||"";const s=document.createElement("input");s.type="color",s.className="ov-token-swatch",s.style.cssText="flex:0 0 38px;height:34px;padding:0;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);background:none;cursor:pointer;",/^#[0-9a-f]{6}$/i.test(t||"")&&(s.value=t),s.addEventListener("input",()=>{a.value=s.value,a.dispatchEvent(new Event("input",{bubbles:!0}))});const c=document.createElement("button");return c.type="button",c.className="btn btn-ghost btn-sm ov-token-remove",c.textContent="\xD7",n.append(o,a,s,c),n}function P(e,t,n,o){const a=document.createElement("div");return a.className=e,a.appendChild(Q(t,n,o)),a}function Q(e,t,n){const o=document.createDocumentFragment(),a=document.createElement("label");if(a.className="form-label",a.textContent=e,o.appendChild(a),o.appendChild(t),n){const s=document.createElement("p");s.className="form-hint text-muted",s.style.cssText="margin-top:.35rem;font-size:.8rem;",s.textContent=n,o.appendChild(s)}return o}function X(e,t,n){const o=document.createElement("input");return o.type="text",o.className=`form-input ${e}`,o.value=t||"",o.placeholder=n||"",o}function ne(e){const t=document.createElement("div"),n=X("ov-scope",e,"#site-navbar");if(n.setAttribute("list","theme-scope-options"),t.appendChild(n),!document.getElementById("theme-scope-options")){const o=document.createElement("datalist");o.id="theme-scope-options";for(const a of se){const s=document.createElement("option");s.value=a.value,s.label=a.label,o.appendChild(s)}t.appendChild(o)}return t}function N(){const e=y.querySelectorAll(".card"),t=[];return e.forEach((n,o)=>{const a={};n.querySelectorAll(".ov-token-row").forEach(s=>{const c=s.querySelector(".ov-token-name")?.value?.trim(),i=s.querySelector(".ov-token-value")?.value?.trim();c&&i&&(a[c]=i)}),t.push({id:b[o]?.id,label:n.querySelector(".ov-label")?.value?.trim()||"",enabled:!!n.querySelector(".ov-enabled")?.checked,scope:n.querySelector(".ov-scope")?.value?.trim()||"",theme:n.querySelector(".ov-theme")?.value||"",tokens:a,css:n.querySelector(".ov-css")?.value||""})}),b=t,t}r.on("click",".ov-remove",function(){const e=Number(this.closest(".card").dataset.i);N(),b.splice(e,1),L()}),r.on("change",".ov-theme",function(){I(this.closest(".card"))}),r.on("change","#theme-frontend, #theme-auto-enabled, #theme-day",()=>{y.querySelectorAll(".card").forEach(I)}),r.on("click",".ov-add-token",function(){this.closest(".card").querySelector(".ov-tokens").appendChild(J("",""))}),r.on("click",".ov-token-remove",function(){this.closest(".ov-token-row").remove()}),r.find("#theme-add-override").on("click",()=>{N(),b.push(G({label:"",scope:"",enabled:!0})),L()}),L();async function M(){const e=document.getElementById("theme-css-preview"),t={...Y(),overrides:N()};try{const n=await T.theme.compile(t);e.textContent=n.css?.trim()||"(no overrides - nothing is injected)",e.style.color=""}catch(n){e.textContent=n.message||String(n),e.style.color="var(--dm-danger)"}}r.find("#theme-refresh-css").on("click",M);function Y(){const e=!d.baseTheme&&r.find("#theme-auto-enabled").prop("checked"),t={enabled:e,dayTheme:S.val()||"charcoal-light",nightTheme:D.val()||"charcoal-dark",dayStart:r.find("#theme-day-start").val()||"07:00",nightStart:r.find("#theme-night-start").val()||"19:00"};return{theme:d.baseTheme?d.theme:e?t.dayTheme:f.val(),...d.baseTheme?{baseTheme:d.baseTheme}:{},adminTheme:B.val(),autoTheme:t,font:{family:r.find("#theme-font-family").val()||"Roboto",size:parseInt(r.find("#theme-font-size").val(),10)||16}}}function Z(e){const t=document.getElementById("theme-errors");for(;t.firstChild;)t.removeChild(t.firstChild);if(!e||!e.length){t.style.display="none";return}t.style.display="";for(const n of e){const o=document.createElement("div");o.textContent=n,t.appendChild(o)}t.scrollIntoView({behavior:"smooth",block:"nearest"})}r.find("#theme-save-btn").on("click",async function(){Z(null);const e={...Y(),overrides:N()};this.disabled=!0;try{d=await T.theme.save(e),Domma.theme.set(e.adminTheme),E.toast("Theme saved.",{type:"success"}),await M()}catch(t){const n=t.validation||[t.message||"Could not save the theme."];Z(n),E.toast("Theme not saved.",{type:"error"})}finally{this.disabled=!1}}),await M()}};
|
|
1
|
+
import{api as x}from"../api.js";import{populateThemeSelect as T}from"../lib/themes.js";const ce=[{value:"#site-navbar",label:"Navigation bar"},{value:"#site-footer",label:"Footer"},{value:".site-content",label:"Page content area"},{value:".page-body",label:"Page body (inside the container)"},{value:".site-sidebar",label:"Sidebar"},{value:".card",label:"Every card"},{value:".cms-form-embed",label:"Embedded forms"},{value:".cms-collection",label:"Collection displays"}];export const themeView={templateUrl:"/admin/js/templates/theme.html",async onMount(o){E.tabs(o.find("#theme-tabs").get(0));const K=E.loader(o.get(0),{type:"dots"});let d,V;try{[d,V]=await Promise.all([x.theme.get(),x.theme.catalog().catch(()=>({themes:[],tokens:[]}))])}catch(e){K.destroy(),E.toast(`Could not load theme settings: ${e.message||e}`,{type:"error"});return}K.destroy();const z=V.tokens||[],B=new Map;async function _(e){if(!e)return null;if(B.has(e))return B.get(e);const t=await x.theme.themeTokens(e).catch(()=>null),n=t&&t.tokens?t.tokens:null;return B.set(e,n),n}function ne(){const e=o.find("#theme-auto-enabled").prop("checked");return d.baseTheme||(e?S.val():f.val())||d.theme}const X=["--dm-card-bg","--dm-surface"],Y=[...X,"--dm-text","--dm-primary","--dm-background"];function H(e){const t=document.createElement("span");return t.style.cssText="display:inline-block;width:.7rem;height:.7rem;border-radius:2px;border:1px solid var(--dm-border);vertical-align:middle;margin-right:.25rem;",/^#[0-9a-f]{3,8}$/i.test(e)?t.style.background=e:t.style.background="transparent",t}async function D(e){const t=e.querySelector(".ov-theme-diff");if(!t)return;for(;t.firstChild;)t.removeChild(t.firstChild);const n=e.querySelector(".ov-theme")?.value;if(!n)return;const s=ne();if(n===s){const r=document.createElement("p");r.className="form-hint text-muted",r.style.cssText="margin:.4rem 0 0;font-size:.8rem;",r.textContent="That is already the site theme, so this changes nothing on its own. Set a token below instead.",t.appendChild(r);return}const[a,l]=await Promise.all([_(s),_(n)]);if(!a||!l)return;const c=Object.keys(l).filter(r=>String(a[r]??"").toLowerCase()!==String(l[r]).toLowerCase()),i=X.filter(r=>c.includes(r)),m=document.createElement("div");m.style.cssText="margin-top:.5rem;padding:.5rem .6rem;border-radius:var(--dm-radius-md);background:var(--dm-surface-raised);font-size:.8rem;";const h=document.createElement("p");if(h.style.cssText="margin:0 0 .35rem;",h.textContent=`Changes ${c.length} of ${Object.keys(l).length} tokens here.`,m.appendChild(h),!i.length){const r=document.createElement("p");r.style.cssText="margin:0 0 .35rem;color:var(--dm-warning);",r.textContent="Both themes use the same surface colour, so this region keeps the fill it has now. Set --dm-card-bg (a card) or --dm-surface (most other regions) below to change it.",m.appendChild(r)}const p=document.createElement("div");p.style.cssText="display:flex;flex-wrap:wrap;gap:.15rem .75rem;";const v=[...Y.filter(r=>c.includes(r)),...c.filter(r=>!Y.includes(r))];for(const r of v.slice(0,6)){const u=document.createElement("span");u.style.cssText="white-space:nowrap;color:var(--dm-text-muted);",u.appendChild(H(a[r]||""));const C=document.createElement("span");C.textContent=`${r} `,u.appendChild(C),u.appendChild(H(l[r])),p.appendChild(u)}if(v.length>6){const r=document.createElement("span");r.style.cssText="color:var(--dm-text-muted);",r.textContent=`+${v.length-6} more`,p.appendChild(r)}m.appendChild(p),t.appendChild(m)}let b=Array.isArray(d.overrides)?d.overrides.map(J):[];const f=o.find("#theme-frontend"),F=o.find("#theme-admin");T(f.get(0)),T(F.get(0),{includeAdmin:!0}),f.val(d.baseTheme||d.theme||"charcoal-dark"),F.val(d.adminTheme||"charcoal-dark"),d.baseTheme&&(f.prop("disabled",!0),o.find("#theme-auto-enabled").prop("disabled",!0),o.find("#theme-auto-roller-hint").show());const S=o.find("#theme-day"),L=o.find("#theme-night");T(S.get(0)),T(L.get(0));const y=d.autoTheme||{};S.val(y.dayTheme||"charcoal-light"),L.val(y.nightTheme||"charcoal-dark"),o.find("#theme-day-start").val(y.dayStart||"07:00"),o.find("#theme-night-start").val(y.nightStart||"19:00");const N=d.switcher||{},q=o.find("#theme-switcher-enabled"),oe=o.find("#theme-switcher-fields");q.prop("checked",N.enabled===!0),o.find("#theme-switcher-position").val(N.position||"right-center"),o.find("#theme-switcher-offset-x").val(N.offsetX??32),o.find("#theme-switcher-offset-y").val(N.offsetY??32);const U=()=>{oe.get(0).style.display=q.prop("checked")?"":"none"};U(),q.on("change",U);const W=o.find("#theme-auto-enabled"),G=o.find("#theme-auto-fields");d.baseTheme||(W.prop("checked",!!y.enabled),y.enabled&&(G.show(),f.prop("disabled",!0)),W.on("change",function(){G.toggle(this.checked),f.prop("disabled",this.checked)})),o.find("#theme-font-family").val(d.font?.family||"Roboto"),o.find("#theme-font-size").val(String(d.font?.size??16));function J(e){return{id:e.id||`ov-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,label:e.label||"",enabled:e.enabled!==!1,scope:e.scope||"",theme:e.theme||"",tokens:{...e.tokens||{}},css:e.css||""}}const g=document.getElementById("theme-overrides"),se=document.getElementById("theme-overrides-empty");function P(){for(;g.firstChild;)g.removeChild(g.firstChild);se.style.display=b.length?"none":"",b.forEach((e,t)=>g.appendChild(ae(e,t))),Domma.icons.scan("#theme-overrides")}function ae(e,t){const n=document.createElement("div");n.className="card mb-3",n.dataset.i=String(t);const s=document.createElement("div");s.className="card-header";const a=document.createElement("h3");a.className="m-0",a.style.fontSize="1rem",a.textContent=e.label||e.scope||`Override ${t+1}`,s.appendChild(a);const l=document.createElement("div");l.style.cssText="display:flex;gap:.5rem;align-items:center;";const c=document.createElement("label");c.style.cssText="display:flex;align-items:center;gap:.35rem;font-size:.85rem;cursor:pointer;";const i=document.createElement("input");i.type="checkbox",i.className="form-check ov-enabled",i.checked=e.enabled,c.appendChild(i),c.appendChild(document.createTextNode("Enabled")),l.appendChild(c);const m=document.createElement("button");m.type="button",m.className="btn btn-ghost btn-sm ov-remove",m.textContent="Remove",l.appendChild(m),s.appendChild(l),n.appendChild(s);const h=document.createElement("div");h.className="card-body";const p=document.createElement("div");p.className="row mb-3",p.appendChild(M("col-4","Name",$("ov-label",e.label,"Navbar island"))),p.appendChild(M("col-8","Target selector",le(e.scope),"The region to re-theme. Everything inside it follows.")),h.appendChild(p);const v=document.createElement("div");v.className="row mb-3";const r=document.createElement("select");r.className="form-select ov-theme",T(r,{includeDefault:!0,defaultLabel:"Keep the site theme"}),r.value=e.theme||"",v.appendChild(M("col-6","Adopt a theme here",r,"Copies every token of that theme onto this region.")),h.appendChild(v);const u=document.createElement("div");u.className="ov-theme-diff",h.appendChild(u);const C=document.createElement("div");C.className="ov-tokens mb-2",Object.entries(e.tokens||{}).forEach(([re,de])=>{C.appendChild(Q(re,de))}),h.appendChild(Z("Token overrides",C));const A=document.createElement("button");A.type="button",A.className="btn btn-ghost btn-sm ov-add-token",A.textContent="+ Add token",h.appendChild(A);const w=document.createElement("details");w.style.marginTop="1rem";const R=document.createElement("summary");R.style.cssText="cursor:pointer;font-size:.85rem;",R.textContent="Custom CSS for this override",w.appendChild(R);const k=document.createElement("textarea");k.className="form-input ov-css",k.rows=4,k.placeholder="#site-navbar .brand { letter-spacing: .04em; }",k.value=e.css||"",k.style.marginTop=".5rem",w.appendChild(k);const I=document.createElement("p");return I.className="form-hint text-muted",I.style.cssText="font-size:.8rem;margin-top:.35rem;",I.textContent="Appended verbatim after the generated rule - you own these selectors. Prefer tokens: they survive a Domma upgrade, hand-written rules fight the cascade.",w.appendChild(I),h.appendChild(w),n.appendChild(h),setTimeout(()=>D(n),0),n}function Q(e,t){const n=document.createElement("div");n.className="ov-token-row",n.style.cssText="display:flex;gap:.5rem;align-items:center;margin-bottom:.5rem;";const s=document.createElement("select");if(s.className="form-select ov-token-name",s.style.flex="1 1 auto",z.length){for(const i of z){const m=document.createElement("option");m.value=i,m.textContent=i,s.appendChild(m)}s.value=e||z[0]}else{const i=document.createElement("option");i.value=e||"",i.textContent=e||"(no tokens found)",s.appendChild(i)}const a=document.createElement("input");a.type="text",a.className="form-input ov-token-value",a.style.flex="0 0 150px",a.placeholder="#0096c7",a.value=t||"";const l=document.createElement("input");l.type="color",l.className="ov-token-swatch",l.style.cssText="flex:0 0 38px;height:34px;padding:0;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);background:none;cursor:pointer;",/^#[0-9a-f]{6}$/i.test(t||"")&&(l.value=t),l.addEventListener("input",()=>{a.value=l.value,a.dispatchEvent(new Event("input",{bubbles:!0}))});const c=document.createElement("button");return c.type="button",c.className="btn btn-ghost btn-sm ov-token-remove",c.textContent="\xD7",n.append(s,a,l,c),n}function M(e,t,n,s){const a=document.createElement("div");return a.className=e,a.appendChild(Z(t,n,s)),a}function Z(e,t,n){const s=document.createDocumentFragment(),a=document.createElement("label");if(a.className="form-label",a.textContent=e,s.appendChild(a),s.appendChild(t),n){const l=document.createElement("p");l.className="form-hint text-muted",l.style.cssText="margin-top:.35rem;font-size:.8rem;",l.textContent=n,s.appendChild(l)}return s}function $(e,t,n){const s=document.createElement("input");return s.type="text",s.className=`form-input ${e}`,s.value=t||"",s.placeholder=n||"",s}function le(e){const t=document.createElement("div"),n=$("ov-scope",e,"#site-navbar");if(n.setAttribute("list","theme-scope-options"),t.appendChild(n),!document.getElementById("theme-scope-options")){const s=document.createElement("datalist");s.id="theme-scope-options";for(const a of ce){const l=document.createElement("option");l.value=a.value,l.label=a.label,s.appendChild(l)}t.appendChild(s)}return t}function O(){const e=g.querySelectorAll(".card"),t=[];return e.forEach((n,s)=>{const a={};n.querySelectorAll(".ov-token-row").forEach(l=>{const c=l.querySelector(".ov-token-name")?.value?.trim(),i=l.querySelector(".ov-token-value")?.value?.trim();c&&i&&(a[c]=i)}),t.push({id:b[s]?.id,label:n.querySelector(".ov-label")?.value?.trim()||"",enabled:!!n.querySelector(".ov-enabled")?.checked,scope:n.querySelector(".ov-scope")?.value?.trim()||"",theme:n.querySelector(".ov-theme")?.value||"",tokens:a,css:n.querySelector(".ov-css")?.value||""})}),b=t,t}o.on("click",".ov-remove",function(){const e=Number(this.closest(".card").dataset.i);O(),b.splice(e,1),P()}),o.on("change",".ov-theme",function(){D(this.closest(".card"))}),o.on("change","#theme-frontend, #theme-auto-enabled, #theme-day",()=>{g.querySelectorAll(".card").forEach(D)}),o.on("click",".ov-add-token",function(){this.closest(".card").querySelector(".ov-tokens").appendChild(Q("",""))}),o.on("click",".ov-token-remove",function(){this.closest(".ov-token-row").remove()}),o.find("#theme-add-override").on("click",()=>{O(),b.push(J({label:"",scope:"",enabled:!0})),P()}),P();async function j(){const e=document.getElementById("theme-css-preview"),t={...ee(),overrides:O()};try{const n=await x.theme.compile(t);e.textContent=n.css?.trim()||"(no overrides - nothing is injected)",e.style.color=""}catch(n){e.textContent=n.message||String(n),e.style.color="var(--dm-danger)"}}o.find("#theme-refresh-css").on("click",j);function ee(){const e=!d.baseTheme&&o.find("#theme-auto-enabled").prop("checked"),t={enabled:e,dayTheme:S.val()||"charcoal-light",nightTheme:L.val()||"charcoal-dark",dayStart:o.find("#theme-day-start").val()||"07:00",nightStart:o.find("#theme-night-start").val()||"19:00"};return{theme:d.baseTheme?d.theme:e?t.dayTheme:f.val(),...d.baseTheme?{baseTheme:d.baseTheme}:{},adminTheme:F.val(),autoTheme:t,font:{family:o.find("#theme-font-family").val()||"Roboto",size:parseInt(o.find("#theme-font-size").val(),10)||16},switcher:{enabled:q.prop("checked"),position:o.find("#theme-switcher-position").val()||"right-center",offsetX:parseInt(o.find("#theme-switcher-offset-x").val(),10)||0,offsetY:parseInt(o.find("#theme-switcher-offset-y").val(),10)||0,themes:Array.isArray(d.switcher?.themes)?d.switcher.themes:[]}}}function te(e){const t=document.getElementById("theme-errors");for(;t.firstChild;)t.removeChild(t.firstChild);if(!e||!e.length){t.style.display="none";return}t.style.display="";for(const n of e){const s=document.createElement("div");s.textContent=n,t.appendChild(s)}t.scrollIntoView({behavior:"smooth",block:"nearest"})}o.find("#theme-save-btn").on("click",async function(){te(null);const e={...ee(),overrides:O()};this.disabled=!0;try{d=await x.theme.save(e),Domma.theme.set(e.adminTheme),E.toast("Theme saved.",{type:"success"}),await j()}catch(t){const n=t.validation||[t.message||"Could not save the theme."];te(n),E.toast("Theme not saved.",{type:"error"})}finally{this.disabled=!1}}),await j()}};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import{apiRequest as at}from"../api.js";import{addBannerAction as st}from"../lib/plugin-chrome.js";import{helpPopovers as O}from"../lib/help-popover.js";import{onceClosed as j,openMenuAt as lt,settingsStore as nt,sidebarCount as it,viewLifetime as dt}from"../lib/tool-kit.js";import{arrangeTodos as rt,countTodos as ct,daysUntil as ut,DEFAULT_SETTINGS as H,dueDay as pt,dueLabel as bt,isoDay as k,isOverdue as mt,normaliseSettings as K,PRIORITIES as P,STATUSES as B}from"../lib/todo-arrange.js";const u={"not-started":"Not started","in-progress":"In progress",completed:"Completed",highest:"Highest",high:"High",medium:"Medium",low:"Low",lowest:"Lowest",open:"Open",all:"All",overdue:"Overdue"},U=nt("todo.settings",K),v=(b,d="GET",n)=>at(b,{method:d,...n!==void 0?{body:JSON.stringify(n)}:{}}),vt=`
|
|
2
|
+
<div class="todo-row{{doneClass}}" data-id="{{id}}">
|
|
3
|
+
<input type="checkbox" class="todo-check" aria-label="Done" data-bind-checked="isDone">
|
|
4
|
+
<span class="todo-text" title="Click to edit">{{text}}</span>
|
|
5
|
+
<span class="todo-due{{dueClass}}" title="{{dueTitle}}">{{due}}</span>
|
|
6
|
+
<button type="button" class="todo-pill" data-kind="status" data-value="{{status}}" title="Change status">{{statusLabel}}</button>
|
|
7
|
+
<button type="button" class="todo-pill" data-kind="priority" data-value="{{priority}}" title="Change priority">{{priorityLabel}}</button>
|
|
8
|
+
<button type="button" class="todo-kebab" aria-label="Task actions" title="Actions">\u22EE</button>
|
|
9
|
+
</div>`;function z(b){const d=document.createElement("template");return d.innerHTML=b.trim(),d.content.firstElementChild}const p=dt(/^#\/todo\b/);export const todoView={templateUrl:"/admin/js/templates/todo.html",async onMount(b){p.reset();const d=b.get(0),n=M.observable(U.read()),l=M.observable([]),m=M.observable(!0),g=M.observable(""),w=M.observable(n.peek().defaultFilter),f=M.observable(""),T=M.observable(n.peek().defaultPriority),x=M.observable(""),A=M.observable(!1),N=M.observable(!1),S=M.computed(()=>ct(l.value)),G=M.computed(()=>["open","all","not-started","in-progress","completed","overdue"].map(e=>({value:e,label:`${u[e]} (${S.value[e]})`}))),y=M.observable([]);p.add(M.effect(()=>{const e=n.value.relativeDue;y.value=rt(l.value,{filter:w.value,query:g.value,sort:n.value.sort}).map(t=>{const o=ut(t.dueAt),s=mt(t);return{id:t.id,text:t.text,status:t.status,priority:t.priority,statusLabel:u[t.status]??t.status,priorityLabel:u[t.priority]??t.priority,doneClass:t.status==="completed"?" is-done":"",isDone:t.status==="completed",due:bt(t.dueAt,{relative:e,format:i=>D(i).format("D MMM")}),dueClass:s?" is-overdue":o===0&&t.status!=="completed"?" is-today":"",dueTitle:o===null?"":`Due ${D(pt(t.dueAt)).format("dddd D MMMM YYYY")}`}})}));const W={rows:y,loading:m,query:g,filter:w,filterOptions:G,counts:S,draft:f,draftPriority:T,draftDue:x,addFocus:A,searchFocus:N,cannotAdd:M.computed(()=>!f.value.trim()),isEmpty:M.computed(()=>!m.value&&y.value.length===0),emptyText:M.computed(()=>l.value.length?w.value==="open"&&!g.value.trim()?"Nothing open - everything is done.":"No tasks match. Change the filter or clear the search.":"No tasks yet. Add one above."),countText:M.computed(()=>{if(m.value)return"";const e=y.value.length,t=l.value.length;return e===t?`${t} task${t===1?"":"s"}`:`${e} of ${t}`})};p.add(M.effect(()=>U.write(n.value))),p.add(M.effect(()=>{l.value,m.peek()||it("#/todo")}));const _=(e,t)=>{n.value=K({...n.peek(),[e]:t})},C=e=>l.peek().find(t=>t.id===e),J=e=>{l.value=l.peek().map(t=>t.id===e.id?e:t)};async function V(){m.value=!0;try{const e=await v("/todo/items");l.value=Array.isArray(e)?e:e.data??[]}catch{E.toast("Failed to load tasks",{type:"error"})}finally{m.value=!1}}async function c(e,t,o){try{return J(await v(`/todo/items/${e}`,"PUT",t)),o&&E.toast(o,{type:"success"}),!0}catch{return E.toast("Failed to update task",{type:"error"}),!1}}async function Q(){const e=f.peek().trim();if(e)try{const t=await v("/todo/items","POST",{text:e,priority:T.peek(),status:"not-started",dueAt:x.peek()||null});l.value=[...l.peek(),t],f.value="",x.value="",T.value=n.peek().defaultPriority,A.value=!0}catch{E.toast("Failed to add task",{type:"error"})}}const X=()=>(Q(),!1);async function q(e){if(n.peek().confirmDelete&&!await E.confirm("Delete this task?"))return!1;try{return await v(`/todo/items/${e}`,"DELETE"),l.value=l.peek().filter(t=>t.id!==e),E.toast("Task deleted",{type:"success"}),!0}catch{return E.toast("Failed to delete task",{type:"error"}),!1}}async function Z(){const e=S.peek().completed;if(!(!e||!await E.confirm(`Delete ${e} completed task${e===1?"":"s"}?`)))try{await v("/todo/completed","DELETE"),l.value=l.peek().filter(t=>t.status!=="completed"),E.toast("Completed tasks cleared",{type:"success"})}catch{E.toast("Failed to clear completed tasks",{type:"error"})}}const F=e=>{const t=C(e);if(!t)return;const o=t.status==="completed"?"not-started":"completed";c(e,{status:o},o==="completed"?"Task completed":"Task reopened")};function Y(e){const t=C(e);if(!t)return;const o=E.slideover({title:"Edit task",size:"md",position:"right"}),s=z(`
|
|
10
|
+
<div class="todo-panel">
|
|
11
|
+
<div>
|
|
12
|
+
<label class="form-label" for="todo-edit-text">Task</label>
|
|
13
|
+
<textarea id="todo-edit-text" class="form-input" data-model="text.value"></textarea>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="todo-panel-row">
|
|
16
|
+
<div>
|
|
17
|
+
<label class="form-label" for="todo-edit-status">Status</label>
|
|
18
|
+
<select id="todo-edit-status" class="form-input" data-model="status.value">
|
|
19
|
+
${B.map(a=>`<option value="${a}">${u[a]}</option>`).join("")}
|
|
20
|
+
</select>
|
|
21
|
+
</div>
|
|
22
|
+
<div>
|
|
23
|
+
<label class="form-label" for="todo-edit-priority">Priority</label>
|
|
24
|
+
<select id="todo-edit-priority" class="form-input" data-model="priority.value">
|
|
25
|
+
${P.map(a=>`<option value="${a}">${u[a]}</option>`).join("")}
|
|
26
|
+
</select>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div>
|
|
30
|
+
<label class="form-label" for="todo-edit-due">Due
|
|
31
|
+
<span data-help="A calendar day. Past due and not completed shows the task as overdue."></span>
|
|
32
|
+
</label>
|
|
33
|
+
<input id="todo-edit-due" type="date" class="form-input" data-model="due.value">
|
|
34
|
+
<div class="todo-quick-due">
|
|
35
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="dueIn" data-days="0">Today</button>
|
|
36
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="dueIn" data-days="1">Tomorrow</button>
|
|
37
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="dueIn" data-days="7">Next week</button>
|
|
38
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="clearDue">No date</button>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<small class="todo-panel-meta">Ctrl+Enter saves. Added <span data-bind-text="created"></span>.</small>
|
|
42
|
+
<div class="todo-panel-actions">
|
|
43
|
+
<button type="button" class="btn btn-sm btn-ghost text-danger" data-on-click="del">
|
|
44
|
+
<span data-icon="trash" data-icon-size="13"></span> Delete
|
|
45
|
+
</button>
|
|
46
|
+
<span class="spacer"></span>
|
|
47
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
|
|
48
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="save"
|
|
49
|
+
data-bind-disabled="cannotSave.value">Save</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>`),i={text:M.observable(t.text||""),status:M.observable(t.status),priority:M.observable(t.priority),due:M.observable(t.dueAt?String(t.dueAt).slice(0,10):""),created:t.createdAt?D(t.createdAt).format("D MMM YYYY"):"earlier"};i.cannotSave=M.computed(()=>!i.text.value.trim());const h=async()=>{await c(t.id,{text:i.text.peek().trim(),status:i.status.peek(),priority:i.priority.peek(),dueAt:i.due.peek()||null},"Task saved")&&o.close()};o.element.appendChild(s);const $=O(s),r=M.applyBindings(i,s,{methods:{save:h,cancel:()=>o.close(),del:async()=>{await q(t.id)&&o.close()},dueIn:a=>{i.due.value=k(Number(a.currentTarget.dataset.days))},clearDue:()=>{i.due.value=""}}});s.addEventListener("keydown",a=>{(a.ctrlKey||a.metaKey)&&a.key==="Enter"&&(a.preventDefault(),h())}),j(o,()=>{r.dispose(),$()}),o.open(),setTimeout(()=>s.querySelector("#todo-edit-text")?.focus(),60)}function tt(){const e=E.slideover({title:"Todo settings",size:"sm",position:"right"}),t=r=>r.map(a=>`<option value="${a}">${u[a]}</option>`).join(""),o=z(`
|
|
52
|
+
<div class="todo-panel">
|
|
53
|
+
<div>
|
|
54
|
+
<label class="form-label" for="todo-set-filter">Show on opening
|
|
55
|
+
<span data-help="Which tasks the list shows when you open Todo. The filter in the toolbar changes it for this visit only."></span>
|
|
56
|
+
</label>
|
|
57
|
+
<select id="todo-set-filter" class="form-input" data-model="defaultFilter.value">
|
|
58
|
+
${t(["open","all","not-started","in-progress","completed","overdue"])}
|
|
59
|
+
</select>
|
|
60
|
+
</div>
|
|
61
|
+
<div>
|
|
62
|
+
<label class="form-label" for="todo-set-sort">Sort by
|
|
63
|
+
<span data-help="Completed tasks always sit at the bottom, whatever the sort."></span>
|
|
64
|
+
</label>
|
|
65
|
+
<select id="todo-set-sort" class="form-input" data-model="sort.value">
|
|
66
|
+
<option value="created">Order added</option>
|
|
67
|
+
<option value="priority">Priority, highest first</option>
|
|
68
|
+
<option value="due">Due date, soonest first</option>
|
|
69
|
+
<option value="status">Status</option>
|
|
70
|
+
</select>
|
|
71
|
+
</div>
|
|
72
|
+
<div>
|
|
73
|
+
<label class="form-label" for="todo-set-priority">Priority for new tasks</label>
|
|
74
|
+
<select id="todo-set-priority" class="form-input" data-model="defaultPriority.value">
|
|
75
|
+
${t(P)}
|
|
76
|
+
</select>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="todo-setting-row">
|
|
79
|
+
<label class="form-label" for="todo-set-relative" style="margin:0">Relative due dates
|
|
80
|
+
<span data-help="On: "Tomorrow", "In 3 days". Off: the date itself."></span>
|
|
81
|
+
</label>
|
|
82
|
+
<input id="todo-set-relative" type="checkbox" class="form-switch" data-model="relativeDue.value">
|
|
83
|
+
</div>
|
|
84
|
+
<div class="todo-setting-row">
|
|
85
|
+
<label class="form-label" for="todo-set-confirm" style="margin:0">Confirm before deleting
|
|
86
|
+
<span data-help="Off deletes straight away - there is no undo."></span>
|
|
87
|
+
</label>
|
|
88
|
+
<input id="todo-set-confirm" type="checkbox" class="form-switch" data-model="confirmDelete.value">
|
|
89
|
+
</div>
|
|
90
|
+
<small class="todo-panel-meta">Saved in this browser as you change them.</small>
|
|
91
|
+
<div class="todo-panel-actions">
|
|
92
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="reset">Reset to defaults</button>
|
|
93
|
+
<span class="spacer"></span>
|
|
94
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="done">Done</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>`),s=r=>M.computed({read:()=>n.value[r],write:a=>_(r,a)}),i=Object.fromEntries(Object.keys(H).map(r=>[r,s(r)]));e.element.appendChild(o);const h=O(o),$=M.applyBindings(i,o,{methods:{done:()=>e.close(),reset:()=>{n.value={...H}}}});j(e,()=>{$.dispose(),h()}),e.open()}st(b,{id:"todo-settings",icon:"settings",title:"Todo settings",onClick:tt}),d.querySelector(".todo-rows").innerHTML=vt,p.add(O(d,{position:"bottom"}));const et=M.applyBindings(W,d,{methods:{add:X,clearCompleted:Z}});p.add(()=>et.dispose());const L=d.querySelector(".todo-list"),ot=E.contextMenu(L,{match:".todo-row",inherit:!1,density:"compact",items:e=>{const t=e.dataset.id,o=C(t);return o?[{label:"Edit\u2026",icon:"edit",action:()=>Y(t)},{label:o.status==="completed"?"Reopen":"Complete",icon:"check",action:()=>F(t)},{type:"divider"},{label:"Status",icon:"circle",submenu:B.map(s=>({label:u[s],disabled:o.status===s,action:()=>c(t,{status:s})}))},{label:"Priority",icon:"flag",submenu:P.map(s=>({label:u[s],disabled:o.priority===s,action:()=>c(t,{priority:s})}))},{label:"Due",icon:"calendar",submenu:[{label:"Today",action:()=>c(t,{dueAt:k(0)})},{label:"Tomorrow",action:()=>c(t,{dueAt:k(1)})},{label:"Next week",action:()=>c(t,{dueAt:k(7)})},{type:"divider"},{label:"No due date",disabled:!o.dueAt,action:()=>c(t,{dueAt:null})}]},{type:"divider"},{label:"Delete",icon:"trash",danger:!0,action:()=>q(t)}]:[]}});p.add(()=>ot?.destroy?.()),L.addEventListener("click",e=>{const t=e.target.closest(".todo-row");if(t){if(e.target.closest(".todo-kebab, .todo-pill"))return lt(e.target.closest(".todo-kebab, .todo-pill"));e.target.closest(".todo-text")&&Y(t.dataset.id)}}),L.addEventListener("change",e=>{e.target.matches(".todo-check")&&F(e.target.closest(".todo-row").dataset.id)});const R=e=>{e.ctrlKey||e.metaKey||e.altKey||e.target.closest?.("input, textarea, select, [contenteditable], .dm-slideover")||(e.key==="/"?(e.preventDefault(),N.value=!0):(e.key==="n"||e.key==="N")&&(e.preventDefault(),A.value=!0))};document.addEventListener("keydown",R),p.add(()=>document.removeEventListener("keydown",R)),I.scan(d),await V()}};
|
package/bin/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import {spawnSync} from 'node:child_process';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import {fileURLToPath} from 'node:url';
|
|
11
11
|
import {composeSmtp} from './lib/smtp-defaults.js';
|
|
12
|
+
import {buildGitignore} from '../server/services/siteGitignore.js';
|
|
12
13
|
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Paths
|
|
@@ -324,16 +325,9 @@ done();
|
|
|
324
325
|
// Write .gitignore
|
|
325
326
|
// ---------------------------------------------------------------------------
|
|
326
327
|
|
|
327
|
-
|
|
328
|
-
.
|
|
329
|
-
|
|
330
|
-
content/media/
|
|
331
|
-
*.log
|
|
332
|
-
.domma-backups/
|
|
333
|
-
|
|
334
|
-
# Analytics plugin runtime counters (written on every page hit). Never track.
|
|
335
|
-
plugins/analytics/data/
|
|
336
|
-
`;
|
|
328
|
+
// The content-only allowlist: this repo tracks the site's content, never the
|
|
329
|
+
// engine or distributed plugins. Same rules the manager's scaffolder writes.
|
|
330
|
+
const GITIGNORE = buildGitignore();
|
|
337
331
|
|
|
338
332
|
step('Writing .gitignore');
|
|
339
333
|
writeFileSync(path.join(target, '.gitignore'), GITIGNORE, 'utf8');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin versions, for the engine updater.
|
|
3
|
+
*
|
|
4
|
+
* The updater replaces each bundled plugin (Blog, Mail Reader) on a site with
|
|
5
|
+
* the engine's copy. A site can hold a NEWER copy than the engine - installed
|
|
6
|
+
* from the marketplace catalogue, where the plugin is written - and replacing
|
|
7
|
+
* that would quietly take the site back a version. So the updater asks first.
|
|
8
|
+
*
|
|
9
|
+
* @module bin/lib/plugin-version
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** -1, 0 or 1. Numeric per dot-separated part; a missing part is 0; junk sorts lowest. */
|
|
13
|
+
export function compareVersions(a, b) {
|
|
14
|
+
const parse = (v) => String(v ?? '').split(/[.+-]/).slice(0, 3).map(n => (/^\d+$/.test(n) ? Number(n) : -1));
|
|
15
|
+
const x = parse(a);
|
|
16
|
+
const y = parse(b);
|
|
17
|
+
for (let i = 0; i < 3; i++) {
|
|
18
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
19
|
+
if (d) return d > 0 ? 1 : -1;
|
|
20
|
+
}
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Should the engine's copy replace the site's? Not when the site's is newer. */
|
|
25
|
+
export function shouldReplacePlugin(siteVersion, engineVersion) {
|
|
26
|
+
if (!siteVersion) return true;
|
|
27
|
+
return compareVersions(siteVersion, engineVersion) <= 0;
|
|
28
|
+
}
|
package/bin/update.js
CHANGED
|
@@ -14,6 +14,7 @@ import path from 'node:path';
|
|
|
14
14
|
import {createInterface} from 'node:readline';
|
|
15
15
|
import {fileURLToPath} from 'node:url';
|
|
16
16
|
import {deepMergeNewKeys} from './lib/config-merge.js';
|
|
17
|
+
import {shouldReplacePlugin} from './lib/plugin-version.js';
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Paths
|
|
@@ -379,6 +380,19 @@ export default async function update(_positional, flags) {
|
|
|
379
380
|
}
|
|
380
381
|
newPlugins.push(name);
|
|
381
382
|
} else {
|
|
383
|
+
// A site can hold a NEWER copy than the engine - installed from
|
|
384
|
+
// the marketplace catalogue, where the plugin is written.
|
|
385
|
+
// Replacing it would take the site back a version, so keep it.
|
|
386
|
+
const versionOf = (dir) => {
|
|
387
|
+
try { return readJson(path.join(dir, 'plugin.json')).version ?? null; } catch { return null; }
|
|
388
|
+
};
|
|
389
|
+
const siteVersion = versionOf(dstPlugin);
|
|
390
|
+
const engineVersion = versionOf(srcPlugin);
|
|
391
|
+
if (!shouldReplacePlugin(siteVersion, engineVersion)) {
|
|
392
|
+
info(` → plugins/${name}/ kept: ${siteVersion} here is newer than the engine's ${engineVersion}`);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
382
396
|
// Existing plugin - identify paths to preserve
|
|
383
397
|
let preservePaths = new Set(['data']);
|
|
384
398
|
try {
|
package/config/server.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "domma-cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.69.3",
|
|
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",
|
|
@@ -19,15 +19,11 @@
|
|
|
19
19
|
"public/css/",
|
|
20
20
|
"config/",
|
|
21
21
|
"!config/connections.json",
|
|
22
|
+
"!config/theme.json",
|
|
22
23
|
"plugins/",
|
|
23
|
-
"!plugins/analytics/data",
|
|
24
24
|
"!plugins/contacts/data",
|
|
25
25
|
"!plugins/notes/data",
|
|
26
26
|
"!plugins/todo/data",
|
|
27
|
-
"!plugins/analytics/stats.json",
|
|
28
|
-
"!plugins/analytics/daily.json",
|
|
29
|
-
"!plugins/analytics/lifetime.json",
|
|
30
|
-
"!plugins/analytics/journeys.json",
|
|
31
27
|
"!plugins/mail-reader/data",
|
|
32
28
|
"!plugins/_lib/data",
|
|
33
29
|
"scripts/",
|
|
@@ -37,7 +33,7 @@
|
|
|
37
33
|
"FEATURES.md"
|
|
38
34
|
],
|
|
39
35
|
"scripts": {
|
|
40
|
-
"test": "node
|
|
36
|
+
"test": "node scripts/run-tests.mjs",
|
|
41
37
|
"build": "node scripts/build.js",
|
|
42
38
|
"create-plugin": "node scripts/create-plugin.js",
|
|
43
39
|
"delete-users": "node -e \"import('fs').then(({readdirSync,rmSync})=>{const d='content/users';readdirSync(d).filter(f=>f.endsWith('.json')).forEach(f=>{rmSync(d+'/'+f);console.log('deleted',f)})})\"",
|
|
@@ -4121,6 +4121,11 @@ return {
|
|
|
4121
4121
|
if (!folder || !Number.isFinite(folder.unseen)) return;
|
|
4122
4122
|
folder.unseen = Math.max(0, folder.unseen + delta);
|
|
4123
4123
|
renderFolders();
|
|
4124
|
+
// The sidebar's unread badge counts the mirror; ask it to look
|
|
4125
|
+
// again. Its url is this screen's plugin (#/plugins/<slug>) - the
|
|
4126
|
+
// reader is shared by more than one mail plugin.
|
|
4127
|
+
const pluginUrl = location.hash.split('/').slice(0, 3).join('/');
|
|
4128
|
+
window.dispatchEvent(new CustomEvent('dm:sidebar-count', {detail: {url: pluginUrl}}));
|
|
4124
4129
|
}
|
|
4125
4130
|
|
|
4126
4131
|
/**
|
|
@@ -4346,6 +4351,21 @@ return {
|
|
|
4346
4351
|
if (features.folders) attachFolderMenu();
|
|
4347
4352
|
|
|
4348
4353
|
$container.find('#mail-settings-btn').on('click', () => openSettings());
|
|
4354
|
+
|
|
4355
|
+
// Settings is a screen-level control, so it belongs in the admin's
|
|
4356
|
+
// plugin banner beside the licence - the same place every other plugin
|
|
4357
|
+
// puts its cog. Imported at RUN time and survivable: an admin that
|
|
4358
|
+
// predates addBannerAction keeps the button in the Folders pane.
|
|
4359
|
+
try {
|
|
4360
|
+
const chrome = await import('/admin/js/lib/plugin-chrome.js');
|
|
4361
|
+
const placed = chrome.addBannerAction?.($container, {
|
|
4362
|
+
id: `${pluginName}-settings`,
|
|
4363
|
+
icon: 'settings',
|
|
4364
|
+
title: 'Mail account settings',
|
|
4365
|
+
onClick: () => openSettings()
|
|
4366
|
+
});
|
|
4367
|
+
if (placed) $container.find('#mail-settings-btn').get(0)?.remove();
|
|
4368
|
+
} catch { /* no banner: the pane button stays */ }
|
|
4349
4369
|
$container.find('#mail-refresh-btn').on('click', () => loadMessages());
|
|
4350
4370
|
|
|
4351
4371
|
// Folder management was right-click only, which is the same as not
|
|
@@ -770,6 +770,26 @@ export function createStore(connection) {
|
|
|
770
770
|
return database.collection(MESSAGES).countDocuments(filter);
|
|
771
771
|
}
|
|
772
772
|
|
|
773
|
+
/**
|
|
774
|
+
* The newest unread messages in a mailbox - who from, what about, when -
|
|
775
|
+
* for the sidebar badge's popover. Headers only; never a body.
|
|
776
|
+
*
|
|
777
|
+
* @param {string} accountId
|
|
778
|
+
* @param {string[]|null} [folders] - which to look in, or null for all mirrored
|
|
779
|
+
* @param {number} [limit]
|
|
780
|
+
* @returns {Promise<{from: object[], subject: string, date: string|null, folder: string}[]>}
|
|
781
|
+
*/
|
|
782
|
+
async function listUnread(accountId, folders = null, limit = 5) {
|
|
783
|
+
const database = await connect();
|
|
784
|
+
const filter = {accountId, seen: false};
|
|
785
|
+
if (Array.isArray(folders) && folders.length) filter.folder = {$in: folders};
|
|
786
|
+
return database.collection(MESSAGES)
|
|
787
|
+
.find(filter, {projection: {_id: 0, from: 1, subject: 1, date: 1, folder: 1}})
|
|
788
|
+
.sort({date: -1, uid: -1})
|
|
789
|
+
.limit(limit)
|
|
790
|
+
.toArray();
|
|
791
|
+
}
|
|
792
|
+
|
|
773
793
|
/** Folders already backfilled in this process, so it runs at most once each. */
|
|
774
794
|
const backfilled = new Set();
|
|
775
795
|
|
|
@@ -993,7 +1013,7 @@ export function createStore(connection) {
|
|
|
993
1013
|
connect,
|
|
994
1014
|
getFolder, saveFolder,
|
|
995
1015
|
putMessages, setFlags, removeMessages, clearFolder,
|
|
996
|
-
storedUids, listMessages, listThreads, searchMessages, countUnread, forgetAccount,
|
|
1016
|
+
storedUids, listMessages, listThreads, searchMessages, countUnread, listUnread, forgetAccount,
|
|
997
1017
|
queueSend, listQueuedSends, claimDueSends, finishSend, cancelSend, getQueuedSend,
|
|
998
1018
|
queueSnooze, listSnoozed, claimDueSnoozes, finishSnooze, wakeSnooze,
|
|
999
1019
|
lastAutoReply, noteAutoReply, listAutoReplies, clearAutoReplies,
|