domma-cms 0.3.0 → 0.5.0

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 (145) hide show
  1. package/README.md +3 -3
  2. package/admin/css/admin.css +1 -1
  3. package/admin/dist/domma/domma-tools.css +2313 -0
  4. package/admin/dist/domma/domma-tools.min.js +10 -0
  5. package/admin/index.html +4 -0
  6. package/admin/js/api.js +1 -1
  7. package/admin/js/app.js +8 -4
  8. package/admin/js/config/sidebar-config.js +1 -1
  9. package/admin/js/lib/markdown-toolbar.js +18 -10
  10. package/admin/js/templates/action-editor.html +171 -0
  11. package/admin/js/templates/actions-list.html +19 -0
  12. package/admin/js/templates/api-reference.html +1411 -0
  13. package/admin/js/templates/block-editor.html +158 -0
  14. package/admin/js/templates/blocks.html +8 -0
  15. package/admin/js/templates/collection-editor.html +47 -0
  16. package/admin/js/templates/collection-entries.html +3 -0
  17. package/admin/js/templates/collections.html +51 -4
  18. package/admin/js/templates/documentation.html +258 -0
  19. package/{plugins/form-builder/admin → admin/js}/templates/form-editor.html +238 -199
  20. package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
  21. package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
  22. package/admin/js/templates/login.html +29 -4
  23. package/admin/js/templates/my-profile.html +17 -0
  24. package/admin/js/templates/page-editor.html +39 -0
  25. package/admin/js/templates/pages.html +6 -1
  26. package/admin/js/templates/pro-docs.html +259 -0
  27. package/admin/js/templates/role-editor.html +59 -0
  28. package/admin/js/templates/roles.html +10 -0
  29. package/admin/js/templates/settings.html +123 -21
  30. package/admin/js/templates/tutorials.html +81 -0
  31. package/admin/js/templates/user-editor.html +7 -0
  32. package/admin/js/templates/users.html +3 -26
  33. package/admin/js/templates/view-editor.html +201 -0
  34. package/admin/js/templates/view-preview.html +51 -0
  35. package/admin/js/templates/views-list.html +19 -0
  36. package/admin/js/views/action-editor.js +1 -0
  37. package/admin/js/views/actions-list.js +1 -0
  38. package/admin/js/views/api-reference.js +1 -0
  39. package/admin/js/views/block-editor.js +8 -0
  40. package/admin/js/views/blocks.js +4 -0
  41. package/admin/js/views/collection-editor.js +3 -3
  42. package/admin/js/views/collection-entries.js +1 -1
  43. package/admin/js/views/collections.js +1 -1
  44. package/admin/js/views/dashboard.js +1 -1
  45. package/admin/js/views/form-editor.js +8 -0
  46. package/admin/js/views/form-submissions.js +1 -0
  47. package/admin/js/views/forms.js +1 -0
  48. package/admin/js/views/index.js +1 -1
  49. package/admin/js/views/login.js +2 -2
  50. package/admin/js/views/media.js +1 -1
  51. package/admin/js/views/my-profile.js +1 -0
  52. package/admin/js/views/page-editor.js +34 -15
  53. package/admin/js/views/pages.js +5 -5
  54. package/admin/js/views/plugins.js +10 -10
  55. package/admin/js/views/pro-docs.js +1 -0
  56. package/admin/js/views/role-editor.js +1 -0
  57. package/admin/js/views/roles.js +4 -0
  58. package/admin/js/views/settings.js +3 -1
  59. package/admin/js/views/user-editor.js +1 -1
  60. package/admin/js/views/users.js +4 -7
  61. package/admin/js/views/view-editor.js +1 -0
  62. package/admin/js/views/view-preview.js +1 -0
  63. package/admin/js/views/views-list.js +1 -0
  64. package/bin/cli.js +1 -1
  65. package/config/auth.json +1 -0
  66. package/config/connections.json.bak +9 -0
  67. package/config/connections.json.example +9 -0
  68. package/config/plugins.json +19 -29
  69. package/config/server.json +6 -6
  70. package/config/site.json +12 -2
  71. package/package.json +24 -10
  72. package/plugins/example-analytics/stats.json +17 -12
  73. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  74. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  75. package/plugins/theme-roller/config.js +1 -0
  76. package/plugins/theme-roller/plugin.js +233 -0
  77. package/plugins/theme-roller/plugin.json +31 -0
  78. package/plugins/theme-roller/public/active-theme.css +0 -0
  79. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  80. package/public/css/forms.css +1 -0
  81. package/public/css/site.css +1 -1
  82. package/public/js/forms.js +1 -0
  83. package/public/js/site.js +1 -1
  84. package/scripts/build.js +194 -129
  85. package/scripts/pro.js +254 -0
  86. package/scripts/reset.js +33 -8
  87. package/scripts/seed.js +343 -78
  88. package/scripts/setup.js +1 -0
  89. package/server/middleware/auth.js +136 -120
  90. package/server/routes/api/actions.js +200 -0
  91. package/server/routes/api/auth.js +292 -146
  92. package/server/routes/api/blocks.js +84 -0
  93. package/server/routes/api/collections.js +79 -27
  94. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +483 -505
  95. package/server/routes/api/layouts.js +49 -39
  96. package/server/routes/api/media.js +118 -92
  97. package/server/routes/api/navigation.js +40 -36
  98. package/server/routes/api/pages.js +132 -118
  99. package/server/routes/api/plugins.js +6 -3
  100. package/server/routes/api/settings.js +104 -88
  101. package/server/routes/api/users.js +27 -19
  102. package/server/routes/api/views.js +148 -0
  103. package/server/routes/public.js +124 -108
  104. package/server/server.js +269 -181
  105. package/server/services/actions.js +387 -0
  106. package/server/services/adapterRegistry.js +98 -0
  107. package/server/services/adapters/FileAdapter.js +192 -0
  108. package/server/services/adapters/MongoAdapter.js +220 -0
  109. package/server/services/blocks.js +162 -0
  110. package/server/services/collections.js +74 -86
  111. package/server/services/connectionManager.js +102 -0
  112. package/server/services/content.js +312 -307
  113. package/server/services/email.js +126 -0
  114. package/server/services/forms.js +173 -0
  115. package/server/services/markdown.js +1378 -747
  116. package/server/services/permissionRegistry.js +173 -0
  117. package/server/services/presetCollections.js +251 -0
  118. package/server/services/renderer.js +75 -1
  119. package/server/services/roles.js +227 -0
  120. package/server/services/rowAccess.js +104 -0
  121. package/server/services/userProfiles.js +199 -0
  122. package/server/services/users.js +281 -212
  123. package/server/services/views.js +280 -0
  124. package/server/templates/page.html +119 -113
  125. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  126. package/plugins/form-builder/admin/views/form-editor.js +0 -1444
  127. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  128. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  129. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  130. package/plugins/form-builder/config.js +0 -9
  131. package/plugins/form-builder/data/forms/consent.json +0 -104
  132. package/plugins/form-builder/data/forms/contact-details.json +0 -99
  133. package/plugins/form-builder/data/forms/contacts.json +0 -66
  134. package/plugins/form-builder/data/forms/feedback.json +0 -130
  135. package/plugins/form-builder/data/submissions/consent.json +0 -13
  136. package/plugins/form-builder/data/submissions/contact-details.json +0 -1
  137. package/plugins/form-builder/data/submissions/contacts.json +0 -26
  138. package/plugins/form-builder/data/submissions/feedback.json +0 -1
  139. package/plugins/form-builder/plugin.json +0 -52
  140. package/plugins/form-builder/public/inject-body.html +0 -352
  141. package/plugins/form-builder/public/inject-head.html +0 -58
  142. package/plugins/form-builder/public/package.json +0 -1
  143. package/scripts/copy-domma.js +0 -48
  144. package/server/services/userTypes.js +0 -167
  145. /package/{plugins/form-builder/public → public/js}/form-logic-engine.js +0 -0
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Views Service (Pro — requires MongoDB)
3
+ *
4
+ * View configs are stored in MongoDB `cms__views` on the 'default' connection.
5
+ * The `connection` field in each view config specifies which connection to use
6
+ * when executing the aggregation pipeline.
7
+ *
8
+ * Allowed aggregation stage types (Phase 1):
9
+ * $match, $lookup, $sort, $project, $unwind, $limit, $skip, $count,
10
+ * $addFields, $group
11
+ *
12
+ * Forbidden stage types:
13
+ * $out, $merge, $function, $accumulator, $graphLookup
14
+ */
15
+ import {v4 as uuidv4} from 'uuid';
16
+ import {buildRowLevelMatch} from './rowAccess.js';
17
+
18
+ /** MongoDB collection where view configs are stored. */
19
+ const VIEWS_COLLECTION = 'cms__views';
20
+
21
+ /** Prefix applied to CMS data collections — mirrors MongoAdapter.PREFIX. */
22
+ const COLLECTION_PREFIX = 'cms_';
23
+
24
+ const ALLOWED_STAGES = new Set([
25
+ '$match', '$lookup', '$sort', '$project',
26
+ '$unwind', '$limit', '$skip', '$count',
27
+ '$addFields', '$group'
28
+ ]);
29
+
30
+ const FORBIDDEN_STAGES = new Set([
31
+ '$out', '$merge', '$function', '$accumulator', '$graphLookup'
32
+ ]);
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Internal helpers
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /**
39
+ * Get the Db instance for storing/reading view configs ('default' connection).
40
+ *
41
+ * @returns {Promise<import('mongodb').Db>}
42
+ * @throws {Error} If MongoDB is not configured
43
+ */
44
+ async function getMetaDb() {
45
+ try {
46
+ const { getDb } = await import('./connectionManager.js');
47
+ return getDb('default');
48
+ } catch {
49
+ throw new Error('Views require a MongoDB connection. Configure a "default" connection in pro mode.');
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Slugify a string to a URL-safe identifier.
55
+ *
56
+ * @param {string} str
57
+ * @returns {string}
58
+ */
59
+ function slugify(str) {
60
+ return str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
61
+ }
62
+
63
+ /**
64
+ * Validate that all pipeline stages use allowed types.
65
+ *
66
+ * @param {object[]} stages
67
+ * @throws {Error} If a stage type is forbidden or not in the allowlist
68
+ */
69
+ function validateStages(stages) {
70
+ for (const stage of stages) {
71
+ if (!stage.type) throw new Error('Pipeline stage missing "type"');
72
+ if (FORBIDDEN_STAGES.has(stage.type)) {
73
+ throw new Error(`Stage type "${stage.type}" is not permitted for security reasons`);
74
+ }
75
+ if (!ALLOWED_STAGES.has(stage.type)) {
76
+ throw new Error(`Stage type "${stage.type}" is not in the allowed list`);
77
+ }
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Convert stage descriptors into MongoDB aggregation stage objects.
83
+ *
84
+ * @param {object[]} stages
85
+ * @returns {object[]}
86
+ */
87
+ function buildPipelineStages(stages) {
88
+ return stages.map(stage => ({ [stage.type]: stage.config }));
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // CRUD
93
+ // ---------------------------------------------------------------------------
94
+
95
+ /**
96
+ * List all view configs.
97
+ *
98
+ * @returns {Promise<object[]>}
99
+ * @throws {Error} If MongoDB is not available
100
+ */
101
+ export async function listViews() {
102
+ const db = await getMetaDb();
103
+ return db.collection(VIEWS_COLLECTION)
104
+ .find({}, { projection: { _id: 0 } })
105
+ .sort({ 'meta.createdAt': -1 })
106
+ .toArray();
107
+ }
108
+
109
+ /**
110
+ * Get a single view config by slug.
111
+ *
112
+ * @param {string} slug
113
+ * @returns {Promise<object|null>}
114
+ * @throws {Error} If MongoDB is not available
115
+ */
116
+ export async function getView(slug) {
117
+ const db = await getMetaDb();
118
+ const doc = await db.collection(VIEWS_COLLECTION).findOne({ slug }, { projection: { _id: 0 } });
119
+ return doc || null;
120
+ }
121
+
122
+ /**
123
+ * Create a new view config.
124
+ *
125
+ * @param {object} data
126
+ * @param {string} data.title
127
+ * @param {string} [data.slug]
128
+ * @param {string} [data.description]
129
+ * @param {string} [data.connection='default']
130
+ * @param {object} data.pipeline - { source: string, stages: object[] }
131
+ * @param {object} [data.display] - { mode, columns, pageSize }
132
+ * @param {object} [data.access] - { roles, public }
133
+ * @param {string|null} [userId]
134
+ * @returns {Promise<object>} Created view config
135
+ * @throws {Error} If slug already exists or validation fails
136
+ */
137
+ export async function createView(data, userId = null) {
138
+ const db = await getMetaDb();
139
+ const { title, description = '', connection = 'default', pipeline, display, access } = data;
140
+
141
+ if (!title) throw new Error('title is required');
142
+ if (!pipeline?.source) throw new Error('pipeline.source is required');
143
+
144
+ const slug = data.slug ? slugify(data.slug) : slugify(title);
145
+ if (!slug) throw new Error('Could not derive a slug from the title');
146
+
147
+ const existing = await db.collection(VIEWS_COLLECTION).findOne({ slug });
148
+ if (existing) throw new Error(`A view with slug "${slug}" already exists`);
149
+
150
+ const stages = pipeline.stages || [];
151
+ validateStages(stages);
152
+
153
+ const now = new Date().toISOString();
154
+ const view = {
155
+ id: uuidv4(),
156
+ slug,
157
+ title: title.trim(),
158
+ description: description.trim(),
159
+ connection,
160
+ pipeline: { source: pipeline.source, stages },
161
+ display: {
162
+ mode: display?.mode || 'table',
163
+ columns: display?.columns || [],
164
+ pageSize: display?.pageSize || 25,
165
+ block: display?.block || ''
166
+ },
167
+ access: {
168
+ roles: access?.roles || ['admin'],
169
+ public: access?.public || false,
170
+ rowLevel: access?.rowLevel || null
171
+ },
172
+ meta: { createdAt: now, updatedAt: now, createdBy: userId }
173
+ };
174
+
175
+ await db.collection(VIEWS_COLLECTION).insertOne({ ...view });
176
+ return view;
177
+ }
178
+
179
+ /**
180
+ * Update an existing view config.
181
+ *
182
+ * @param {string} slug
183
+ * @param {object} data - Partial view fields to merge
184
+ * @returns {Promise<object>} Updated view config
185
+ * @throws {Error} If view not found or validation fails
186
+ */
187
+ export async function updateView(slug, data) {
188
+ const db = await getMetaDb();
189
+ const existing = await db.collection(VIEWS_COLLECTION).findOne({ slug });
190
+ if (!existing) throw new Error(`View "${slug}" not found`);
191
+
192
+ const stages = data.pipeline?.stages ?? existing.pipeline?.stages ?? [];
193
+ validateStages(stages);
194
+
195
+ const { _id, id, meta, ...rest } = data;
196
+ const updated = {
197
+ ...existing,
198
+ ...rest,
199
+ slug,
200
+ pipeline: {
201
+ ...existing.pipeline,
202
+ ...data.pipeline,
203
+ stages
204
+ },
205
+ meta: { ...existing.meta, updatedAt: new Date().toISOString() }
206
+ };
207
+
208
+ await db.collection(VIEWS_COLLECTION).replaceOne({ slug }, { ...updated });
209
+ const { _id: _stripped, ...result } = updated;
210
+ return result;
211
+ }
212
+
213
+ /**
214
+ * Delete a view config.
215
+ *
216
+ * @param {string} slug
217
+ * @returns {Promise<void>}
218
+ * @throws {Error} If view not found
219
+ */
220
+ export async function deleteView(slug) {
221
+ const db = await getMetaDb();
222
+ const result = await db.collection(VIEWS_COLLECTION).deleteOne({ slug });
223
+ if (result.deletedCount === 0) throw new Error(`View "${slug}" not found`);
224
+ }
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // Execution
228
+ // ---------------------------------------------------------------------------
229
+
230
+ /**
231
+ * Execute a view's aggregation pipeline and return paginated results.
232
+ *
233
+ * @param {string} slug
234
+ * @param {object} [opts]
235
+ * @param {number} [opts.page=1]
236
+ * @param {number} [opts.limit=25]
237
+ * @param {object|null} [opts.user=null] - Authenticated user (for row-level filtering)
238
+ * @returns {Promise<{ results: object[], total: number, page: number, limit: number }>}
239
+ * @throws {Error} If view not found, connection unavailable, or stage validation fails
240
+ */
241
+ export async function executeView(slug, {page = 1, limit = 25, user = null} = {}) {
242
+ const view = await getView(slug);
243
+ if (!view) throw new Error(`View "${slug}" not found`);
244
+
245
+ const { getDb } = await import('./connectionManager.js');
246
+ const db = getDb(view.connection || 'default');
247
+
248
+ const sourceCollection = `${COLLECTION_PREFIX}${view.pipeline.source}`;
249
+ const stages = view.pipeline?.stages || [];
250
+ validateStages(stages);
251
+
252
+ const pipeline = buildPipelineStages(stages);
253
+
254
+ // Prepend row-level $match if configured
255
+ const rowFilter = buildRowLevelMatch(user, view.access?.rowLevel);
256
+ if (rowFilter) {
257
+ pipeline.unshift({$match: rowFilter});
258
+ }
259
+
260
+ // Count total without pagination
261
+ const countPipeline = [...pipeline, { $count: 'total' }];
262
+ const countResult = await db.collection(sourceCollection)
263
+ .aggregate(countPipeline)
264
+ .toArray();
265
+ const total = countResult[0]?.total ?? 0;
266
+
267
+ // Add pagination stages
268
+ const offset = (page - 1) * limit;
269
+ pipeline.push({ $skip: offset });
270
+ pipeline.push({ $limit: limit });
271
+
272
+ const docs = await db.collection(sourceCollection)
273
+ .aggregate(pipeline, { allowDiskUse: true })
274
+ .toArray();
275
+
276
+ // Strip MongoDB internal _id from all results
277
+ const results = docs.map(({ _id, ...rest }) => rest);
278
+
279
+ return { results, total, page, limit };
280
+ }
@@ -1,113 +1,119 @@
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>{{seoTitle}}</title>
7
- <meta name="description" content="{{seoDescription}}">
8
- {{#if ogImage}}<meta property="og:image" content="{{ogImage}}">{{/if}}
9
-
10
- <!-- Fonts -->
11
- {{#if fontLink}}{{fontLink}}{{/if}}
12
-
13
- <!-- DommaJS CSS -->
14
- <link rel="stylesheet" href="/dist/domma/domma.css">
15
- <link rel="stylesheet" href="/dist/domma/grid.css">
16
- <link rel="stylesheet" href="/dist/domma/elements.css">
17
- <link rel="stylesheet" href="/dist/domma/themes/domma-themes.css">
18
-
19
- <!-- Site CSS -->
20
- <link rel="stylesheet" href="/public/css/site.css">
21
-
22
- <!-- Font overrides -->
23
- {{fontStyleTag}}
24
-
25
- <!-- Plugin head injection -->
26
- {{headInject}}
27
-
28
- <!-- Late head injection — custom CSS always loads last so it can override everything -->
29
- {{headInjectLate}}
30
- </head>
31
- <body class="dm-cloaked dm-theme-{{theme}}" data-layout="{{layout}}">
32
-
33
- {{#if showNavbar}}
34
- <nav id="site-navbar"></nav>
35
- {{/if}}
36
-
37
- <main class="site-main {{#if showSidebar}}with-sidebar{{/if}}">
38
- {{#if showSidebar}}
39
- <aside id="site-sidebar" class="site-sidebar"></aside>
40
- {{/if}}
41
-
42
- <article class="site-content">
43
- <div class="container">
44
- <div class="page-body">
45
- {{html}}
46
- </div>
47
- </div>
48
- </article>
49
- </main>
50
-
51
- {{#if showFooter}}
52
- <footer id="site-footer" class="page-footer"></footer>
53
- {{/if}}
54
-
55
- <!-- DOMPurify - must load before DommaJS -->
56
- <script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
57
-
58
- <!-- DommaJS -->
59
- <script src="/dist/domma/domma.min.js"></script>
60
-
61
- <!-- Initialise DommaJS before module loads -->
62
- <script>
63
- (function () {
64
- var _stored;
65
- try {
66
- _stored = JSON.parse(localStorage.getItem('domma:reduced_motion'));
67
- } catch (e) {
68
- }
69
- if (_stored === true) {
70
- document.documentElement.classList.add('dm-reduced-motion');
71
- }
72
- // Override window.matchMedia so Domma JS effects (scribe, breathe, etc.)
73
- // respect the stored preference. When explicitly set to false the user is
74
- // overriding the OS "reduce" preference to allow motion on this site.
75
- if (_stored !== null && _stored !== undefined && window.matchMedia) {
76
- var _orig = window.matchMedia.bind(window);
77
- window.matchMedia = function (q) {
78
- if (q === '(prefers-reduced-motion: reduce)') {
79
- return {
80
- matches: !!_stored, media: q, onchange: null,
81
- addListener: function () {
82
- }, removeListener: function () {
83
- },
84
- addEventListener: function () {
85
- }, removeEventListener: function () {
86
- },
87
- dispatchEvent: function () {
88
- return false;
89
- }
90
- };
91
- }
92
- return _orig(q);
93
- };
94
- }
95
- }());
96
- if (window.Domma && typeof window.Domma.init === 'function') {
97
- window.Domma.init();
98
- }
99
- if (window.Domma && window.Domma.theme) {
100
- window.Domma.theme.init({ theme: '{{theme}}', persist: false });
101
- }
102
- window.__CMS_NAV__ = {{navJson}};
103
- window.__CMS_SITE__ = {{siteJson}};
104
- {{dconfigScript}}
105
- </script>
106
-
107
- <!-- Site initialisation -->
108
- <script src="/public/js/site.js" type="module"></script>
109
-
110
- <!-- Plugin body-end injection -->
111
- {{bodyEndInject}}
112
- </body>
113
- </html>
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>{{seoTitle}}</title>
7
+ <meta name="description" content="{{seoDescription}}">
8
+ {{#if ogImage}}<meta property="og:image" content="{{ogImage}}">{{/if}}
9
+
10
+ <!-- Fonts -->
11
+ {{#if fontLink}}{{fontLink}}{{/if}}
12
+
13
+ <!-- DommaJS CSS -->
14
+ <link rel="stylesheet" href="/dist/domma/domma.css">
15
+ <link rel="stylesheet" href="/dist/domma/grid.css">
16
+ <link rel="stylesheet" href="/dist/domma/elements.css">
17
+ <link rel="stylesheet" href="/dist/domma/themes/domma-themes.css">
18
+
19
+ <!-- Site CSS -->
20
+ <link rel="stylesheet" href="/public/css/site.css">
21
+ <link rel="stylesheet" href="/public/css/forms.css">
22
+
23
+ <!-- Font overrides -->
24
+ {{fontStyleTag}}
25
+
26
+ <!-- Plugin head injection -->
27
+ {{headInject}}
28
+
29
+ <!-- Late head injection — custom CSS always loads last so it can override everything -->
30
+ {{headInjectLate}}
31
+ </head>
32
+ <body class="dm-cloaked dm-theme-{{theme}}" data-layout="{{layout}}">
33
+
34
+ {{#if showNavbar}}
35
+ <nav id="site-navbar"></nav>
36
+ {{/if}}
37
+
38
+ <main class="site-main {{#if showSidebar}}with-sidebar{{/if}}">
39
+ {{#if showSidebar}}
40
+ <aside id="site-sidebar" class="site-sidebar"></aside>
41
+ {{/if}}
42
+
43
+ <article class="site-content">
44
+ <div class="container">
45
+ {{breadcrumbsHtml}}
46
+ <div class="page-body">
47
+ {{html}}
48
+ </div>
49
+ </div>
50
+ </article>
51
+ </main>
52
+
53
+ {{#if showFooter}}
54
+ <footer id="site-footer" class="page-footer"></footer>
55
+ {{/if}}
56
+
57
+ <!-- DOMPurify - must load before DommaJS -->
58
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
59
+
60
+ <!-- DommaJS -->
61
+ <script src="/dist/domma/domma.min.js"></script>
62
+
63
+ <!-- Initialise DommaJS before module loads -->
64
+ <script>
65
+ (function () {
66
+ var _stored;
67
+ try {
68
+ _stored = JSON.parse(localStorage.getItem('domma:reduced_motion'));
69
+ } catch (e) {
70
+ }
71
+ if (_stored === true) {
72
+ document.documentElement.classList.add('dm-reduced-motion');
73
+ }
74
+ // Override window.matchMedia so Domma JS effects (scribe, breathe, etc.)
75
+ // respect the stored preference. When explicitly set to false the user is
76
+ // overriding the OS "reduce" preference to allow motion on this site.
77
+ if (_stored !== null && _stored !== undefined && window.matchMedia) {
78
+ var _orig = window.matchMedia.bind(window);
79
+ window.matchMedia = function (q) {
80
+ if (q === '(prefers-reduced-motion: reduce)') {
81
+ return {
82
+ matches: !!_stored, media: q, onchange: null,
83
+ addListener: function () {
84
+ }, removeListener: function () {
85
+ },
86
+ addEventListener: function () {
87
+ }, removeEventListener: function () {
88
+ },
89
+ dispatchEvent: function () {
90
+ return false;
91
+ }
92
+ };
93
+ }
94
+ return _orig(q);
95
+ };
96
+ }
97
+ }());
98
+ if (window.Domma && typeof window.Domma.init === 'function') {
99
+ window.Domma.init();
100
+ }
101
+ if (window.Domma && window.Domma.theme) {
102
+ window.Domma.theme.init({ theme: '{{theme}}', persist: false });
103
+ }
104
+ window.__CMS_NAV__ = {{navJson}};
105
+ window.__CMS_SITE__ = {{siteJson}};
106
+ {{dconfigScript}}
107
+ </script>
108
+
109
+ <!-- Site initialisation -->
110
+ <script src="/public/js/site.js" type="module"></script>
111
+
112
+ <!-- Core Forms (logic engine must load before renderer) -->
113
+ <script src="/public/js/form-logic-engine.js"></script>
114
+ <script src="/public/js/forms.js" type="module"></script>
115
+
116
+ <!-- Plugin body-end injection -->
117
+ {{bodyEndInject}}
118
+ </body>
119
+ </html>
@@ -1,29 +0,0 @@
1
- <div class="view-header">
2
- <h1><span data-icon="settings"></span> Form Builder Settings</h1>
3
- <div style="display:flex;gap:.5rem;">
4
- <a href="#/plugins/form-builder" class="btn btn-ghost btn-sm">
5
- <span data-icon="layout"></span> All Forms
6
- </a>
7
- </div>
8
- </div>
9
-
10
- <div class="card mb-4">
11
- <div class="card-body" style="display:flex;align-items:center;gap:1rem;">
12
- <span data-icon="info" style="flex-shrink:0;"></span>
13
- <div>
14
- <p style="margin:0 0 .25rem;">SMTP and email identity settings have moved to <a href="#/settings" class="link">Site Settings</a>.</p>
15
- <p class="text-muted" style="margin:0;font-size:.85rem;">Configure your mail server there, then use the button below to verify the connection.</p>
16
- </div>
17
- </div>
18
- </div>
19
-
20
- <div class="card">
21
- <div class="card-header"><h2>Test Email</h2></div>
22
- <div class="card-body">
23
- <p class="text-muted" style="margin:0 0 1rem;font-size:.9rem;">Send a test email using the SMTP settings configured in Site Settings to verify your mail server connection.</p>
24
- <button id="test-email-btn" class="btn btn-primary">
25
- <span data-icon="send"></span> Send Test Email
26
- </button>
27
- <p id="test-email-result" class="form-hint" style="margin-top:.75rem;display:none;"></p>
28
- </div>
29
- </div>