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.
Files changed (221) hide show
  1. package/CLAUDE.md +64 -1
  2. package/README.md +14 -10
  3. package/admin/css/admin.css +1 -1
  4. package/admin/index.html +1 -0
  5. package/admin/js/api.js +1 -1
  6. package/admin/js/app.js +4 -4
  7. package/admin/js/lib/analytics-shape.js +1 -0
  8. package/admin/js/lib/contacts-arrange.js +1 -0
  9. package/admin/js/lib/help-popover.js +1 -0
  10. package/admin/js/lib/notes-arrange.js +1 -0
  11. package/admin/js/lib/plugin-accent.js +1 -1
  12. package/admin/js/lib/plugin-chrome.js +1 -1
  13. package/admin/js/lib/sidebar-grouping.js +1 -1
  14. package/admin/js/lib/sidebar-grouping.test.js +1 -1
  15. package/admin/js/lib/sidebar-renderer.js +3 -3
  16. package/admin/js/lib/todo-arrange.js +1 -0
  17. package/admin/js/lib/tool-kit.js +1 -0
  18. package/admin/js/templates/analytics.html +138 -0
  19. package/admin/js/templates/contacts.html +156 -0
  20. package/admin/js/templates/notes.html +137 -0
  21. package/admin/js/templates/plugin-marketplace.html +17 -4
  22. package/admin/js/templates/plugins.html +13 -7
  23. package/admin/js/templates/theme.html +38 -0
  24. package/admin/js/templates/todo.html +113 -0
  25. package/admin/js/views/analytics.js +62 -0
  26. package/admin/js/views/contacts.js +174 -0
  27. package/admin/js/views/index.js +1 -1
  28. package/admin/js/views/notes.js +104 -0
  29. package/admin/js/views/plugin-marketplace.js +1 -1
  30. package/admin/js/views/plugins.js +24 -22
  31. package/admin/js/views/theme.js +1 -1
  32. package/admin/js/views/todo.js +96 -0
  33. package/bin/cli.js +4 -10
  34. package/bin/lib/plugin-version.js +28 -0
  35. package/bin/update.js +14 -0
  36. package/config/menus/admin-sidebar.json +1 -1
  37. package/config/server.json +3 -1
  38. package/package.json +3 -7
  39. package/plugins/_lib/admin/mail/reader-view.js +20 -0
  40. package/plugins/_lib/mail/store.js +21 -1
  41. package/plugins/blog/CLAUDE.md +237 -0
  42. package/plugins/blog/admin/css/index.css +178 -1
  43. package/plugins/blog/admin/templates/blog.html +35 -53
  44. package/plugins/blog/admin/templates/categories.html +19 -6
  45. package/plugins/blog/admin/templates/comments.html +26 -9
  46. package/plugins/blog/admin/templates/post-editor.html +36 -13
  47. package/plugins/blog/admin/templates/settings.html +2 -11
  48. package/plugins/blog/admin/views/blog.js +288 -162
  49. package/plugins/blog/admin/views/categories.js +212 -201
  50. package/plugins/blog/admin/views/comments.js +207 -158
  51. package/plugins/blog/admin/views/kit.js +372 -0
  52. package/plugins/blog/admin/views/post-editor.js +269 -21
  53. package/plugins/blog/admin/views/settings-cog.js +56 -0
  54. package/plugins/blog/admin/views/settings.js +16 -86
  55. package/plugins/blog/blocks/blog-card-compact.css +17 -0
  56. package/plugins/blog/blocks/blog-card-compact.html +8 -0
  57. package/plugins/blog/blocks/blog-card-row.css +22 -0
  58. package/plugins/blog/blocks/blog-card-row.html +11 -0
  59. package/plugins/blog/blocks/blog-card.css +35 -0
  60. package/plugins/blog/blocks/blog-card.html +16 -0
  61. package/plugins/blog/blocks/blog-post-classic.css +15 -0
  62. package/plugins/blog/blocks/blog-post-classic.html +28 -0
  63. package/plugins/blog/blocks/blog-post-feature.css +20 -0
  64. package/plugins/blog/blocks/blog-post-feature.html +30 -0
  65. package/plugins/blog/blocks/blog-post-minimal.css +10 -0
  66. package/plugins/blog/blocks/blog-post-minimal.html +23 -0
  67. package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
  68. package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
  69. package/plugins/blog/collections/posts/schema.json +3 -1
  70. package/plugins/blog/config.js +6 -0
  71. package/plugins/blog/lib/layouts.js +304 -0
  72. package/plugins/blog/lib/page.js +158 -0
  73. package/plugins/blog/lib/render.js +119 -0
  74. package/plugins/blog/lib/samples.js +234 -0
  75. package/plugins/blog/plugin.js +225 -15
  76. package/plugins/blog/plugin.json +21 -81
  77. package/plugins/blog/plugin.public.js +115 -210
  78. package/plugins/blog/public/blog.css +177 -0
  79. package/plugins/blog/public/blog.js +586 -0
  80. package/plugins/blog/public/samples/aurora.svg +7 -0
  81. package/plugins/blog/public/samples/dusk.svg +7 -0
  82. package/plugins/blog/public/samples/ember.svg +7 -0
  83. package/plugins/blog/public/samples/harbour.svg +7 -0
  84. package/plugins/blog/public/samples/meadow.svg +7 -0
  85. package/plugins/blog/public/samples/rosewood.svg +7 -0
  86. package/plugins/blog/templates/index.html +9 -8
  87. package/plugins/blog/templates/post.html +4 -17
  88. package/plugins/blog/tests/layouts.test.js +97 -0
  89. package/plugins/blog/tests/public.test.js +71 -0
  90. package/plugins/free-tier.lock.json +74 -0
  91. package/plugins/mail-reader/CLAUDE.md +78 -0
  92. package/plugins/mail-reader/plugin.json +2 -1
  93. package/public/css/theme-switcher.css +1 -0
  94. package/public/js/analytics.js +1 -0
  95. package/public/js/theme-switcher.js +1 -0
  96. package/scripts/build.js +41 -2
  97. package/scripts/run-tests.mjs +92 -0
  98. package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
  99. package/{plugins/contacts/plugin.js → server/routes/api/contacts.js} +71 -43
  100. package/server/routes/api/dashboard.js +8 -2
  101. package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
  102. package/server/routes/api/plugin-marketplace.js +78 -23
  103. package/server/routes/api/plugins.js +148 -3
  104. package/server/routes/api/todo.js +178 -0
  105. package/server/server.js +61 -1
  106. package/server/services/collections.js +13 -3
  107. package/server/services/managerClient.js +72 -136
  108. package/server/services/markdown.js +54 -1
  109. package/server/services/permissionRegistry.js +50 -0
  110. package/server/services/pluginEntitlement.js +171 -0
  111. package/server/services/pluginEnvelope.js +242 -0
  112. package/server/services/pluginInstaller.js +246 -26
  113. package/server/services/pluginScaffold.js +5 -0
  114. package/server/services/plugins.js +117 -7
  115. package/server/services/presetCollections.js +344 -0
  116. package/server/services/promoted-plugins-migration.js +254 -0
  117. package/server/services/publicCors.js +46 -0
  118. package/server/services/renderer.js +83 -3
  119. package/server/services/sidebarBadges.js +37 -0
  120. package/server/services/siteGitignore.js +307 -0
  121. package/server/services/themeSettings.js +39 -0
  122. package/config/theme.json +0 -18
  123. package/plugins/analytics/admin/css/index.css +0 -1
  124. package/plugins/analytics/admin/templates/analytics.html +0 -70
  125. package/plugins/analytics/admin/views/analytics.js +0 -176
  126. package/plugins/analytics/config.js +0 -14
  127. package/plugins/analytics/plugin.json +0 -60
  128. package/plugins/analytics/public/inject-body.html +0 -60
  129. package/plugins/analytics/public/inject-head.html +0 -1
  130. package/plugins/blog/templates/author.html +0 -9
  131. package/plugins/blog/templates/category.html +0 -9
  132. package/plugins/blog/templates/tag.html +0 -9
  133. package/plugins/contacts/admin/templates/contacts.html +0 -254
  134. package/plugins/contacts/admin/views/contacts.js +0 -943
  135. package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
  136. package/plugins/contacts/collections/user-contacts/schema.json +0 -179
  137. package/plugins/contacts/config.js +0 -6
  138. package/plugins/contacts/plugin.json +0 -36
  139. package/plugins/demo-viewer/config.js +0 -4
  140. package/plugins/demo-viewer/plugin.js +0 -36
  141. package/plugins/demo-viewer/plugin.json +0 -9
  142. package/plugins/invoice/admin/css/index.css +0 -1
  143. package/plugins/invoice/admin/templates/editor.html +0 -220
  144. package/plugins/invoice/admin/templates/index.html +0 -173
  145. package/plugins/invoice/admin/templates/issuers.html +0 -2
  146. package/plugins/invoice/admin/templates/receivers.html +0 -2
  147. package/plugins/invoice/admin/views/contacts-source.js +0 -266
  148. package/plugins/invoice/admin/views/editor.js +0 -434
  149. package/plugins/invoice/admin/views/export.js +0 -199
  150. package/plugins/invoice/admin/views/help-content.js +0 -61
  151. package/plugins/invoice/admin/views/index.js +0 -643
  152. package/plugins/invoice/admin/views/issuers.js +0 -30
  153. package/plugins/invoice/admin/views/media.js +0 -172
  154. package/plugins/invoice/admin/views/party-view.js +0 -386
  155. package/plugins/invoice/admin/views/payments.js +0 -127
  156. package/plugins/invoice/admin/views/print.js +0 -130
  157. package/plugins/invoice/admin/views/receivers.js +0 -55
  158. package/plugins/invoice/admin/views/send.js +0 -212
  159. package/plugins/invoice/admin/views/settings.js +0 -594
  160. package/plugins/invoice/admin/views/view-lifecycle.js +0 -33
  161. package/plugins/invoice/collections/invoice-issuers/schema.json +0 -82
  162. package/plugins/invoice/collections/invoice-receivers/schema.json +0 -16
  163. package/plugins/invoice/collections/invoices/schema.json +0 -33
  164. package/plugins/invoice/config.js +0 -37
  165. package/plugins/invoice/pdf.js +0 -164
  166. package/plugins/invoice/plugin.js +0 -1456
  167. package/plugins/invoice/plugin.json +0 -86
  168. package/plugins/invoice/templates/_base.css +0 -1
  169. package/plugins/invoice/templates/classic-nologo.html +0 -100
  170. package/plugins/invoice/templates/classic.html +0 -91
  171. package/plugins/invoice/templates/invoice-print.html +0 -237
  172. package/plugins/invoice/templates/minimal.html +0 -99
  173. package/plugins/invoice/templates/modern-nologo.html +0 -114
  174. package/plugins/invoice/templates/modern.html +0 -113
  175. package/plugins/invoice/templates/templates.json +0 -11
  176. package/plugins/notes/admin/templates/notes.html +0 -83
  177. package/plugins/notes/admin/views/notes.js +0 -281
  178. package/plugins/notes/collections/user-notes/schema.json +0 -54
  179. package/plugins/notes/config.js +0 -6
  180. package/plugins/notes/plugin.json +0 -33
  181. package/plugins/surveys/admin/templates/audience.html +0 -47
  182. package/plugins/surveys/admin/templates/results.html +0 -58
  183. package/plugins/surveys/admin/templates/survey-editor.html +0 -141
  184. package/plugins/surveys/admin/templates/surveys.html +0 -25
  185. package/plugins/surveys/admin/views/audience.js +0 -301
  186. package/plugins/surveys/admin/views/results.js +0 -172
  187. package/plugins/surveys/admin/views/survey-editor.js +0 -211
  188. package/plugins/surveys/admin/views/surveys.js +0 -161
  189. package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
  190. package/plugins/surveys/collections/survey-groups/schema.json +0 -11
  191. package/plugins/surveys/collections/survey-invites/schema.json +0 -16
  192. package/plugins/surveys/collections/surveys/schema.json +0 -23
  193. package/plugins/surveys/config.js +0 -8
  194. package/plugins/surveys/lib/audience.js +0 -37
  195. package/plugins/surveys/lib/campaigns.js +0 -43
  196. package/plugins/surveys/lib/ledger.js +0 -110
  197. package/plugins/surveys/lib/sending.js +0 -106
  198. package/plugins/surveys/lib/stats.js +0 -62
  199. package/plugins/surveys/lib/submit.js +0 -95
  200. package/plugins/surveys/lib/tokens.js +0 -28
  201. package/plugins/surveys/plugin.js +0 -174
  202. package/plugins/surveys/plugin.json +0 -65
  203. package/plugins/surveys/plugin.public.js +0 -149
  204. package/plugins/surveys/public/dist-shared.mjs +0 -1
  205. package/plugins/surveys/public/survey.css +0 -1
  206. package/plugins/surveys/public/survey.mjs +0 -1
  207. package/plugins/surveys/templates/survey-page.html +0 -63
  208. package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
  209. package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
  210. package/plugins/theme-switcher/config.js +0 -10
  211. package/plugins/theme-switcher/plugin.js +0 -26
  212. package/plugins/theme-switcher/plugin.json +0 -74
  213. package/plugins/theme-switcher/public/inject-body.html +0 -153
  214. package/plugins/theme-switcher/public/inject-head.html +0 -260
  215. package/plugins/todo/admin/templates/todo.html +0 -158
  216. package/plugins/todo/admin/views/todo.js +0 -343
  217. package/plugins/todo/collections/todos/schema.json +0 -60
  218. package/plugins/todo/config.js +0 -7
  219. package/plugins/todo/plugin.js +0 -102
  220. package/plugins/todo/plugin.json +0 -33
  221. /package/{plugins/contacts/admin/css/index.css → admin/css/contacts.css} +0 -0
