domma-cms 0.30.0 → 0.32.1

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.
Files changed (43) hide show
  1. package/admin/dist/domma/domma-tools.css +3 -3
  2. package/admin/dist/domma/domma-tools.min.js +4 -4
  3. package/admin/js/lib/themes.js +1 -1
  4. package/admin/js/templates/effects.html +752 -752
  5. package/admin/js/templates/forms.html +17 -17
  6. package/admin/js/templates/my-profile.html +17 -17
  7. package/admin/js/templates/role-editor.html +70 -70
  8. package/admin/js/templates/roles.html +10 -10
  9. package/admin/js/views/settings.js +1 -1
  10. package/bin/lib/config-merge.js +44 -44
  11. package/config/site.json +1 -0
  12. package/package.json +2 -2
  13. package/plugins/surveys/admin/templates/audience.html +47 -0
  14. package/plugins/surveys/admin/templates/results.html +58 -0
  15. package/plugins/surveys/admin/templates/survey-editor.html +141 -0
  16. package/plugins/surveys/admin/templates/surveys.html +25 -0
  17. package/plugins/surveys/admin/views/audience.js +301 -0
  18. package/plugins/surveys/admin/views/results.js +172 -0
  19. package/plugins/surveys/admin/views/survey-editor.js +211 -0
  20. package/plugins/surveys/admin/views/surveys.js +161 -0
  21. package/plugins/surveys/collections/survey-contacts/schema.json +13 -0
  22. package/plugins/surveys/collections/survey-groups/schema.json +11 -0
  23. package/plugins/surveys/collections/survey-invites/schema.json +16 -0
  24. package/plugins/surveys/collections/surveys/schema.json +23 -0
  25. package/plugins/surveys/config.js +8 -0
  26. package/plugins/surveys/plugin.js +169 -0
  27. package/plugins/surveys/plugin.json +65 -0
  28. package/plugins/surveys/public/dist-shared.mjs +1 -0
  29. package/plugins/surveys/public/survey.css +1 -0
  30. package/plugins/surveys/public/survey.mjs +1 -0
  31. package/plugins/surveys/templates/survey-page.html +63 -0
  32. package/server/middleware/auth.js +253 -253
  33. package/server/routes/api/auth.js +309 -309
  34. package/server/routes/api/navigation.js +42 -42
  35. package/server/routes/api/settings.js +141 -141
  36. package/server/routes/docs-public.js +43 -0
  37. package/server/routes/public.js +2 -2
  38. package/server/server.js +13 -1
  39. package/{scripts/migrate-docs.js → server/services/docs.js} +204 -126
  40. package/server/services/email.js +167 -167
  41. package/server/services/userProfiles.js +199 -199
  42. package/server/services/users.js +302 -302
  43. package/config/connections.json.bak +0 -9
