domma-cms 0.17.0 → 0.21.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.
- package/CLAUDE.md +39 -3
- package/admin/css/admin.css +1 -1
- package/admin/css/dashboard.css +1 -1
- package/admin/index.html +2 -2
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/lib/card-builder.js +3 -3
- package/admin/js/lib/crud-tutorial.js +1 -0
- package/admin/js/lib/effects-builder.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +6 -6
- package/admin/js/lib/project-context.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +4 -0
- package/admin/js/templates/action-editor.html +7 -0
- package/admin/js/templates/block-editor.html +7 -0
- package/admin/js/templates/collection-editor.html +9 -0
- package/admin/js/templates/dashboard/cache.html +32 -0
- package/admin/js/templates/dashboard.html +4 -0
- package/admin/js/templates/form-editor.html +9 -0
- package/admin/js/templates/menu-editor.html +98 -0
- package/admin/js/templates/menu-locations.html +14 -0
- package/admin/js/templates/menus.html +14 -0
- package/admin/js/templates/page-editor.html +9 -2
- package/admin/js/templates/project-detail.html +50 -0
- package/admin/js/templates/project-editor.html +45 -0
- package/admin/js/templates/project-settings.html +60 -0
- package/admin/js/templates/projects.html +13 -0
- package/admin/js/templates/role-editor.html +11 -0
- package/admin/js/templates/settings.html +26 -0
- package/admin/js/templates/tutorials.html +335 -2
- package/admin/js/templates/view-editor.html +7 -0
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/actions-list.js +1 -1
- package/admin/js/views/block-editor-enhance.js +1 -1
- package/admin/js/views/block-editor.js +8 -8
- package/admin/js/views/blocks.js +2 -2
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/dashboard/widgets/activity-feed.js +1 -1
- package/admin/js/views/dashboard/widgets/cache.js +1 -0
- package/admin/js/views/dashboard/widgets/journeys.js +1 -1
- package/admin/js/views/dashboard/widgets/spike-feed.js +1 -1
- package/admin/js/views/dashboard/widgets/top-pages.js +1 -1
- package/admin/js/views/dashboard.js +1 -1
- package/admin/js/views/form-editor.js +6 -6
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/menu-editor.js +19 -0
- package/admin/js/views/menu-locations.js +1 -0
- package/admin/js/views/menus.js +5 -0
- package/admin/js/views/page-editor.js +41 -36
- package/admin/js/views/pages.js +3 -3
- package/admin/js/views/project-detail.js +4 -0
- package/admin/js/views/project-editor.js +1 -0
- package/admin/js/views/project-settings.js +1 -0
- package/admin/js/views/projects.js +7 -0
- package/admin/js/views/role-editor.js +1 -1
- package/admin/js/views/roles.js +3 -3
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/tutorials.js +1 -1
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +3 -3
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/views-list.js +1 -1
- package/config/cache.json +4 -0
- package/config/cache.json.example +12 -0
- package/config/menu-locations.json +5 -0
- package/config/menus/admin-sidebar.json +185 -0
- package/config/menus/footer.json +33 -0
- package/config/menus/main.json +35 -0
- package/config/menus/sproj-1779696558011-menu.json +17 -0
- package/config/menus/sproj-1779696960337-menu.json +18 -0
- package/config/menus/sproj-1779696985353-menu.json +18 -0
- package/config/site.json +6 -22
- package/package.json +4 -3
- package/plugins/analytics/daily.json +3 -0
- package/plugins/analytics/journeys.json +8 -0
- package/plugins/analytics/lifetime.json +1 -1
- package/public/css/site.css +1 -1
- package/public/js/collection-browser.js +4 -0
- package/public/js/forms.js +1 -1
- package/public/js/site.js +1 -1
- package/server/config.js +12 -1
- package/server/middleware/auth.js +88 -22
- package/server/routes/api/actions.js +58 -5
- package/server/routes/api/auth.js +2 -2
- package/server/routes/api/blocks.js +18 -3
- package/server/routes/api/cache.js +57 -0
- package/server/routes/api/collections.js +201 -8
- package/server/routes/api/forms.js +266 -21
- package/server/routes/api/menu-locations.js +46 -0
- package/server/routes/api/menus.js +115 -0
- package/server/routes/api/navigation.js +2 -0
- package/server/routes/api/pages.js +1 -1
- package/server/routes/api/projects.js +107 -0
- package/server/routes/api/scaffold.js +86 -0
- package/server/routes/api/settings.js +3 -0
- package/server/routes/api/sidebar.js +23 -0
- package/server/routes/api/users.js +32 -7
- package/server/routes/api/views.js +10 -2
- package/server/routes/public.js +88 -7
- package/server/server.js +54 -3
- package/server/services/actions.js +137 -8
- package/server/services/adapters/FileAdapter.js +23 -8
- package/server/services/adapters/MongoAdapter.js +36 -18
- package/server/services/blocks.js +23 -8
- package/server/services/cache/drivers/MemoryDriver.js +118 -0
- package/server/services/cache/drivers/NoneDriver.js +12 -0
- package/server/services/cache/index.js +229 -0
- package/server/services/cache/lru.js +61 -0
- package/server/services/collections.js +102 -12
- package/server/services/content.js +25 -6
- package/server/services/filterEngine.js +281 -0
- package/server/services/forms.js +3 -0
- package/server/services/hooks.js +48 -0
- package/server/services/markdown.js +711 -124
- package/server/services/menus-migration.js +107 -0
- package/server/services/menus.js +422 -0
- package/server/services/permissionRegistry.js +26 -0
- package/server/services/plugins.js +9 -2
- package/server/services/presetCollections.js +22 -0
- package/server/services/projects.js +429 -0
- package/server/services/recipes/contact-list.json +78 -0
- package/server/services/recipes/onboarding.json +426 -0
- package/server/services/references.js +174 -0
- package/server/services/renderer.js +237 -40
- package/server/services/roles.js +6 -1
- package/server/services/rowAccess.js +86 -13
- package/server/services/scaffolder.js +465 -0
- package/server/services/sidebar-migration.js +117 -0
- package/server/services/sitemap.js +112 -0
- package/server/services/userRoles.js +86 -0
- package/server/services/users.js +23 -2
- package/server/services/views.js +19 -4
- package/server/templates/page.html +135 -130
- /package/config/{navigation.json → navigation.json.bak} +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Roles — multi-role support helpers.
|
|
3
|
+
*
|
|
4
|
+
* A user has ONE primary role (`user.role`) and an optional array of
|
|
5
|
+
* additional roles (`user.additionalRoles`). Permission, visibility, and
|
|
6
|
+
* row-access checks consult ALL of them and grant access if any one role
|
|
7
|
+
* satisfies — "any path wins" semantics that matches how most apps actually
|
|
8
|
+
* model multi-membership (you can be both a candidate AND a recruiter).
|
|
9
|
+
*
|
|
10
|
+
* Hierarchical-level comparisons (`canManageUser`, `requireAdmin`, the
|
|
11
|
+
* level-clamp in `checkSingleVisibility`) use `getEffectiveLevel` which
|
|
12
|
+
* returns the LOWEST level number across all the user's roles — i.e. the
|
|
13
|
+
* highest privilege the user has access to. This is what makes "additional
|
|
14
|
+
* admin" meaningfully grant admin-tier access without rewriting the level
|
|
15
|
+
* arithmetic everywhere.
|
|
16
|
+
*
|
|
17
|
+
* Backward compatibility: users with no `additionalRoles` field behave
|
|
18
|
+
* exactly as before — `getEffectiveRoles` returns `[user.role]`, `getEffectiveLevel`
|
|
19
|
+
* returns `getRoleLevel(user.role)`. Existing callers that read `user.role`
|
|
20
|
+
* directly continue to work; the new helpers are opt-in for callers that
|
|
21
|
+
* want the union semantics.
|
|
22
|
+
*/
|
|
23
|
+
import {getRoleLevel} from './roles.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Return all roles a user holds, primary first, deduplicated. Empty array
|
|
27
|
+
* if the user is null/has no role at all.
|
|
28
|
+
*
|
|
29
|
+
* @param {object|null} user
|
|
30
|
+
* @returns {string[]}
|
|
31
|
+
*/
|
|
32
|
+
export function getEffectiveRoles(user) {
|
|
33
|
+
if (!user || !user.role) return [];
|
|
34
|
+
const additional = Array.isArray(user.additionalRoles) ? user.additionalRoles : [];
|
|
35
|
+
const out = [user.role];
|
|
36
|
+
for (const r of additional) {
|
|
37
|
+
if (r && r !== user.role && !out.includes(r)) out.push(r);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Return the effective role level for a user — the LOWEST level number
|
|
44
|
+
* across all roles. Lower number = higher privilege, so the user's most
|
|
45
|
+
* privileged role wins all hierarchical checks.
|
|
46
|
+
*
|
|
47
|
+
* Returns `Infinity` for users with no roles (denies all level-gated checks).
|
|
48
|
+
*
|
|
49
|
+
* @param {object|null} user
|
|
50
|
+
* @returns {number}
|
|
51
|
+
*/
|
|
52
|
+
export function getEffectiveLevel(user) {
|
|
53
|
+
const roles = getEffectiveRoles(user);
|
|
54
|
+
if (!roles.length) return Infinity;
|
|
55
|
+
return Math.min(...roles.map(r => getRoleLevel(r)));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check whether a user holds a specific role (primary or additional).
|
|
60
|
+
* Exact-name match — does NOT walk the hierarchy. For hierarchical "user
|
|
61
|
+
* is at least this level" checks use `getEffectiveLevel` against the target
|
|
62
|
+
* role's level instead.
|
|
63
|
+
*
|
|
64
|
+
* @param {object|null} user
|
|
65
|
+
* @param {string} role
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
export function userHasRole(user, role) {
|
|
69
|
+
if (!user || !role) return false;
|
|
70
|
+
return getEffectiveRoles(user).includes(role);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check whether a user holds ANY of the given roles. Convenience helper
|
|
75
|
+
* for "anyone in this set can do X" patterns — equivalent to OR-ing
|
|
76
|
+
* `userHasRole` over the list.
|
|
77
|
+
*
|
|
78
|
+
* @param {object|null} user
|
|
79
|
+
* @param {string[]} roles
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
export function userHasAnyRole(user, roles) {
|
|
83
|
+
if (!user || !Array.isArray(roles) || !roles.length) return false;
|
|
84
|
+
const userRoles = new Set(getEffectiveRoles(user));
|
|
85
|
+
return roles.some(r => userRoles.has(r));
|
|
86
|
+
}
|
package/server/services/users.js
CHANGED
|
@@ -9,6 +9,7 @@ import bcrypt from 'bcryptjs';
|
|
|
9
9
|
import {v4 as uuidv4} from 'uuid';
|
|
10
10
|
import {config} from '../config.js';
|
|
11
11
|
import {deleteProfile, ensureProfile} from './userProfiles.js';
|
|
12
|
+
import * as cache from './cache/index.js';
|
|
12
13
|
|
|
13
14
|
const USERS_DIR = path.resolve(config.content.usersDir);
|
|
14
15
|
|
|
@@ -122,7 +123,7 @@ export async function getUserByEmail(email) {
|
|
|
122
123
|
* @param {object} data - { name, email, password, role }
|
|
123
124
|
* @returns {Promise<object>} Created user (password stripped)
|
|
124
125
|
*/
|
|
125
|
-
export async function createUser({ name, email, password, role = 'user' }) {
|
|
126
|
+
export async function createUser({ name, email, password, role = 'user', additionalRoles = [], meta, projects }) {
|
|
126
127
|
const existing = await getUserByEmail(email);
|
|
127
128
|
if (existing) throw new Error('A user with that email already exists');
|
|
128
129
|
|
|
@@ -134,10 +135,19 @@ export async function createUser({ name, email, password, role = 'user' }) {
|
|
|
134
135
|
name: name.trim(),
|
|
135
136
|
password: hash,
|
|
136
137
|
role,
|
|
138
|
+
// Additional roles beyond the primary — permission/visibility checks
|
|
139
|
+
// grant access if ANY role satisfies. The primary `role` is what
|
|
140
|
+
// appears in UI badges and is used for hierarchical level comparisons
|
|
141
|
+
// when a single value is needed.
|
|
142
|
+
additionalRoles: Array.isArray(additionalRoles) ? additionalRoles.filter(r => r && r !== role) : [],
|
|
143
|
+
// Project access-scope: when non-empty, the user is restricted to artefacts
|
|
144
|
+
// tagged with one of these project slugs (see canSeeArtefact in projects.js).
|
|
145
|
+
projects: Array.isArray(projects) ? projects.filter(Boolean) : [],
|
|
137
146
|
isActive: true,
|
|
138
147
|
createdAt: now,
|
|
139
148
|
updatedAt: now,
|
|
140
|
-
lastLogin: null
|
|
149
|
+
lastLogin: null,
|
|
150
|
+
...(meta != null && {meta})
|
|
141
151
|
};
|
|
142
152
|
|
|
143
153
|
await writeUserFile(user);
|
|
@@ -169,6 +179,17 @@ export async function updateUser(id, updates) {
|
|
|
169
179
|
};
|
|
170
180
|
|
|
171
181
|
await writeUserFile(updated);
|
|
182
|
+
|
|
183
|
+
// Invalidate per-user cache when the project access-scope changes — downstream
|
|
184
|
+
// request handlers cache scope-filtered responses keyed by `user:<id>`.
|
|
185
|
+
if (updates.projects !== undefined) {
|
|
186
|
+
const before = JSON.stringify(user.projects || []);
|
|
187
|
+
const after = JSON.stringify(updated.projects || []);
|
|
188
|
+
if (before !== after) {
|
|
189
|
+
await cache.invalidateTags([`user:${id}`]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
172
193
|
return stripPassword(updated);
|
|
173
194
|
}
|
|
174
195
|
|
package/server/services/views.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import {v4 as uuidv4} from 'uuid';
|
|
16
16
|
import {buildRowLevelMatch} from './rowAccess.js';
|
|
17
|
+
import * as cache from './cache/index.js';
|
|
17
18
|
|
|
18
19
|
/** MongoDB collection where view configs are stored. */
|
|
19
20
|
const VIEWS_COLLECTION = 'cms__views';
|
|
@@ -136,7 +137,7 @@ export async function getView(slug) {
|
|
|
136
137
|
*/
|
|
137
138
|
export async function createView(data, userId = null) {
|
|
138
139
|
const db = await getMetaDb();
|
|
139
|
-
const { title, description = '', connection = 'default', pipeline, display, access } = data;
|
|
140
|
+
const { title, description = '', connection = 'default', pipeline, display, access, meta: inputMeta } = data;
|
|
140
141
|
|
|
141
142
|
if (!title) throw new Error('title is required');
|
|
142
143
|
if (!pipeline?.source) throw new Error('pipeline.source is required');
|
|
@@ -169,10 +170,16 @@ export async function createView(data, userId = null) {
|
|
|
169
170
|
public: access?.public || false,
|
|
170
171
|
rowLevel: access?.rowLevel || null
|
|
171
172
|
},
|
|
172
|
-
meta: {
|
|
173
|
+
meta: {
|
|
174
|
+
createdAt: now,
|
|
175
|
+
updatedAt: now,
|
|
176
|
+
createdBy: userId,
|
|
177
|
+
...(inputMeta?.project != null && {project: inputMeta.project})
|
|
178
|
+
}
|
|
173
179
|
};
|
|
174
180
|
|
|
175
181
|
await db.collection(VIEWS_COLLECTION).insertOne({ ...view });
|
|
182
|
+
await cache.invalidateTags([`view:${slug}`]);
|
|
176
183
|
return view;
|
|
177
184
|
}
|
|
178
185
|
|
|
@@ -192,7 +199,7 @@ export async function updateView(slug, data) {
|
|
|
192
199
|
const stages = data.pipeline?.stages ?? existing.pipeline?.stages ?? [];
|
|
193
200
|
validateStages(stages);
|
|
194
201
|
|
|
195
|
-
const { _id, id, meta, ...rest } = data;
|
|
202
|
+
const { _id, id, meta: inputMeta, ...rest } = data;
|
|
196
203
|
const updated = {
|
|
197
204
|
...existing,
|
|
198
205
|
...rest,
|
|
@@ -202,10 +209,17 @@ export async function updateView(slug, data) {
|
|
|
202
209
|
...data.pipeline,
|
|
203
210
|
stages
|
|
204
211
|
},
|
|
205
|
-
meta: {
|
|
212
|
+
meta: {
|
|
213
|
+
...existing.meta,
|
|
214
|
+
...(inputMeta && typeof inputMeta === 'object'
|
|
215
|
+
? ('project' in inputMeta ? {project: inputMeta.project} : {})
|
|
216
|
+
: {}),
|
|
217
|
+
updatedAt: new Date().toISOString()
|
|
218
|
+
}
|
|
206
219
|
};
|
|
207
220
|
|
|
208
221
|
await db.collection(VIEWS_COLLECTION).replaceOne({ slug }, { ...updated });
|
|
222
|
+
await cache.invalidateTags([`view:${slug}`]);
|
|
209
223
|
const { _id: _stripped, ...result } = updated;
|
|
210
224
|
return result;
|
|
211
225
|
}
|
|
@@ -221,6 +235,7 @@ export async function deleteView(slug) {
|
|
|
221
235
|
const db = await getMetaDb();
|
|
222
236
|
const result = await db.collection(VIEWS_COLLECTION).deleteOne({ slug });
|
|
223
237
|
if (result.deletedCount === 0) throw new Error(`View "${slug}" not found`);
|
|
238
|
+
await cache.invalidateTags([`view:${slug}`]);
|
|
224
239
|
}
|
|
225
240
|
|
|
226
241
|
// ---------------------------------------------------------------------------
|
|
@@ -1,130 +1,135 @@
|
|
|
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
|
-
{{
|
|
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}} {{layoutBodyClass}}" 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"{{#if pageBodyStyle}} style="{{pageBodyStyle}}"{{/if}}>
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
{{seoTags}}
|
|
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}} {{layoutBodyClass}}" 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"{{#if pageBodyStyle}} style="{{pageBodyStyle}}"{{/if}}>
|
|
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
|
+
{{footerScript}}
|
|
107
|
+
(function () {
|
|
108
|
+
var c = window.__CMS_SITE__ && window.__CMS_SITE__.autoTheme;
|
|
109
|
+
if (!c || !c.enabled) return;
|
|
110
|
+
var n = new Date(), m = n.getHours() * 60 + n.getMinutes(), ds = (c.dayStart || "07:00").split(":"),
|
|
111
|
+
ns = (c.nightStart || "19:00").split(":"), d = +ds[0] * 60 + (+ds[1] || 0),
|
|
112
|
+
e = +ns[0] * 60 + (+ns[1] || 0), t = (m >= d && m < e) ? c.dayTheme : c.nightTheme;
|
|
113
|
+
if (window.Domma && window.Domma.theme) window.Domma.theme.set(t);
|
|
114
|
+
}());
|
|
115
|
+
{{dconfigScript}}
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<!-- Site initialisation -->
|
|
119
|
+
<script src="/public/js/site.js?v=20260524-formerror" type="module"></script>
|
|
120
|
+
|
|
121
|
+
<!-- Interactive Collection Browser (used by [collection] shortcodes with
|
|
122
|
+
searchable / filterable / sortable / paginate attributes) -->
|
|
123
|
+
<script src="/public/js/collection-browser.js?v=20260523-browser"></script>
|
|
124
|
+
|
|
125
|
+
<!-- Core Forms (logic engine must load before renderer) -->
|
|
126
|
+
<script src="/public/js/form-logic-engine.js?v=20260509-chooser"></script>
|
|
127
|
+
<script src="/public/js/forms.js?v=20260509-chooser" type="module"></script>
|
|
128
|
+
|
|
129
|
+
<!-- Core effects runtime -->
|
|
130
|
+
<script src="/public/js/effects.js"></script>
|
|
131
|
+
|
|
132
|
+
<!-- Plugin body-end injection -->
|
|
133
|
+
{{bodyEndInject}}
|
|
134
|
+
</body>
|
|
135
|
+
</html>
|
|
File without changes
|