@@ -1,65 +0,0 @@
1
- {
2
- "name": "surveys",
3
- "displayName": "Surveys",
4
- "version": "1.0.0",
5
- "description": "Targeted surveys over the Forms engine: shared audiences, tokenised invites, reminders, and per-survey statistics.",
6
- "author": "Darryl Waterhouse",
7
- "date": "2026-06-26",
8
- "icon": "clipboard-list",
9
- "admin": {
10
- "sidebar": [
11
- {
12
- "id": "surveys",
13
- "text": "Surveys",
14
- "icon": "clipboard-list",
15
- "url": "#/plugins/surveys",
16
- "section": "#/plugins/surveys"
17
- }
18
- ],
19
- "routes": [
20
- {
21
- "path": "/plugins/surveys",
22
- "view": "plugin-surveys",
23
- "title": "Surveys - Domma CMS"
24
- },
25
- {
26
- "path": "/plugins/surveys/new",
27
- "view": "plugin-survey-editor",
28
- "title": "New Survey - Domma CMS"
29
- },
30
- {
31
- "path": "/plugins/surveys/edit/:slug",
32
- "view": "plugin-survey-editor",
33
- "title": "Edit Survey - Domma CMS"
34
- },
35
- {
36
- "path": "/plugins/surveys/audience",
37
- "view": "plugin-survey-audience",
38
- "title": "Survey Audience - Domma CMS"
39
- },
40
- {
41
- "path": "/plugins/surveys/results/:slug",
42
- "view": "plugin-survey-results",
43
- "title": "Survey Results - Domma CMS"
44
- }
45
- ],
46
- "views": {
47
- "plugin-surveys": {
48
- "entry": "surveys/admin/views/surveys.js?v=86946458",
49
- "exportName": "surveysView"
50
- },
51
- "plugin-survey-editor": {
52
- "entry": "surveys/admin/views/survey-editor.js?v=c6b3e1b7",
53
- "exportName": "surveyEditorView"
54
- },
55
- "plugin-survey-audience": {
56
- "entry": "surveys/admin/views/audience.js?v=6aca096f",
57
- "exportName": "audienceView"
58
- },
59
- "plugin-survey-results": {
60
- "entry": "surveys/admin/views/results.js?v=97b8e1fd",
61
- "exportName": "resultsView"
62
- }
63
- }
64
- }
65
- }
@@ -1,149 +0,0 @@
1
- /**
2
- * Surveys Plugin - Public Routes
3
- * Root-level public routes. Mounted at the Fastify root (no prefix).
4
- *
5
- * This file currently exposes the tokenised submit endpoint. The hosted survey
6
- * page and scheduler contract routes are added in later tasks.
7
- */
8
- import path from 'path';
9
- import fs from 'fs/promises';
10
- import {fileURLToPath} from 'url';
11
- import {createEntry, getEntry, listEntries, updateEntry, getCollection} from '../../server/services/collections.js';
12
- import {readForm} from '../../server/services/forms.js';
13
- import {createTransport, sendFormEmail, sendEmail} from '../../server/services/email.js';
14
- import {getConfig} from '../../server/config.js';
15
- import {createCampaigns} from './lib/campaigns.js';
16
- import {createAudience} from './lib/audience.js';
17
- import {createLedger} from './lib/ledger.js';
18
- import {processSurveySubmission, SurveyError} from './lib/submit.js';
19
- import {runSend} from './lib/sending.js';
20
- import {parseMarkdown} from '../../server/services/markdown.js';
21
-
22
- /** Render a Markdown string to sanitised HTML, or '' when empty. */
23
- async function renderMarkdown(md) {
24
- const text = (md ?? '').trim();
25
- if (!text) return '';
26
- try {
27
- const {html} = await parseMarkdown('---\n---\n' + text, {});
28
- return html || '';
29
- } catch {
30
- return '';
31
- }
32
- }
33
-
34
- const __dirname = fileURLToPath(new URL('.', import.meta.url));
35
- const TEMPLATES_DIR = path.join(__dirname, 'templates');
36
- const col = {createEntry, getEntry, listEntries, updateEntry};
37
-
38
- function escapeHtml(str) {
39
- return String(str ?? '')
40
- .replace(/&/g, '&')
41
- .replace(/</g, '&lt;')
42
- .replace(/>/g, '&gt;')
43
- .replace(/"/g, '&quot;')
44
- .replace(/'/g, '&#39;');
45
- }
46
-
47
- export default async function surveysPublicPlugin(fastify, options) {
48
- const campaigns = createCampaigns(col);
49
- const audience = createAudience(col);
50
- const ledger = createLedger(col);
51
- const schedulerToken = options.settings?.schedulerToken || '';
52
-
53
- async function sendCompletionEmail(form, data) {
54
- const ea = form.actions?.email;
55
- if (!ea?.enabled || !ea.recipients) return;
56
- const smtp = getConfig('site').smtp || {};
57
- const transport = await createTransport(smtp);
58
- await sendFormEmail(transport, {
59
- from: smtp.fromAddress, fromName: smtp.fromName, to: ea.recipients,
60
- subject: `${ea.subjectPrefix || `[${form.title}]`} New survey response`,
61
- formTitle: form.title, fields: form.fields, data
62
- });
63
- }
64
-
65
- const deps = {
66
- col, ledger,
67
- getSurveyBySlug: (slug) => campaigns.getBySlug(slug),
68
- readForm,
69
- getCollection,
70
- sendCompletionEmail,
71
- now: () => new Date().toISOString()
72
- };
73
-
74
- fastify.post('/api/plugins/surveys/submit/:slug', async (req, reply) => {
75
- const {token, data} = req.body ?? {};
76
- try {
77
- const result = await processSurveySubmission(req.params.slug, token, data || {}, deps);
78
- return reply.send(result);
79
- } catch (err) {
80
- const code = err instanceof SurveyError ? err.statusCode : 500;
81
- return reply.code(code).send({error: err.message});
82
- }
83
- });
84
-
85
- function requireScheduler(req, reply) {
86
- const hdr = req.headers.authorization || '';
87
- const tok = hdr.startsWith('Bearer ') ? hdr.slice(7) : '';
88
- if (!schedulerToken || tok !== schedulerToken) { reply.code(403).send({error: 'Forbidden'}); return false; }
89
- return true;
90
- }
91
-
92
- fastify.get('/api/plugins/surveys/due-work', async (req, reply) => {
93
- if (!requireScheduler(req, reply)) return;
94
- const now = new Date();
95
- const surveys = (await campaigns.all()).filter(s => s.data.status === 'open');
96
- const out = [];
97
- for (const s of surveys) {
98
- const due = await ledger.dueForReminder(s.id, Number(s.data.reminderAfterDays) || 7, now);
99
- if (due.length) out.push({plugin: 'surveys', campaignSlug: s.data.slug, dueCount: due.length});
100
- }
101
- return reply.send(out);
102
- });
103
-
104
- fastify.post('/api/plugins/surveys/run', async (req, reply) => {
105
- if (!requireScheduler(req, reply)) return;
106
- const {campaignSlug, audience: mode = 'non-responders'} = req.body ?? {};
107
- const survey = await campaigns.getBySlug(campaignSlug);
108
- if (!survey) return reply.code(404).send({error: 'Survey not found'});
109
- const smtp = getConfig('site').smtp || {};
110
- const transport = await createTransport(smtp);
111
- // Absolute survey links: configured site URL, else this request's origin.
112
- const baseUrl = (getConfig('site').url || '').replace(/\/$/, '')
113
- || `${req.protocol}://${req.headers.host}`;
114
- const {results} = await runSend({
115
- survey, mode: mode === 'non-responders' ? 'non-responders' : 'all',
116
- audience, ledger, transport, sendEmail, smtp,
117
- baseUrl, fromName: 'Surveys', nowIso: new Date().toISOString()
118
- });
119
- return reply.send({results});
120
- });
121
-
122
- // Hosted survey page - token travels in ?t=, or the visitor enters a code.
123
- fastify.get('/survey/:slug', async (req, reply) => {
124
- const survey = await campaigns.getBySlug(req.params.slug);
125
- if (!survey) {
126
- return reply.code(404).type('text/html')
127
- .send('<!DOCTYPE html><html lang="en-GB"><head><meta charset="UTF-8"><title>Survey unavailable</title></head><body><p>This survey is not available.</p></body></html>');
128
- }
129
- if (survey.data.status !== 'open') {
130
- return reply.code(410).type('text/html')
131
- .send('<!DOCTYPE html><html lang="en-GB"><head><meta charset="UTF-8"><title>Survey closed</title></head><body><p>This survey has closed.</p></body></html>');
132
- }
133
- const template = await fs.readFile(path.join(TEMPLATES_DIR, 'survey-page.html'), 'utf8');
134
- const [introHtml, footerHtml] = await Promise.all([
135
- renderMarkdown(survey.data.intro),
136
- renderMarkdown(survey.data.footer)
137
- ]);
138
- const html = template
139
- .replace(/\{\{title\}\}/g, escapeHtml(survey.data.title))
140
- .replace(/\{\{slug\}\}/g, escapeHtml(survey.data.slug))
141
- .replace(/\{\{formSlug\}\}/g, escapeHtml(survey.data.formSlug))
142
- // Intro/footer are already sanitised HTML from the Markdown pipeline.
143
- .replace(/\{\{intro\}\}/g, introHtml)
144
- .replace(/\{\{footer\}\}/g, footerHtml);
145
- return reply.type('text/html').send(html);
146
- });
147
-
148
- fastify.surveysPublic = {campaigns, audience, ledger, deps};
149
- }
@@ -1 +0,0 @@
1
- export function renderDistribution(a){return'<div class="survey-dist">'+a.map(s=>`<div class="survey-dist-row"><span class="survey-dist-label">${e(s.label)}</span><span class="survey-dist-bar"><span class="survey-dist-fill" data-pct="${s.pct}" style="width:${s.pct}%"></span></span><span class="survey-dist-val">${s.value} (${s.pct}%)</span></div>`).join("")+"</div>"}function e(a){return String(a??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}
@@ -1 +0,0 @@
1
- .survey-container{max-width:720px;margin:0 auto;padding:2rem 1rem 4rem}.survey-title{margin-bottom:1.5rem}.survey-code{margin-bottom:1.5rem;padding:1rem 1.25rem;border:1px solid var(--dm-border-color, #d0d0d0);border-radius:var(--dm-radius, 8px);background:var(--dm-surface, #fafafa)}.survey-code label{display:block;margin-bottom:.5rem;font-weight:600}.survey-code-row{display:flex;gap:.5rem;align-items:stretch;flex-wrap:wrap}.survey-code-row input{flex:1 1 200px;padding:.5rem .75rem;border:1px solid var(--dm-border-color, #d0d0d0);border-radius:var(--dm-radius, 6px);font:inherit}.survey-state{padding:1.5rem;border-radius:var(--dm-radius, 8px);text-align:center;font-size:1.05rem;line-height:1.5}.survey-state-success{background:var(--dm-success-bg, #e8f6ec);color:var(--dm-success-text, #1c6b34);border:1px solid var(--dm-success-border, #b6e0c2)}.survey-state-error{background:var(--dm-danger-bg, #fcebec);color:var(--dm-danger-text, #8a1f28);border:1px solid var(--dm-danger-border, #f0bfc4)}.survey-state-info{background:var(--dm-surface, #f4f4f4);color:var(--dm-text-muted, #555);border:1px solid var(--dm-border-color, #d8d8d8)}.survey-dist{display:flex;flex-direction:column;gap:.5rem;margin:.75rem 0}.survey-dist-row{display:grid;grid-template-columns:minmax(80px,30%) 1fr auto;align-items:center;gap:.75rem}.survey-dist-label{font-size:.9rem;color:var(--dm-text, #333);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.survey-dist-bar{position:relative;height:.9rem;background:var(--dm-surface-alt, #ececec);border-radius:var(--dm-radius, 6px);overflow:hidden}.survey-dist-fill{position:absolute;inset:0 auto 0 0;height:100%;background:var(--dm-primary, #3b6ef0);border-radius:inherit;transition:width .3s ease}.survey-dist-val{font-size:.85rem;color:var(--dm-text-muted, #666);min-width:3.5rem;text-align:right}.survey-intro{margin:0 0 1.5rem}.survey-footer{margin:1.5rem 0 0;padding-top:1rem;border-top:1px solid var(--dm-border, #e5e7eb);color:var(--dm-text-muted, #6b7280);font-size:.92rem}.survey-intro:empty,.survey-footer:empty{display:none;border:0;margin:0;padding:0}
@@ -1 +0,0 @@
1
- const SKIP_TYPES=new Set(["page-break","spacer","heading","paragraph","html"]);function mapType(e){switch(e){case"textarea":return"textarea";case"select":case"dropdown":return"select";case"radio":return"radio";case"checkbox":case"checkboxes":return"checkbox";case"email":return"email";case"number":return"number";case"date":return"date";case"tel":case"phone":return"tel";default:return"text"}}function buildBlueprint(e){const r={};return(e||[]).forEach(t=>{!t||!t.name||SKIP_TYPES.has(t.type)||(r[t.name]={type:mapType(t.type),label:t.label||t.name,required:!!t.required,options:t.options})}),r}function toast(e,r){typeof E<"u"&&E&&typeof E.toast=="function"&&E.toast(e,{type:r||"info"})}function clear(e){for(;e.firstChild;)e.removeChild(e.firstChild)}function showState(e,r,t){clear(e);const o=document.createElement("div");o.className="survey-state survey-state-"+(t||"info"),o.textContent=r,e.appendChild(o)}async function submitResponse(e,r,t){if(typeof H<"u"&&H&&typeof H.post=="function")return H.post("/api/plugins/surveys/submit/"+e,{token:r,data:t});const o=await fetch("/api/plugins/surveys/submit/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r,data:t})}),s=await o.json().catch(()=>({}));if(!o.ok){const n=new Error(s.error||"Submission failed");throw n.body=s,n}return s}async function renderForm(e,r,t,o){let s;try{const i=await fetch("/api/forms/"+r+"/public");if(!i.ok)throw new Error("Form not found");s=await i.json()}catch{showState(e,"This survey form could not be loaded.","error");return}const n=s.fields||[],u=s.settings||{},c=buildBlueprint(n),f=async i=>{try{const a=await submitResponse(t,o,i||{});showState(e,a.message||"Thank you for completing this survey.","success"),toast(a.message||"Response recorded","success")}catch(a){const d=a.body&&a.body.error||a.message||"Submission failed";toast(d,"error"),showState(e,d,"error")}};clear(e),typeof Domma<"u"&&Domma.forms&&typeof Domma.forms.render=="function"?Domma.forms.render(e,c,{},{submitText:u.submitText||"Submit",onSubmit:f}):showState(e,"This survey requires JavaScript components that failed to load.","error")}function startMount(e){const r=e.getAttribute("data-survey-slug"),t=e.getAttribute("data-form-slug");if(!r||!t){showState(e,"This survey is misconfigured.","error");return}let s=new URLSearchParams(window.location.search).get("t");if(s){renderForm(e,t,r,s);return}const n=document.getElementById("survey-code");if(!n){showState(e,"Please use the personalised link from your invitation, or enter your code via the survey page.","info");return}n.style.display="",showState(e,"Enter your code above to begin.","info"),n.addEventListener("submit",u=>{u.preventDefault();const c=n.querySelector('[name="code"]');s=c?c.value.trim():"",s&&(n.style.display="none",clear(e),renderForm(e,t,r,s))})}function start(){const e=new Set,r=document.getElementById("survey-mount");r&&e.add(r),document.querySelectorAll(".survey-mount").forEach(t=>e.add(t)),e.forEach(startMount)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",start):start();
@@ -1,63 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en-GB">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>{{title}}</title>
7
- <meta name="robots" content="noindex">
8
-
9
- <!-- DommaJS CSS -->
10
- <link rel="stylesheet" href="/dist/domma/domma.css">
11
- <link rel="stylesheet" href="/dist/domma/grid.css">
12
- <link rel="stylesheet" href="/dist/domma/elements.css">
13
- <link rel="stylesheet" href="/dist/domma/themes/domma-themes.css">
14
-
15
- <!-- Site CSS -->
16
- <link rel="stylesheet" href="/public/css/site.css">
17
- <link rel="stylesheet" href="/public/css/forms.css">
18
-
19
- <!-- Survey CSS -->
20
- <link rel="stylesheet" href="/plugins/surveys/public/survey.css">
21
- </head>
22
- <body class="dm-cloaked">
23
-
24
- <main class="site-main">
25
- <article class="site-content">
26
- <div class="container survey-container">
27
- <h1 class="survey-title">{{title}}</h1>
28
-
29
- <form id="survey-code" class="survey-code" style="display:none">
30
- <label for="survey-code-input">Enter your code</label>
31
- <div class="survey-code-row">
32
- <input id="survey-code-input" name="code" type="text" placeholder="Enter your code" autocomplete="off">
33
- <button type="submit" class="btn btn-primary">Start</button>
34
- </div>
35
- </form>
36
-
37
- <div class="survey-intro">{{intro}}</div>
38
-
39
- <div id="survey-mount" data-survey-slug="{{slug}}" data-form-slug="{{formSlug}}"></div>
40
-
41
- <div class="survey-footer">{{footer}}</div>
42
- </div>
43
- </article>
44
- </main>
45
-
46
- <!-- DOMPurify - must load before DommaJS -->
47
- <script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
48
-
49
- <!-- DommaJS -->
50
- <script src="/dist/domma/domma.min.js"></script>
51
- <script>
52
- if (window.Domma && typeof window.Domma.init === 'function') {
53
- window.Domma.init();
54
- }
55
- </script>
56
-
57
- <!-- Core Forms (logic engine must load before renderer) -->
58
- <script src="/public/js/form-logic-engine.js"></script>
59
-
60
- <!-- Survey client -->
61
- <script type="module" src="/plugins/surveys/public/survey.mjs"></script>
62
- </body>
63
- </html>
@@ -1,86 +0,0 @@
1
- <div class="view-header">
2
- <h1><span data-icon="sun"></span> Theme Switcher</h1>
3
- <div>
4
- <button id="save-settings-btn" class="btn btn-primary">
5
- <span data-icon="save"></span> Save
6
- </button>
7
- </div>
8
- </div>
9
-
10
- <div class="row mb-3">
11
- <div class="col">
12
- <div class="card">
13
- <div class="card-header"><strong>Disc Position &amp; Offset</strong></div>
14
- <div class="card-body">
15
-
16
- <div class="row" style="align-items:flex-start;gap:0;">
17
-
18
- <!-- Position picker -->
19
- <div class="col-auto" style="margin-right:1.5rem;margin-bottom:1rem;">
20
- <label class="form-label" style="margin-bottom:.6rem;">Position</label>
21
- <div id="ts-pos-grid"
22
- style="display:grid;grid-template-columns:1fr 1fr;gap:2px;border:1px solid var(--border-color,rgba(255,255,255,.1));border-radius:10px;overflow:hidden;background:var(--border-color,rgba(255,255,255,.07));width:fit-content;">
23
-
24
- <div class="ts-pos-btn" data-pos="top-left"
25
- style="display:flex;flex-direction:column;align-items:flex-start;padding:.55rem .8rem;cursor:pointer;font-size:.78rem;color:var(--text-muted,#888);border-right:1px solid var(--border-color,rgba(255,255,255,.08));border-bottom:1px solid var(--border-color,rgba(255,255,255,.08));gap:.05rem;user-select:none;transition:background .15s,color .15s;min-width:84px;">
26
- <span style="font-size:.6rem;opacity:.55;letter-spacing:.06em;text-transform:uppercase;line-height:1.2;">Top</span>
27
- <span style="font-weight:600;">↖ Left</span>
28
- </div>
29
- <div class="ts-pos-btn" data-pos="top-right"
30
- style="display:flex;flex-direction:column;align-items:flex-end;padding:.55rem .8rem;cursor:pointer;font-size:.78rem;color:var(--text-muted,#888);border-bottom:1px solid var(--border-color,rgba(255,255,255,.08));gap:.05rem;user-select:none;transition:background .15s,color .15s;min-width:84px;">
31
- <span style="font-size:.6rem;opacity:.55;letter-spacing:.06em;text-transform:uppercase;line-height:1.2;">Top</span>
32
- <span style="font-weight:600;">Right ↗</span>
33
- </div>
34
-
35
- <div class="ts-pos-btn" data-pos="left-center"
36
- style="display:flex;flex-direction:column;align-items:flex-start;padding:.55rem .8rem;cursor:pointer;font-size:.78rem;color:var(--text-muted,#888);border-right:1px solid var(--border-color,rgba(255,255,255,.08));border-bottom:1px solid var(--border-color,rgba(255,255,255,.08));gap:.05rem;user-select:none;transition:background .15s,color .15s;min-width:84px;">
37
- <span style="font-size:.6rem;opacity:.55;letter-spacing:.06em;text-transform:uppercase;line-height:1.2;">Side</span>
38
- <span style="font-weight:600;">← Left</span>
39
- </div>
40
- <div class="ts-pos-btn" data-pos="right-center"
41
- style="display:flex;flex-direction:column;align-items:flex-end;padding:.55rem .8rem;cursor:pointer;font-size:.78rem;color:var(--text-muted,#888);border-bottom:1px solid var(--border-color,rgba(255,255,255,.08));gap:.05rem;user-select:none;transition:background .15s,color .15s;min-width:84px;">
42
- <span style="font-size:.6rem;opacity:.55;letter-spacing:.06em;text-transform:uppercase;line-height:1.2;">Side</span>
43
- <span style="font-weight:600;">Right →</span>
44
- </div>
45
-
46
- <div class="ts-pos-btn" data-pos="bottom-left"
47
- style="display:flex;flex-direction:column;align-items:flex-start;padding:.55rem .8rem;cursor:pointer;font-size:.78rem;color:var(--text-muted,#888);border-right:1px solid var(--border-color,rgba(255,255,255,.08));gap:.05rem;user-select:none;transition:background .15s,color .15s;min-width:84px;">
48
- <span style="font-weight:600;">↙ Left</span>
49
- <span style="font-size:.6rem;opacity:.55;letter-spacing:.06em;text-transform:uppercase;line-height:1.2;">Bottom</span>
50
- </div>
51
- <div class="ts-pos-btn" data-pos="bottom-right"
52
- style="display:flex;flex-direction:column;align-items:flex-end;padding:.55rem .8rem;cursor:pointer;font-size:.78rem;color:var(--text-muted,#888);gap:.05rem;user-select:none;transition:background .15s,color .15s;min-width:84px;">
53
- <span style="font-weight:600;">Right ↘</span>
54
- <span style="font-size:.6rem;opacity:.55;letter-spacing:.06em;text-transform:uppercase;line-height:1.2;">Bottom</span>
55
- </div>
56
-
57
- </div>
58
- <input type="hidden" id="field-position" value="right-center">
59
- </div>
60
-
61
- <!-- Offset -->
62
- <div class="col-3">
63
- <label class="form-label">Edge offset</label>
64
- <div style="display:flex;flex-direction:column;gap:.55rem;">
65
- <div style="display:flex;align-items:center;gap:.4rem;">
66
- <span style="font-size:.75rem;color:var(--text-muted,#888);width:22px;">H</span>
67
- <input id="field-offset-x" type="number" class="form-input" min="0" max="500"
68
- placeholder="32" style="max-width:80px;">
69
- <span class="text-muted" style="font-size:.8rem;">px</span>
70
- </div>
71
- <div style="display:flex;align-items:center;gap:.4rem;">
72
- <span style="font-size:.75rem;color:var(--text-muted,#888);width:22px;">V</span>
73
- <input id="field-offset-y" type="number" class="form-input" min="0" max="500"
74
- placeholder="32" style="max-width:80px;">
75
- <span class="text-muted" style="font-size:.8rem;">px</span>
76
- </div>
77
- </div>
78
- <p class="form-hint text-muted" style="margin-top:.35rem;font-size:.75rem;">Distance from the
79
- screen edge.</p>
80
- </div>
81
-
82
- </div>
83
- </div>
84
- </div>
85
- </div>
86
- </div>
@@ -1,65 +0,0 @@
1
- /**
2
- * Theme Switcher - Admin Settings View
3
- */
4
- import {apiRequest} from '/admin/js/api.js';
5
-
6
- export const themeSwitcherView = {
7
- templateUrl: '/plugins/theme-switcher/admin/templates/theme-switcher.html',
8
-
9
- async onMount($container) {
10
- let settings = {};
11
- try {
12
- settings = await apiRequest('/plugins/theme-switcher/settings');
13
- } catch {
14
- E.toast('Could not load settings.', {type: 'error'});
15
- }
16
-
17
- const fieldPos = document.getElementById('field-position');
18
- const fieldOffX = document.getElementById('field-offset-x');
19
- const fieldOffY = document.getElementById('field-offset-y');
20
- const saveBtn = document.getElementById('save-settings-btn');
21
- const grid = document.getElementById('ts-pos-grid');
22
-
23
- if (fieldPos) fieldPos.value = settings.position || 'right-center';
24
- if (fieldOffX) fieldOffX.value = settings.offsetX ?? 32;
25
- if (fieldOffY) fieldOffY.value = settings.offsetY ?? 32;
26
-
27
- function updatePosBtns(pos) {
28
- document.querySelectorAll('.ts-pos-btn').forEach(function (btn) {
29
- const active = btn.getAttribute('data-pos') === pos;
30
- btn.style.background = active ? 'var(--dm-primary, #5b8cff)' : '';
31
- btn.style.color = active ? '#fff' : '';
32
- });
33
- }
34
-
35
- updatePosBtns(settings.position || 'right-center');
36
-
37
- if (grid) {
38
- grid.addEventListener('click', function (e) {
39
- const btn = e.target.closest('[data-pos]');
40
- if (!btn) return;
41
- const pos = btn.getAttribute('data-pos');
42
- if (fieldPos) fieldPos.value = pos;
43
- updatePosBtns(pos);
44
- });
45
- }
46
-
47
- if (saveBtn) {
48
- saveBtn.addEventListener('click', async function () {
49
- const data = {
50
- position: fieldPos ? fieldPos.value : 'right-center',
51
- offsetX: fieldOffX ? (parseInt(fieldOffX.value, 10) || 32) : 32,
52
- offsetY: fieldOffY ? (parseInt(fieldOffY.value, 10) || 32) : 32,
53
- };
54
- try {
55
- await apiRequest('/plugins/theme-switcher/settings', {method: 'PUT', body: JSON.stringify(data)});
56
- E.toast('Settings saved.', {type: 'success'});
57
- } catch {
58
- E.toast('Failed to save settings.', {type: 'error'});
59
- }
60
- });
61
- }
62
-
63
- Domma.icons.scan();
64
- }
65
- };
@@ -1,10 +0,0 @@
1
- export default {
2
- themes: [
3
- 'ocean', 'forest', 'sunset', 'royal', 'lemon', 'silver',
4
- 'charcoal', 'christmas', 'unicorn', 'dreamy', 'grayve', 'mint', 'wedding'
5
- ],
6
- // position: 'right-center' | 'left-center' | 'bottom-right' | 'bottom-left' | 'top-left' | 'top-right'
7
- position: 'right-center',
8
- offsetX: 32,
9
- offsetY: 32,
10
- };
@@ -1,26 +0,0 @@
1
- /**
2
- * Theme Switcher Plugin
3
- *
4
- * Exposes a public GET /settings endpoint so the injected client script
5
- * can read position/offset without requiring auth.
6
- */
7
- import {getPluginSettings, savePluginState} from '../../server/services/plugins.js';
8
-
9
- export default async function themeSwitcherPlugin(fastify, options) {
10
- const {authenticate, requireAdmin} = options.auth;
11
-
12
- fastify.get('/settings', async () => {
13
- const s = await getPluginSettings('theme-switcher');
14
- return {
15
- position: s.position || 'right-center',
16
- offsetX: s.offsetX ?? 32,
17
- offsetY: s.offsetY ?? 32,
18
- };
19
- });
20
-
21
- fastify.put('/settings', {preHandler: [authenticate, requireAdmin]}, async (request) => {
22
- const body = request.body || {};
23
- savePluginState('theme-switcher', {settings: body});
24
- return {ok: true};
25
- });
26
- }
@@ -1,74 +0,0 @@
1
- {
2
- "name": "theme-switcher",
3
- "displayName": "Theme Switcher",
4
- "version": "1.1.0",
5
- "description": "Floating disc icon with colour theme dots for switching between all Domma themes",
6
- "author": "Domma",
7
- "date": "2026-03-31",
8
- "icon": "sun",
9
- "inject": {
10
- "head": "public/inject-head.html",
11
- "bodyEnd": "public/inject-body.html"
12
- },
13
- "admin": {
14
- "sidebar": [
15
- {
16
- "id": "plugin-theme-switcher",
17
- "text": "Theme Switcher",
18
- "icon": "sun",
19
- "url": "#/plugins/theme-switcher",
20
- "section": "#/plugins/theme-switcher",
21
- "permissions": [
22
- "plugins"
23
- ]
24
- }
25
- ],
26
- "routes": [
27
- {
28
- "path": "/plugins/theme-switcher",
29
- "view": "pluginThemeSwitcher",
30
- "title": "Theme Switcher - Domma CMS"
31
- }
32
- ],
33
- "views": {
34
- "pluginThemeSwitcher": {
35
- "entry": "theme-switcher/admin/views/theme-switcher.js?v=c906bed1",
36
- "exportName": "themeSwitcherView"
37
- }
38
- }
39
- },
40
- "settingsSchema": [
41
- {
42
- "key": "position",
43
- "type": "select",
44
- "label": "Position",
45
- "default": "right-center",
46
- "options": [
47
- {
48
- "value": "right-center",
49
- "label": "Right - Centre"
50
- },
51
- {
52
- "value": "left-center",
53
- "label": "Left - Centre"
54
- },
55
- {
56
- "value": "bottom-right",
57
- "label": "Bottom Right"
58
- },
59
- {
60
- "value": "bottom-left",
61
- "label": "Bottom Left"
62
- }
63
- ]
64
- },
65
- {
66
- "key": "offset",
67
- "type": "number",
68
- "label": "Offset (px)",
69
- "default": 32,
70
- "min": 0,
71
- "max": 200
72
- }
73
- ]
74
- }