@@ -1,17 +1,17 @@
1
- <div class="view-header">
2
- <h1><span data-icon="layout"></span> Forms</h1>
3
- <div style="display:flex;gap:.5rem;">
4
- <button id="test-email-btn" class="btn btn-ghost btn-sm">
5
- <span data-icon="mail"></span> Test Email
6
- </button>
7
- <button id="create-form-btn" class="btn btn-primary">
8
- <span data-icon="plus"></span> Create Form
9
- </button>
10
- </div>
11
- </div>
12
-
13
- <div class="card">
14
- <div class="card-body">
15
- <div id="forms-table"></div>
16
- </div>
17
- </div>
1
+ <div class="view-header">
2
+ <h1><span data-icon="layout"></span> Forms</h1>
3
+ <div style="display:flex;gap:.5rem;">
4
+ <button id="test-email-btn" class="btn btn-ghost btn-sm">
5
+ <span data-icon="mail"></span> Test Email
6
+ </button>
7
+ <button id="create-form-btn" class="btn btn-primary">
8
+ <span data-icon="plus"></span> Create Form
9
+ </button>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="card">
14
+ <div class="card-body">
15
+ <div id="forms-table"></div>
16
+ </div>
17
+ </div>
@@ -1,17 +1,17 @@
1
- <div class="view-header">
2
- <h1><span data-icon="user"></span> My Profile</h1>
3
- </div>
4
-
5
- <div class="card">
6
- <div class="card-header"><h2>Account</h2></div>
7
- <div class="card-body">
8
- <div id="account-form-container"></div>
9
- </div>
10
- </div>
11
-
12
- <div class="card mt-4" id="profile-card" style="display:none;">
13
- <div class="card-header"><h2>Profile</h2></div>
14
- <div class="card-body">
15
- <div id="profile-form-container"></div>
16
- </div>
17
- </div>
1
+ <div class="view-header">
2
+ <h1><span data-icon="user"></span> My Profile</h1>
3
+ </div>
4
+
5
+ <div class="card">
6
+ <div class="card-header"><h2>Account</h2></div>
7
+ <div class="card-body">
8
+ <div id="account-form-container"></div>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="card mt-4" id="profile-card" style="display:none;">
13
+ <div class="card-header"><h2>Profile</h2></div>
14
+ <div class="card-body">
15
+ <div id="profile-form-container"></div>
16
+ </div>
17
+ </div>
@@ -1,70 +1,70 @@
1
- <div class="view-header">
2
- <h1><span data-icon="shield"></span> <span id="role-title">Edit Role</span></h1>
3
- <div>
4
- <a href="#/roles" class="btn btn-secondary"><span data-icon="arrow-left"></span> Back</a>
5
- <button id="btn-save-role" class="btn btn-primary"><span data-icon="save"></span> Save</button>
6
- </div>
7
- </div>
8
-
9
- <div class="tabs" id="role-tabs">
10
- <div class="tab-list">
11
- <button class="tab-item active">General</button>
12
- <button class="tab-item">Permissions</button>
13
- </div>
14
- <div class="tab-content">
15
- <!-- General Tab -->
16
- <div class="tab-panel active" id="panel-general">
17
- <div class="card">
18
- <div class="card-body">
19
- <div class="row">
20
- <div class="col-6">
21
- <div class="mb-3">
22
- <label class="form-label">Name (slug)</label>
23
- <input id="role-name" type="text" class="form-input" placeholder="e.g. moderator">
24
- <p class="form-hint">Lowercase identifier. Cannot be changed for the root admin
25
- role.</p>
26
- </div>
27
- </div>
28
- <div class="col-6">
29
- <div class="mb-3">
30
- <label class="form-label">Display Label</label>
31
- <input id="role-label" type="text" class="form-input" placeholder="e.g. Moderator">
32
- </div>
33
- </div>
34
- </div>
35
- <div class="row">
36
- <div class="col-6">
37
- <div class="mb-3">
38
- <label class="form-label">Level</label>
39
- <input id="role-level" type="number" class="form-input" min="0">
40
- <p class="form-hint">Lower = more privileged. 0 is reserved for the root admin.</p>
41
- </div>
42
- </div>
43
- <div class="col-6">
44
- <div class="mb-3">
45
- <label class="form-label">Badge Colour</label>
46
- <select id="role-badge" class="form-input"></select>
47
- </div>
48
- </div>
49
- </div>
50
- <div class="row">
51
- <div class="col-6">
52
- <div class="mb-3">
53
- <label class="form-label">Project</label>
54
- <select id="role-project" class="form-input">
55
- <option value="">— none —</option>
56
- </select>
57
- <p class="form-hint">Tag this role to a project for grouping.</p>
58
- </div>
59
- </div>
60
- </div>
61
- </div>
62
- </div>
63
- </div>
64
-
65
- <!-- Permissions Tab -->
66
- <div class="tab-panel" id="panel-permissions">
67
- <div id="permissions-container"></div>
68
- </div>
69
- </div>
70
- </div>
1
+ <div class="view-header">
2
+ <h1><span data-icon="shield"></span> <span id="role-title">Edit Role</span></h1>
3
+ <div>
4
+ <a href="#/roles" class="btn btn-secondary"><span data-icon="arrow-left"></span> Back</a>
5
+ <button id="btn-save-role" class="btn btn-primary"><span data-icon="save"></span> Save</button>
6
+ </div>
7
+ </div>
8
+
9
+ <div class="tabs" id="role-tabs">
10
+ <div class="tab-list">
11
+ <button class="tab-item active">General</button>
12
+ <button class="tab-item">Permissions</button>
13
+ </div>
14
+ <div class="tab-content">
15
+ <!-- General Tab -->
16
+ <div class="tab-panel active" id="panel-general">
17
+ <div class="card">
18
+ <div class="card-body">
19
+ <div class="row">
20
+ <div class="col-6">
21
+ <div class="mb-3">
22
+ <label class="form-label">Name (slug)</label>
23
+ <input id="role-name" type="text" class="form-input" placeholder="e.g. moderator">
24
+ <p class="form-hint">Lowercase identifier. Cannot be changed for the root admin
25
+ role.</p>
26
+ </div>
27
+ </div>
28
+ <div class="col-6">
29
+ <div class="mb-3">
30
+ <label class="form-label">Display Label</label>
31
+ <input id="role-label" type="text" class="form-input" placeholder="e.g. Moderator">
32
+ </div>
33
+ </div>
34
+ </div>
35
+ <div class="row">
36
+ <div class="col-6">
37
+ <div class="mb-3">
38
+ <label class="form-label">Level</label>
39
+ <input id="role-level" type="number" class="form-input" min="0">
40
+ <p class="form-hint">Lower = more privileged. 0 is reserved for the root admin.</p>
41
+ </div>
42
+ </div>
43
+ <div class="col-6">
44
+ <div class="mb-3">
45
+ <label class="form-label">Badge Colour</label>
46
+ <select id="role-badge" class="form-input"></select>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ <div class="row">
51
+ <div class="col-6">
52
+ <div class="mb-3">
53
+ <label class="form-label">Project</label>
54
+ <select id="role-project" class="form-input">
55
+ <option value="">— none —</option>
56
+ </select>
57
+ <p class="form-hint">Tag this role to a project for grouping.</p>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+ <!-- Permissions Tab -->
66
+ <div class="tab-panel" id="panel-permissions">
67
+ <div id="permissions-container"></div>
68
+ </div>
69
+ </div>
70
+ </div>
@@ -1,10 +1,10 @@
1
- <div class="view-header">
2
- <h1><span data-icon="shield"></span> Roles &amp; Permissions</h1>
3
- <button id="btn-add-role" class="btn btn-primary"><span data-icon="plus"></span> Add Role</button>
4
- </div>
5
-
6
- <div class="card">
7
- <div class="card-body">
8
- <div id="roles-table"></div>
9
- </div>
10
- </div>
1
+ <div class="view-header">
2
+ <h1><span data-icon="shield"></span> Roles &amp; Permissions</h1>
3
+ <button id="btn-add-role" class="btn btn-primary"><span data-icon="plus"></span> Add Role</button>
4
+ </div>
5
+
6
+ <div class="card">
7
+ <div class="card-body">
8
+ <div id="roles-table"></div>
9
+ </div>
10
+ </div>
@@ -1,3 +1,3 @@
1
- import{api as C,apiRequest as v}from"../api.js";import{populateThemeSelect as w}from"../lib/themes.js";export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(e){E.tabs(e.find("#settings-tabs").get(0));const k=E.loader(e.get(0),{type:"dots"}),l=await C.settings.get().catch(()=>({}));if(k.destroy(),e.find("#field-site-title").val(l.title||""),e.find("#field-tagline").val(l.tagline||""),e.find("#field-font-family").val(l.fontFamily||"Roboto"),e.find("#field-font-size").val(l.fontSize||16),w(e.find("#field-theme").get(0)),w(e.find("#field-admin-theme").get(0)),e.find("#field-theme").val(l.baseTheme||l.theme||"charcoal-dark"),l.baseTheme){e.find("#field-theme").prop("disabled",!0);const t=document.createElement("p");t.className="form-hint",t.style.cssText="margin-top:.4rem;font-size:.8rem;color:var(--dm-info);";const s=document.createElement("a");s.href="#/plugins/theme-roller",s.textContent="Theme Roller",t.appendChild(document.createTextNode(`Custom theme \u201C${l.theme}\u201D is active (based on ${l.baseTheme}). Manage via `)),t.appendChild(s),t.appendChild(document.createTextNode(".")),e.find("#field-theme").get(0).closest(".col-6").appendChild(t)}e.find("#field-admin-theme").val(l.adminTheme||"charcoal-dark");const p=l.autoTheme||{},h=!!p.enabled,b=e.find("#field-auto-theme-enabled"),y=e.find("#auto-theme-fields"),u=e.find("#field-theme"),f=u.get(0).innerHTML;e.find("#field-day-theme").html(f),e.find("#field-night-theme").html(f),l.baseTheme?(b.prop("disabled",!0),e.find("#auto-theme-roller-hint").show()):(b.prop("checked",h),h&&(y.show(),u.prop("disabled",!0)),b.on("change",function(){const t=this.checked;y.toggle(t),u.prop("disabled",t)})),e.find("#field-day-theme").val(p.dayTheme||"charcoal-light"),e.find("#field-night-theme").val(p.nightTheme||"charcoal-dark"),e.find("#field-day-start").val(p.dayStart||"07:00"),e.find("#field-night-start").val(p.nightStart||"19:00"),e.find("#field-spacer-size").val(l.layoutOptions?.spacerSize??40),e.find("#field-seo-title").val(l.seo?.defaultTitle||""),e.find("#field-seo-separator").val(l.seo?.titleSeparator||" | "),e.find("#field-seo-desc").val(l.seo?.defaultDescription||""),e.find("#field-footer-copy").val(l.footer?.copyright||""),e.find("#field-social-twitter").val(l.social?.twitter||""),e.find("#field-social-facebook").val(l.social?.facebook||""),e.find("#field-social-instagram").val(l.social?.instagram||""),e.find("#field-social-linkedin").val(l.social?.linkedin||""),e.find("#field-social-github").val(l.social?.github||""),e.find("#field-social-youtube").val(l.social?.youtube||""),e.find("#field-smtp-host").val(l.smtp?.host||""),e.find("#field-smtp-port").val(l.smtp?.port||587),e.find("#field-smtp-user").val(l.smtp?.user||""),e.find("#field-smtp-pass").val(l.smtp?.pass||""),e.find("#field-smtp-secure").prop("checked",l.smtp?.secure||!1),e.find("#field-smtp-from-address").val(l.smtp?.fromAddress||""),e.find("#field-smtp-from-name").val(l.smtp?.fromName||"");const i=l.backToTop||{};e.find("#field-btt-enabled").prop("checked",i.enabled!==!1),e.find("#field-btt-threshold").val(i.scrollThreshold??300),e.find("#field-btt-position").val(i.position||"bottom-right"),e.find("#field-btt-offset").val(i.offset??32),e.find("#field-btt-bottom-offset").val(i.bottomOffset??i.offset??32),e.find("#field-btt-label").val(i.label||""),e.find("#field-btt-smooth").prop("checked",i.smooth!==!1);const d=l.cookieConsent||{};e.find("#field-cc-enabled").prop("checked",d.enabled!==!1),e.find("#field-cc-message").val(d.message||""),e.find("#field-cc-accept-all").val(d.acceptAllText||"Accept All"),e.find("#field-cc-reject-all").val(d.rejectAllText||"Reject All"),e.find("#field-cc-customize").val(d.customizeText||"Customize"),e.find("#field-cc-save-prefs").val(d.savePreferencesText||"Save Preferences"),e.find("#field-cc-privacy-text").val(d.privacyPolicyText||"Privacy Policy"),e.find("#field-cc-privacy-url").val(d.privacyPolicyUrl||""),e.find("#field-cc-cookie-text").val(d.cookiePolicyText||"Cookie Policy"),e.find("#field-cc-cookie-url").val(d.cookiePolicyUrl||""),e.find("#field-cc-position").val(d.position||"bottom"),e.find("#field-cc-layout").val(d.layout||"bar"),e.find("#field-cc-theme").val(d.theme||"dark"),e.find("#field-cc-show-functional").prop("checked",d.showFunctional!==!1),e.find("#field-cc-show-analytics").prop("checked",d.showAnalytics!==!1),e.find("#field-cc-show-marketing").prop("checked",d.showMarketing!==!1),e.find("#field-cc-version").val(d.consentVersion||"1.0");const m=l.breadcrumbs||{};e.find("#field-breadcrumbs-enabled").prop("checked",m.enabled===!0),e.find("#field-breadcrumbs-home-label").val(m.homeLabel||"Home"),e.find("#field-breadcrumbs-position").val(m.position||"TL"),e.find("#field-bc-offset-x").val(m.offsetX??8),e.find("#field-bc-offset-y").val(m.offsetY??8);function c(t){e.find(".bc-pos-btn").each(function(){const s=this.dataset.pos===t;this.style.background=s?"var(--primary, #5b8cff)":"",this.style.color=s?"#fff":""})}c(m.position||"TL"),e.find(".bc-pos-btn").on("click",function(){const t=this.dataset.pos;e.find("#field-breadcrumbs-position").val(t),c(t)});let a=null;try{const{css:t}=await v("/settings/custom-css");e.find("#field-custom-css").val(t||""),E.editor&&(a=E.editor(e.find("#field-custom-css").get(0),{mode:"code",language:"css",lineNumbers:!0,showToolbar:!1,minHeight:420,placeholder:"/* Add your custom CSS here */",characterCount:!0}))}catch{}a&&a._editorEl&&a._editorEl.addEventListener("keydown",t=>{if(!t.ctrlKey&&!t.metaKey)return;if(t.key==="s"){t.preventDefault(),e.find("#save-css-btn").get(0)?.click();return}const s=a._editorEl,n=s.selectionStart!==s.selectionEnd;if((t.key==="c"||t.key==="x")&&!n){t.preventDefault();const o=s.value,g=s.selectionStart,T=o.lastIndexOf(`
1
+ import{api as C,apiRequest as v}from"../api.js";import{populateThemeSelect as w}from"../lib/themes.js";export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(e){E.tabs(e.find("#settings-tabs").get(0));const k=E.loader(e.get(0),{type:"dots"}),l=await C.settings.get().catch(()=>({}));if(k.destroy(),e.find("#field-site-title").val(l.title||""),e.find("#field-tagline").val(l.tagline||""),e.find("#field-font-family").val(l.fontFamily||"Roboto"),e.find("#field-font-size").val(l.fontSize||16),w(e.find("#field-theme").get(0)),w(e.find("#field-admin-theme").get(0),{includeAdmin:!0}),e.find("#field-theme").val(l.baseTheme||l.theme||"charcoal-dark"),l.baseTheme){e.find("#field-theme").prop("disabled",!0);const t=document.createElement("p");t.className="form-hint",t.style.cssText="margin-top:.4rem;font-size:.8rem;color:var(--dm-info);";const s=document.createElement("a");s.href="#/plugins/theme-roller",s.textContent="Theme Roller",t.appendChild(document.createTextNode(`Custom theme \u201C${l.theme}\u201D is active (based on ${l.baseTheme}). Manage via `)),t.appendChild(s),t.appendChild(document.createTextNode(".")),e.find("#field-theme").get(0).closest(".col-6").appendChild(t)}e.find("#field-admin-theme").val(l.adminTheme||"charcoal-dark");const p=l.autoTheme||{},h=!!p.enabled,b=e.find("#field-auto-theme-enabled"),y=e.find("#auto-theme-fields"),u=e.find("#field-theme"),f=u.get(0).innerHTML;e.find("#field-day-theme").html(f),e.find("#field-night-theme").html(f),l.baseTheme?(b.prop("disabled",!0),e.find("#auto-theme-roller-hint").show()):(b.prop("checked",h),h&&(y.show(),u.prop("disabled",!0)),b.on("change",function(){const t=this.checked;y.toggle(t),u.prop("disabled",t)})),e.find("#field-day-theme").val(p.dayTheme||"charcoal-light"),e.find("#field-night-theme").val(p.nightTheme||"charcoal-dark"),e.find("#field-day-start").val(p.dayStart||"07:00"),e.find("#field-night-start").val(p.nightStart||"19:00"),e.find("#field-spacer-size").val(l.layoutOptions?.spacerSize??40),e.find("#field-seo-title").val(l.seo?.defaultTitle||""),e.find("#field-seo-separator").val(l.seo?.titleSeparator||" | "),e.find("#field-seo-desc").val(l.seo?.defaultDescription||""),e.find("#field-footer-copy").val(l.footer?.copyright||""),e.find("#field-social-twitter").val(l.social?.twitter||""),e.find("#field-social-facebook").val(l.social?.facebook||""),e.find("#field-social-instagram").val(l.social?.instagram||""),e.find("#field-social-linkedin").val(l.social?.linkedin||""),e.find("#field-social-github").val(l.social?.github||""),e.find("#field-social-youtube").val(l.social?.youtube||""),e.find("#field-smtp-host").val(l.smtp?.host||""),e.find("#field-smtp-port").val(l.smtp?.port||587),e.find("#field-smtp-user").val(l.smtp?.user||""),e.find("#field-smtp-pass").val(l.smtp?.pass||""),e.find("#field-smtp-secure").prop("checked",l.smtp?.secure||!1),e.find("#field-smtp-from-address").val(l.smtp?.fromAddress||""),e.find("#field-smtp-from-name").val(l.smtp?.fromName||"");const i=l.backToTop||{};e.find("#field-btt-enabled").prop("checked",i.enabled!==!1),e.find("#field-btt-threshold").val(i.scrollThreshold??300),e.find("#field-btt-position").val(i.position||"bottom-right"),e.find("#field-btt-offset").val(i.offset??32),e.find("#field-btt-bottom-offset").val(i.bottomOffset??i.offset??32),e.find("#field-btt-label").val(i.label||""),e.find("#field-btt-smooth").prop("checked",i.smooth!==!1);const d=l.cookieConsent||{};e.find("#field-cc-enabled").prop("checked",d.enabled!==!1),e.find("#field-cc-message").val(d.message||""),e.find("#field-cc-accept-all").val(d.acceptAllText||"Accept All"),e.find("#field-cc-reject-all").val(d.rejectAllText||"Reject All"),e.find("#field-cc-customize").val(d.customizeText||"Customize"),e.find("#field-cc-save-prefs").val(d.savePreferencesText||"Save Preferences"),e.find("#field-cc-privacy-text").val(d.privacyPolicyText||"Privacy Policy"),e.find("#field-cc-privacy-url").val(d.privacyPolicyUrl||""),e.find("#field-cc-cookie-text").val(d.cookiePolicyText||"Cookie Policy"),e.find("#field-cc-cookie-url").val(d.cookiePolicyUrl||""),e.find("#field-cc-position").val(d.position||"bottom"),e.find("#field-cc-layout").val(d.layout||"bar"),e.find("#field-cc-theme").val(d.theme||"dark"),e.find("#field-cc-show-functional").prop("checked",d.showFunctional!==!1),e.find("#field-cc-show-analytics").prop("checked",d.showAnalytics!==!1),e.find("#field-cc-show-marketing").prop("checked",d.showMarketing!==!1),e.find("#field-cc-version").val(d.consentVersion||"1.0");const m=l.breadcrumbs||{};e.find("#field-breadcrumbs-enabled").prop("checked",m.enabled===!0),e.find("#field-breadcrumbs-home-label").val(m.homeLabel||"Home"),e.find("#field-breadcrumbs-position").val(m.position||"TL"),e.find("#field-bc-offset-x").val(m.offsetX??8),e.find("#field-bc-offset-y").val(m.offsetY??8);function c(t){e.find(".bc-pos-btn").each(function(){const s=this.dataset.pos===t;this.style.background=s?"var(--primary, #5b8cff)":"",this.style.color=s?"#fff":""})}c(m.position||"TL"),e.find(".bc-pos-btn").on("click",function(){const t=this.dataset.pos;e.find("#field-breadcrumbs-position").val(t),c(t)});let a=null;try{const{css:t}=await v("/settings/custom-css");e.find("#field-custom-css").val(t||""),E.editor&&(a=E.editor(e.find("#field-custom-css").get(0),{mode:"code",language:"css",lineNumbers:!0,showToolbar:!1,minHeight:420,placeholder:"/* Add your custom CSS here */",characterCount:!0}))}catch{}a&&a._editorEl&&a._editorEl.addEventListener("keydown",t=>{if(!t.ctrlKey&&!t.metaKey)return;if(t.key==="s"){t.preventDefault(),e.find("#save-css-btn").get(0)?.click();return}const s=a._editorEl,n=s.selectionStart!==s.selectionEnd;if((t.key==="c"||t.key==="x")&&!n){t.preventDefault();const o=s.value,g=s.selectionStart,T=o.lastIndexOf(`
2
2
  `,g-1)+1,x=o.indexOf(`
3
3
  `,g),S=x===-1?o.slice(T):o.slice(T,x+1);if(navigator.clipboard.writeText(S),t.key==="x"){const z=o.slice(0,T),P=x===-1?"":o.slice(x+1);s.value=z+P,s.selectionStart=s.selectionEnd=T,s.dispatchEvent(new Event("input",{bubbles:!0})),E.toast("Line cut",{type:"info",duration:1500})}else E.toast("Line copied",{type:"info",duration:1500})}}),e.find("#save-settings-btn").on("click",async()=>{const t=e.find("#field-admin-theme").val(),s=!l.baseTheme&&e.find("#field-auto-theme-enabled").prop("checked"),n={enabled:s,dayTheme:e.find("#field-day-theme").val()||"charcoal-light",nightTheme:e.find("#field-night-theme").val()||"charcoal-dark",dayStart:e.find("#field-day-start").val()||"07:00",nightStart:e.find("#field-night-start").val()||"19:00"},o={title:e.find("#field-site-title").val().trim(),tagline:e.find("#field-tagline").val().trim(),fontFamily:e.find("#field-font-family").val()||"Roboto",fontSize:parseInt(e.find("#field-font-size").val(),10)||16,theme:l.baseTheme?l.theme:s?n.dayTheme:e.find("#field-theme").val(),...l.baseTheme?{baseTheme:l.baseTheme}:{},autoTheme:n,adminTheme:t,layoutOptions:{spacerSize:parseInt(e.find("#field-spacer-size").val(),10)||40},seo:{defaultTitle:e.find("#field-seo-title").val().trim(),titleSeparator:e.find("#field-seo-separator").val()||" | ",defaultDescription:e.find("#field-seo-desc").val().trim()},footer:{copyright:e.find("#field-footer-copy").val().trim(),links:l.footer?.links||[]},social:{twitter:e.find("#field-social-twitter").val().trim(),facebook:e.find("#field-social-facebook").val().trim(),instagram:e.find("#field-social-instagram").val().trim(),linkedin:e.find("#field-social-linkedin").val().trim(),github:e.find("#field-social-github").val().trim(),youtube:e.find("#field-social-youtube").val().trim()},smtp:{host:e.find("#field-smtp-host").val().trim(),port:parseInt(e.find("#field-smtp-port").val(),10)||587,user:e.find("#field-smtp-user").val().trim(),pass:e.find("#field-smtp-pass").val(),secure:e.find("#field-smtp-secure").prop("checked"),fromAddress:e.find("#field-smtp-from-address").val().trim(),fromName:e.find("#field-smtp-from-name").val().trim()},backToTop:{enabled:e.find("#field-btt-enabled").prop("checked"),scrollThreshold:parseInt(e.find("#field-btt-threshold").val(),10)||300,position:e.find("#field-btt-position").val()||"bottom-right",offset:parseInt(e.find("#field-btt-offset").val(),10)||32,bottomOffset:parseInt(e.find("#field-btt-bottom-offset").val(),10)||32,label:e.find("#field-btt-label").val().trim(),smooth:e.find("#field-btt-smooth").prop("checked")},cookieConsent:{enabled:e.find("#field-cc-enabled").prop("checked"),message:e.find("#field-cc-message").val().trim(),acceptAllText:e.find("#field-cc-accept-all").val().trim(),rejectAllText:e.find("#field-cc-reject-all").val().trim(),customizeText:e.find("#field-cc-customize").val().trim(),savePreferencesText:e.find("#field-cc-save-prefs").val().trim(),privacyPolicyText:e.find("#field-cc-privacy-text").val().trim(),privacyPolicyUrl:e.find("#field-cc-privacy-url").val().trim(),cookiePolicyText:e.find("#field-cc-cookie-text").val().trim(),cookiePolicyUrl:e.find("#field-cc-cookie-url").val().trim(),position:e.find("#field-cc-position").val(),layout:e.find("#field-cc-layout").val(),theme:e.find("#field-cc-theme").val(),showFunctional:e.find("#field-cc-show-functional").prop("checked"),showAnalytics:e.find("#field-cc-show-analytics").prop("checked"),showMarketing:e.find("#field-cc-show-marketing").prop("checked"),consentVersion:e.find("#field-cc-version").val().trim()||"1.0"}},g=e.find("#save-settings-btn");g.prop("disabled",!0);try{await C.settings.save(o),Domma.theme.set(t),E.toast("Settings saved.",{type:"success"})}catch{E.toast("Failed to save settings.",{type:"error"})}finally{g.prop("disabled",!1)}}),e.find("#send-test-email-btn").on("click",async()=>{const t=e.find("#field-test-email-to").val().trim(),s=e.find("#test-email-result").get(0),n=e.find("#send-test-email-btn");n.prop("disabled",!0),s&&(s.textContent="Sending\u2026",s.style.color="");try{const o=await v("/settings/test-email",{method:"POST",body:JSON.stringify({to:t||void 0})});s&&(s.textContent=o.message||"Test email sent.",s.style.color="var(--success,#4ade80)")}catch(o){s&&(s.textContent=o.message||"Failed to send test email.",s.style.color="var(--danger,#f87171)")}finally{n.prop("disabled",!1)}}),e.find("#save-breadcrumbs-btn").on("click",async()=>{const t=e.find("#save-breadcrumbs-btn");t.prop("disabled",!0);try{const s={enabled:e.find("#field-breadcrumbs-enabled").prop("checked"),homeLabel:e.find("#field-breadcrumbs-home-label").val().trim()||"Home",position:e.find("#field-breadcrumbs-position").val()||"TL",offsetX:parseInt(e.find("#field-bc-offset-x").val(),10)||8,offsetY:parseInt(e.find("#field-bc-offset-y").val(),10)||8};await C.settings.save({...l,breadcrumbs:s}),l.breadcrumbs=s,E.toast("Breadcrumbs settings saved.",{type:"success"})}catch{E.toast("Failed to save breadcrumbs settings.",{type:"error"})}finally{t.prop("disabled",!1)}}),e.find("#save-css-btn").on("click",async()=>{const t=a?a.getValue():e.find("#field-custom-css").val(),s=e.find("#save-css-btn");s.prop("disabled",!0);try{await v("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:t})}),E.toast("Custom CSS saved.",{type:"success"})}catch(n){E.toast(n.message||"Failed to save CSS.",{type:"error"})}finally{s.prop("disabled",!1)}});async function r(){try{const t=await v("/cache/status"),s=t.enabled?`enabled (${t.driver})`:"disabled",n=t.maxItems?` \xB7 ${t.size} / ${t.maxItems} entries`:"";e.find("#cache-status").text(s+n)}catch{e.find("#cache-status").text("unknown")}try{const{entries:t}=await v("/cache/keys");A(e.find("#cache-keys-table"),t),e.find("#cache-key-count").text(`(${t.length})`)}catch(t){e.find("#cache-keys-table").get(0).textContent="Failed to load keys: "+t.message}}await r(),e.find("#btn-clear-cache").on("click",async()=>{if(!await E.confirm("Clear the entire response cache?"))return;const t=e.find("#btn-clear-cache");t.prop("disabled",!0);try{await v("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"}),await r()}catch(s){E.toast(s.message||"Failed to clear cache.",{type:"error"})}finally{t.prop("disabled",!1)}}),e.find("#btn-refresh-cache-keys").on("click",r)}};function A(e,k){const l=e.get(0);if(!l)return;for(;l.firstChild;)l.removeChild(l.firstChild);if(!k.length){const f=document.createElement("p");f.className="text-muted",f.style.fontSize=".875rem",f.textContent="Cache is empty.",l.appendChild(f);return}const p=[...k].reverse(),h=document.createElement("table");h.className="table table-compact";const b=document.createElement("thead"),y=document.createElement("tr");for(const f of["Key","Tags","Expires"]){const i=document.createElement("th");i.textContent=f,y.appendChild(i)}b.appendChild(y),h.appendChild(b);const u=document.createElement("tbody");for(const f of p){const i=document.createElement("tr"),d=document.createElement("td");d.style.fontFamily="monospace",d.style.fontSize="12px",d.textContent=f.key,i.appendChild(d);const m=document.createElement("td");m.style.fontSize="12px";for(const a of f.tags){const r=document.createElement("span");r.className="badge",r.style.marginRight="4px",r.style.fontSize="11px",r.textContent=a,m.appendChild(r)}i.appendChild(m);const c=document.createElement("td");if(c.style.fontSize="12px",c.style.color="var(--dm-muted, #6b7280)",f.expiresAt===null)c.textContent="no expiry";else{const a=f.expiresAt-Date.now();a<=0?c.textContent="expired":a<6e4?c.textContent=`${Math.round(a/1e3)}s`:a<36e5?c.textContent=`${Math.round(a/6e4)}m`:c.textContent=`${Math.round(a/36e5)}h`}i.appendChild(c),u.appendChild(i)}h.appendChild(u),l.appendChild(h)}
@@ -1,44 +1,44 @@
1
- /**
2
- * Domma CMS — Config Merge Utility
3
- * Merges new keys from an upstream config into an existing user config,
4
- * without ever overwriting values the user already has.
5
- */
6
-
7
- /**
8
- * Deep-merge new keys from `upstream` into `existing`.
9
- * Existing values are never modified — only missing keys are added.
10
- *
11
- * @param {object} existing - The user's current config object
12
- * @param {object} upstream - The upstream (new version) config object
13
- * @param {string} [_prefix] - Internal: key path prefix for reporting
14
- * @returns {{ merged: object, added: string[] }} Merged object + list of added key paths
15
- */
16
- export function deepMergeNewKeys(existing, upstream, _prefix = '') {
17
- const merged = {...existing};
18
- const added = [];
19
-
20
- for (const [key, upstreamVal] of Object.entries(upstream)) {
21
- const fullKey = _prefix ? `${_prefix}.${key}` : key;
22
-
23
- if (!(key in existing)) {
24
- // Key is entirely missing — add it wholesale
25
- merged[key] = upstreamVal;
26
- added.push(fullKey);
27
- } else if (
28
- upstreamVal !== null &&
29
- typeof upstreamVal === 'object' &&
30
- !Array.isArray(upstreamVal) &&
31
- typeof existing[key] === 'object' &&
32
- existing[key] !== null &&
33
- !Array.isArray(existing[key])
34
- ) {
35
- // Both sides are plain objects — recurse
36
- const child = deepMergeNewKeys(existing[key], upstreamVal, fullKey);
37
- merged[key] = child.merged;
38
- added.push(...child.added);
39
- }
40
- // Otherwise: existing value wins — no action
41
- }
42
-
43
- return {merged, added};
44
- }
1
+ /**
2
+ * Domma CMS — Config Merge Utility
3
+ * Merges new keys from an upstream config into an existing user config,
4
+ * without ever overwriting values the user already has.
5
+ */
6
+
7
+ /**
8
+ * Deep-merge new keys from `upstream` into `existing`.
9
+ * Existing values are never modified — only missing keys are added.
10
+ *
11
+ * @param {object} existing - The user's current config object
12
+ * @param {object} upstream - The upstream (new version) config object
13
+ * @param {string} [_prefix] - Internal: key path prefix for reporting
14
+ * @returns {{ merged: object, added: string[] }} Merged object + list of added key paths
15
+ */
16
+ export function deepMergeNewKeys(existing, upstream, _prefix = '') {
17
+ const merged = {...existing};
18
+ const added = [];
19
+
20
+ for (const [key, upstreamVal] of Object.entries(upstream)) {
21
+ const fullKey = _prefix ? `${_prefix}.${key}` : key;
22
+
23
+ if (!(key in existing)) {
24
+ // Key is entirely missing — add it wholesale
25
+ merged[key] = upstreamVal;
26
+ added.push(fullKey);
27
+ } else if (
28
+ upstreamVal !== null &&
29
+ typeof upstreamVal === 'object' &&
30
+ !Array.isArray(upstreamVal) &&
31
+ typeof existing[key] === 'object' &&
32
+ existing[key] !== null &&
33
+ !Array.isArray(existing[key])
34
+ ) {
35
+ // Both sides are plain objects — recurse
36
+ const child = deepMergeNewKeys(existing[key], upstreamVal, fullKey);
37
+ merged[key] = child.merged;
38
+ added.push(...child.added);
39
+ }
40
+ // Otherwise: existing value wins — no action
41
+ }
42
+
43
+ return {merged, added};
44
+ }
package/config/site.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "title": "My CMS Site",
3
+ "url": "http://domma-cms.local:4096",
3
4
  "tagline": "This is the Domma CMS",
4
5
  "theme": "silver-dark",
5
6
  "seo": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.30.0",
3
+ "version": "0.32.1",
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",
@@ -81,7 +81,7 @@
81
81
  "@fastify/rate-limit": "^10.3.0",
82
82
  "@fastify/static": "9.1.1",
83
83
  "bcryptjs": "^3.0.3",
84
- "domma-js": "^0.27.1",
84
+ "domma-js": "0.29.1",
85
85
  "dotenv": "^17.2.3",
86
86
  "fastify": "5.8.5",
87
87
  "gray-matter": "^4.0.3",
@@ -0,0 +1,47 @@
1
+ <div class="survey-audience-plugin">
2
+
3
+ <div class="toolbar mb-3" style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
4
+ <h2 style="margin:0;flex:1;">Survey Audience</h2>
5
+ <button id="back-surveys" class="btn btn-outline">
6
+ <span data-icon="arrow-left"></span>
7
+ Surveys
8
+ </button>
9
+ <button id="import-contacts" class="btn btn-outline">
10
+ <span data-icon="download"></span>
11
+ Import from Contacts
12
+ </button>
13
+ <button id="add-contact" class="btn btn-primary">
14
+ <span data-icon="plus"></span>
15
+ Add contact
16
+ </button>
17
+ </div>
18
+
19
+ <div class="grid grid-cols-3" style="gap:1rem;align-items:start;">
20
+
21
+ <!-- Contacts table -->
22
+ <div style="grid-column:span 2;">
23
+ <div id="audience-table"></div>
24
+ <div id="audience-empty" style="display:none;text-align:center;padding:2rem 1rem;color:var(--dm-text-muted);">
25
+ <p>No audience contacts yet. Add one or import from the Contacts plugin.</p>
26
+ </div>
27
+ </div>
28
+
29
+ <!-- Groups panel -->
30
+ <div class="card">
31
+ <div class="card-header"><strong>Groups</strong></div>
32
+ <div class="card-body">
33
+ <div style="display:flex;gap:0.5rem;margin-bottom:1rem;">
34
+ <input id="new-group-name" type="text" class="form-input" placeholder="New group name…" style="flex:1;">
35
+ <button id="add-group" class="btn btn-primary">
36
+ <span data-icon="plus"></span>
37
+ </button>
38
+ </div>
39
+ <div id="groups-list">
40
+ <p class="text-muted">No groups yet.</p>
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ </div>
46
+
47
+ </div>
@@ -0,0 +1,58 @@
1
+ <div class="survey-results-plugin">
2
+
3
+ <style>
4
+ /* Layout only + real Domma theme tokens — surfaces use .card so the
5
+ active theme is always respected; no hardcoded colours. */
6
+ .survey-results-plugin .sr-head { display:flex; align-items:flex-start; gap:1rem; flex-wrap:wrap; margin-bottom:1.25rem; }
7
+ .survey-results-plugin .sr-head h2 { margin:0 0 0.25rem; }
8
+ .survey-results-plugin .sr-sub { color:var(--dm-text-muted); margin:0; }
9
+ .survey-results-plugin .sr-head-actions { margin-left:auto; display:flex; gap:0.5rem; flex-wrap:wrap; }
10
+
11
+ .survey-results-plugin .sr-summary { display:grid; grid-template-columns:1.4fr 1fr 1fr 1fr; gap:1rem; margin-bottom:1.5rem; }
12
+ @media (max-width:760px){ .survey-results-plugin .sr-summary { grid-template-columns:1fr 1fr; } }
13
+ .survey-results-plugin .sr-rate-num { font-size:2.4rem; font-weight:800; line-height:1; color:var(--dm-primary); }
14
+ .survey-results-plugin .sr-kpi-num { font-size:2rem; font-weight:800; line-height:1; }
15
+ .survey-results-plugin .sr-kpi-lbl { color:var(--dm-text-muted); font-size:0.85rem; margin-top:0.35rem; }
16
+ .survey-results-plugin .sr-rate-bar { height:8px; border-radius:999px; background:var(--dm-border); margin-top:0.75rem; overflow:hidden; }
17
+ .survey-results-plugin .sr-rate-fill { height:100%; background:var(--dm-primary); border-radius:999px; }
18
+
19
+ .survey-results-plugin .sr-section-title { font-size:1.05rem; font-weight:700; margin:0 0 0.75rem; }
20
+ .survey-results-plugin .sr-q-head { display:flex; align-items:baseline; gap:0.6rem; margin-bottom:0.75rem; }
21
+ .survey-results-plugin .sr-q-num { display:inline-flex; align-items:center; justify-content:center; min-width:1.6rem; height:1.6rem; border-radius:50%; background:var(--dm-primary); color:var(--dm-text-inverse); font-size:0.8rem; font-weight:700; }
22
+ .survey-results-plugin .sr-q-label { font-weight:700; }
23
+ .survey-results-plugin .sr-q-meta { margin-left:auto; color:var(--dm-text-muted); font-size:0.8rem; text-transform:uppercase; letter-spacing:0.03em; }
24
+ .survey-results-plugin .sr-avg { font-size:1.1rem; margin:0 0 0.6rem; }
25
+ .survey-results-plugin .sr-avg strong { font-size:1.5rem; color:var(--dm-primary); }
26
+
27
+ /* Distribution bars (survey.css isn't loaded in admin, so style here). */
28
+ .survey-results-plugin .survey-dist { display:flex; flex-direction:column; gap:0.5rem; }
29
+ .survey-results-plugin .survey-dist-row { display:grid; grid-template-columns:minmax(80px,160px) 1fr auto; align-items:center; gap:0.75rem; }
30
+ .survey-results-plugin .survey-dist-label { font-size:0.9rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
31
+ .survey-results-plugin .survey-dist-bar { height:14px; background:var(--dm-border); border-radius:999px; overflow:hidden; }
32
+ .survey-results-plugin .survey-dist-fill { height:100%; background:var(--dm-primary); border-radius:999px; min-width:2px; }
33
+ .survey-results-plugin .survey-dist-val { font-size:0.85rem; color:var(--dm-text-muted); white-space:nowrap; }
34
+
35
+ /* Free-text answers. */
36
+ .survey-results-plugin .sr-quotes { max-height:260px; overflow:auto; display:flex; flex-direction:column; gap:0.5rem; }
37
+ .survey-results-plugin .sr-quote { padding:0.5rem 0.75rem; background:var(--dm-surface-raised); border-left:3px solid var(--dm-border); border-radius:0 6px 6px 0; font-size:0.92rem; }
38
+ .survey-results-plugin .sr-empty { color:var(--dm-text-muted); font-style:italic; }
39
+ </style>
40
+
41
+ <div class="sr-head">
42
+ <div>
43
+ <h2 id="results-title">Survey Results</h2>
44
+ <p class="sr-sub" id="results-sub">How your audience responded.</p>
45
+ </div>
46
+ <div class="sr-head-actions">
47
+ <button id="export-csv" class="btn btn-outline"><span data-icon="download"></span> CSV</button>
48
+ <button id="export-json" class="btn btn-outline"><span data-icon="download"></span> JSON</button>
49
+ <button id="back-surveys" class="btn btn-outline"><span data-icon="arrow-left"></span> Surveys</button>
50
+ </div>
51
+ </div>
52
+
53
+ <div id="summary" class="sr-summary"></div>
54
+
55
+ <h3 class="sr-section-title">Question breakdown</h3>
56
+ <div id="questions"></div>
57
+
58
+ </div>