domma-cms 0.1.0 → 0.2.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 (87) hide show
  1. package/admin/css/admin.css +78 -1
  2. package/admin/js/api.js +32 -0
  3. package/admin/js/app.js +24 -7
  4. package/admin/js/config/sidebar-config.js +8 -0
  5. package/admin/js/templates/collection-editor.html +80 -0
  6. package/admin/js/templates/collection-entries.html +36 -0
  7. package/admin/js/templates/collections.html +12 -0
  8. package/admin/js/templates/documentation.html +136 -0
  9. package/admin/js/templates/navigation.html +26 -4
  10. package/admin/js/templates/page-editor.html +91 -85
  11. package/admin/js/templates/settings.html +433 -172
  12. package/admin/js/views/collection-editor.js +487 -0
  13. package/admin/js/views/collection-entries.js +484 -0
  14. package/admin/js/views/collections.js +153 -0
  15. package/admin/js/views/dashboard.js +14 -6
  16. package/admin/js/views/index.js +9 -3
  17. package/admin/js/views/login.js +3 -2
  18. package/admin/js/views/navigation.js +77 -11
  19. package/admin/js/views/page-editor.js +207 -25
  20. package/admin/js/views/pages.js +14 -6
  21. package/admin/js/views/settings.js +137 -2
  22. package/admin/js/views/users.js +10 -7
  23. package/bin/cli.js +37 -10
  24. package/config/auth.json +2 -1
  25. package/config/content.json +1 -0
  26. package/config/navigation.json +14 -4
  27. package/config/plugins.json +0 -18
  28. package/config/presets.json +4 -8
  29. package/config/site.json +44 -3
  30. package/package.json +6 -2
  31. package/plugins/domma-effects/admin/templates/domma-effects.html +92 -3
  32. package/plugins/domma-effects/plugin.js +125 -0
  33. package/plugins/domma-effects/public/inject-body.html +19 -0
  34. package/plugins/example-analytics/admin/views/analytics.js +2 -2
  35. package/plugins/example-analytics/plugin.json +8 -0
  36. package/plugins/example-analytics/stats.json +15 -1
  37. package/plugins/form-builder/admin/templates/form-editor.html +19 -6
  38. package/plugins/form-builder/admin/views/form-editor.js +634 -9
  39. package/plugins/form-builder/admin/views/form-submissions.js +4 -4
  40. package/plugins/form-builder/admin/views/forms-list.js +5 -5
  41. package/plugins/form-builder/data/forms/consent.json +104 -0
  42. package/plugins/form-builder/data/forms/contacts.json +66 -0
  43. package/plugins/form-builder/data/submissions/consent.json +13 -0
  44. package/plugins/form-builder/data/submissions/contacts.json +26 -0
  45. package/plugins/form-builder/plugin.js +62 -11
  46. package/plugins/form-builder/plugin.json +12 -16
  47. package/plugins/form-builder/public/form-logic-engine.js +568 -0
  48. package/plugins/form-builder/public/inject-body.html +88 -6
  49. package/plugins/form-builder/public/inject-head.html +16 -0
  50. package/plugins/form-builder/public/package.json +1 -0
  51. package/public/css/site.css +113 -0
  52. package/public/js/btt.js +90 -0
  53. package/public/js/cookie-consent.js +61 -0
  54. package/public/js/site.js +129 -34
  55. package/scripts/build.js +129 -0
  56. package/scripts/seed.js +517 -7
  57. package/server/routes/api/collections.js +301 -0
  58. package/server/routes/api/settings.js +66 -2
  59. package/server/server.js +19 -15
  60. package/server/services/collections.js +430 -0
  61. package/server/services/content.js +11 -2
  62. package/server/services/hooks.js +109 -0
  63. package/server/services/markdown.js +500 -149
  64. package/server/services/plugins.js +6 -1
  65. package/server/services/renderer.js +73 -7
  66. package/server/templates/page.html +38 -3
  67. package/plugins/back-to-top/admin/templates/back-to-top-settings.html +0 -55
  68. package/plugins/back-to-top/admin/views/back-to-top-settings.js +0 -44
  69. package/plugins/back-to-top/config.js +0 -10
  70. package/plugins/back-to-top/plugin.js +0 -24
  71. package/plugins/back-to-top/plugin.json +0 -36
  72. package/plugins/back-to-top/public/inject-body.html +0 -105
  73. package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +0 -113
  74. package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +0 -73
  75. package/plugins/cookie-consent/config.js +0 -30
  76. package/plugins/cookie-consent/plugin.js +0 -24
  77. package/plugins/cookie-consent/plugin.json +0 -36
  78. package/plugins/cookie-consent/public/inject-body.html +0 -69
  79. package/plugins/custom-css/admin/templates/custom-css.html +0 -17
  80. package/plugins/custom-css/admin/views/custom-css.js +0 -35
  81. package/plugins/custom-css/config.js +0 -1
  82. package/plugins/custom-css/data/custom.css +0 -0
  83. package/plugins/custom-css/plugin.js +0 -63
  84. package/plugins/custom-css/plugin.json +0 -32
  85. package/plugins/custom-css/public/inject-head.html +0 -1
  86. package/plugins/form-builder/data/forms/contact.json +0 -52
  87. package/plugins/form-builder/data/submissions/contact.json +0 -14
@@ -17,7 +17,7 @@
17
17
  function buildBlueprintFromFields(fields) {
18
18
  var blueprint = {};
19
19
  fields.forEach(function (field) {
20
- if (field.type === 'page-break') return;
20
+ if (field.type === 'page-break' || field.type === 'spacer') return;
21
21
  var def = {
22
22
  type: field.type,
23
23
  label: field.label || field.name,
@@ -52,7 +52,7 @@
52
52
  group = [];
53
53
  title = field.label || ('Step ' + (steps.length + 1));
54
54
  desc = field.description || '';
55
- } else {
55
+ } else if (field.type !== 'spacer') {
56
56
  group.push(field);
57
57
  }
58
58
  });
@@ -101,6 +101,12 @@
101
101
  }
102
102
  });
103
103
 
104
+ // Init conditional logic engine on wizard form
105
+ if (window.FormLogicEngine && fields.some(function (f) { return f.logic; })) {
106
+ var wizardRuntime = new window.FormLogicEngine.FormLogicRuntime(form, wrapper);
107
+ wizardRuntime.init();
108
+ }
109
+
104
110
  // Inject honeypot into the first step's form element
105
111
  if (settings.honeypot) {
106
112
  var firstForm = wrapper.querySelector('form');
@@ -121,6 +127,18 @@
121
127
  }
122
128
  });
123
129
 
130
+ // Init conditional logic engine on standard form
131
+ if (window.FormLogicEngine && fields.some(function (f) { return f.logic; })) {
132
+ var stdRuntime = new window.FormLogicEngine.FormLogicRuntime(form, wrapper);
133
+ stdRuntime.init();
134
+ }
135
+
136
+ if (fields.some(function (f) {
137
+ return f.type === 'spacer';
138
+ })) {
139
+ injectSpacers(wrapper, fields);
140
+ }
141
+
124
142
  if (settings.honeypot) {
125
143
  var formEl = wrapper.querySelector('form');
126
144
  if (formEl) injectHoneypot(formEl);
@@ -129,7 +147,9 @@
129
147
 
130
148
  } else {
131
149
  // Fallback: manual form rendering (no Domma dependency)
132
- renderManualForm(wrapper, fields.filter(function (f) { return f.type !== 'page-break'; }), settings, slug);
150
+ renderManualForm(wrapper, fields.filter(function (f) {
151
+ return f.type !== 'page-break' && f.type !== 'spacer';
152
+ }), settings, slug, form);
133
153
  }
134
154
  })
135
155
  .catch(function (err) {
@@ -141,6 +161,33 @@
141
161
  });
142
162
  }
143
163
 
164
+ /**
165
+ * After Domma renders a form, insert .fb-spacer divs at the positions
166
+ * where spacer pseudo-fields appear in the original fields array.
167
+ */
168
+ function injectSpacers(wrapper, allFields) {
169
+ var form = wrapper.querySelector('form');
170
+ if (!form) return;
171
+ var groups = Array.from(form.querySelectorAll('.form-group'));
172
+ var groupIdx = 0;
173
+ allFields.forEach(function (field) {
174
+ if (field.type === 'spacer') {
175
+ var spacer = document.createElement('div');
176
+ spacer.className = 'fb-spacer';
177
+ var target = groups[groupIdx];
178
+ if (target) {
179
+ form.insertBefore(spacer, target);
180
+ } else {
181
+ var submitBtn = form.querySelector('[type="submit"]');
182
+ if (submitBtn) form.insertBefore(spacer, submitBtn);
183
+ else form.appendChild(spacer);
184
+ }
185
+ } else if (field.type !== 'page-break') {
186
+ groupIdx++;
187
+ }
188
+ });
189
+ }
190
+
144
191
  function injectHoneypot(formEl) {
145
192
  var hpWrap = document.createElement('div');
146
193
  hpWrap.className = 'fb-form-honeypot';
@@ -186,7 +233,7 @@
186
233
  }
187
234
 
188
235
  // Manual form renderer — used when Domma forms API is unavailable
189
- function renderManualForm(wrapper, fields, settings, slug) {
236
+ function renderManualForm(wrapper, fields, settings, slug, formDef) {
190
237
  var form = document.createElement('form');
191
238
  form.noValidate = true;
192
239
 
@@ -246,8 +293,6 @@
246
293
 
247
294
  form.addEventListener('submit', function (e) {
248
295
  e.preventDefault();
249
- wrapper.classList.add('fb-form-loading');
250
- submitBtn.disabled = true;
251
296
 
252
297
  var data = {};
253
298
  fields.forEach(function (field) {
@@ -255,6 +300,37 @@
255
300
  if (el) data[field.name] = el.value;
256
301
  });
257
302
 
303
+ // Client-side logic validation for manual fallback
304
+ if (window.FormLogicEngine && formDef) {
305
+ var Engine = window.FormLogicEngine;
306
+ var requiredErrors = [];
307
+ var validationErrors = [];
308
+ fields.forEach(function (field) {
309
+ var vis = Engine.evaluateFieldVisibility(field, data);
310
+ if (vis === 'hidden') {
311
+ delete data[field.name];
312
+ return;
313
+ }
314
+ var required = Engine.evaluateFieldRequirement(field, data);
315
+ var val = data[field.name];
316
+ if (required && (!val || !String(val).trim())) {
317
+ requiredErrors.push(field.label || field.name);
318
+ }
319
+ var errors = Engine.validateField(field, val || '', data);
320
+ if (errors.length) validationErrors.push(errors[0].message);
321
+ });
322
+ if (requiredErrors.length || validationErrors.length) {
323
+ var errParts = [];
324
+ if (requiredErrors.length) errParts.push('Required: ' + requiredErrors.join(', '));
325
+ if (validationErrors.length) errParts.push(validationErrors.join('; '));
326
+ showMessage(wrapper, errParts.join('. '), 'error');
327
+ return;
328
+ }
329
+ }
330
+
331
+ wrapper.classList.add('fb-form-loading');
332
+ submitBtn.disabled = true;
333
+
258
334
  submitForm(slug, data, settings, wrapper, form).finally(function () {
259
335
  wrapper.classList.remove('fb-form-loading');
260
336
  submitBtn.disabled = false;
@@ -262,6 +338,12 @@
262
338
  });
263
339
 
264
340
  wrapper.appendChild(form);
341
+
342
+ // Init conditional logic engine on manual form
343
+ if (window.FormLogicEngine && formDef && fields.some(function (f) { return f.logic; })) {
344
+ var manualRuntime = new window.FormLogicEngine.FormLogicRuntime(formDef, wrapper);
345
+ manualRuntime.init();
346
+ }
265
347
  }
266
348
 
267
349
  // Initialise all form targets on the page
@@ -39,4 +39,20 @@
39
39
  .wizard-step-title { font-size: 1.1rem; font-weight: 600; margin-bottom: .25rem; }
40
40
  .wizard-step-description { font-size: .875rem; color: var(--text-muted, #888); margin-bottom: 1rem; }
41
41
  .wizard-nav { display: flex; justify-content: space-between; gap: .5rem; margin-top: 1rem; }
42
+
43
+ .fb-field-hidden { display: none !important; }
44
+ .fb-validation-error { color: var(--danger, #ef4444); font-size: .8rem; margin-top: .25rem; }
45
+
46
+ .fb-form-wrapper .domma-form-field {
47
+ margin-bottom: 2rem;
48
+ }
49
+
50
+ .fb-form-wrapper .form-group:not(.form-buttons) {
51
+ margin-bottom: 2rem;
52
+ }
53
+
54
+ .fb-spacer {
55
+ height: 2rem;
56
+ }
42
57
  </style>
58
+ <script src="/plugins/form-builder/public/form-logic-engine.js"></script>
@@ -0,0 +1 @@
1
+ { "type": "commonjs" }
@@ -29,6 +29,11 @@ body, button, input, select, textarea {
29
29
  overflow: hidden;
30
30
  }
31
31
 
32
+ .table-responsive {
33
+ overflow-x: auto;
34
+ -webkit-overflow-scrolling: touch;
35
+ }
36
+
32
37
  .container {
33
38
  max-width: 860px;
34
39
  margin: 0 auto;
@@ -147,6 +152,8 @@ body, button, input, select, textarea {
147
152
  /* Navbar icon tweaks */
148
153
  .navbar-link span[data-icon],
149
154
  .navbar-link svg,
155
+ .navbar-dropdown-toggle span[data-icon],
156
+ .navbar-dropdown-toggle svg,
150
157
  .navbar-dropdown-item span[data-icon],
151
158
  .navbar-dropdown-item svg {
152
159
  width: 13px !important;
@@ -154,6 +161,33 @@ body, button, input, select, textarea {
154
161
  margin-right: 10px !important;
155
162
  }
156
163
 
164
+ /* Normalise dropdown toggle font size to match regular nav links at every breakpoint */
165
+ .navbar-dropdown-toggle {
166
+ font-size: var(--dm-font-size-base);
167
+ }
168
+
169
+ @media (min-width: 993px) {
170
+ .navbar-dropdown-toggle {
171
+ font-size: var(--dm-font-size-sm);
172
+ }
173
+ }
174
+
175
+ @media (min-width: 1201px) {
176
+ .navbar-dropdown-toggle {
177
+ font-size: var(--dm-font-size-xs);
178
+ }
179
+ }
180
+
181
+ /* Reduced motion override — applied when user enables the footer toggle */
182
+ .dm-reduced-motion *,
183
+ .dm-reduced-motion *::before,
184
+ .dm-reduced-motion *::after {
185
+ animation-duration: 0.001ms !important;
186
+ animation-iteration-count: 1 !important;
187
+ transition-duration: 0.001ms !important;
188
+ scroll-behavior: auto !important;
189
+ }
190
+
157
191
  /* Footer */
158
192
  .page-footer {
159
193
  border-top: 1px solid var(--border-color, rgba(255,255,255,.08));
@@ -183,7 +217,86 @@ body, button, input, select, textarea {
183
217
 
184
218
  .footer-links a:hover { color: var(--text, #eee); }
185
219
 
220
+ .footer-social {
221
+ display: flex;
222
+ gap: 0.5rem;
223
+ align-items: center;
224
+ }
225
+
226
+ .footer-social-link {
227
+ display: inline-flex;
228
+ align-items: center;
229
+ justify-content: center;
230
+ width: 1.75rem;
231
+ height: 1.75rem;
232
+ color: var(--text-muted, #888);
233
+ transition: color 0.15s;
234
+ }
235
+
236
+ .footer-social-link:hover {
237
+ color: var(--text, #eee);
238
+ }
239
+
240
+ .footer-social-link svg {
241
+ width: 1rem;
242
+ height: 1rem;
243
+ }
244
+
245
+ .footer-motion-switch {
246
+ font-size: .8rem;
247
+ color: var(--text-muted, #888);
248
+ white-space: nowrap;
249
+ }
250
+
251
+ .footer-motion-switch .form-switch-label {
252
+ color: var(--text-muted, #888);
253
+ }
254
+
255
+ .footer-motion-switch .form-switch-input {
256
+ width: 2rem;
257
+ height: 1.125rem;
258
+ }
259
+
260
+ .footer-motion-switch .form-switch-input::after {
261
+ width: .875rem;
262
+ height: .875rem;
263
+ }
264
+
265
+ .footer-motion-switch .form-switch-input:checked::after {
266
+ transform: translateX(.875rem);
267
+ }
268
+
269
+ /* Slideover panel layout */
270
+ .dm-slideover-header {
271
+ display: flex;
272
+ align-items: center;
273
+ justify-content: space-between;
274
+ padding: .875rem 1.25rem;
275
+ border-bottom: 1px solid var(--border-color, rgba(255, 255, 255, .08));
276
+ flex-shrink: 0;
277
+ }
278
+
279
+ .dm-slideover-title {
280
+ margin: 0;
281
+ font-size: 1rem;
282
+ font-weight: 600;
283
+ line-height: 1.4;
284
+ }
285
+
286
+ .dm-slideover-body {
287
+ padding: 1.25rem;
288
+ overflow-y: auto;
289
+ flex: 1;
290
+ }
291
+
186
292
  @media (max-width: 768px) {
187
293
  .site-main.with-sidebar { grid-template-columns: 1fr; }
188
294
  .site-sidebar { display: none; }
189
295
  }
296
+
297
+ /* Hero full-width breakout — escapes the .container to span the full viewport */
298
+ .hero-breakout {
299
+ width: 100vw;
300
+ margin-left: calc(50% - 50vw);
301
+ margin-right: calc(50% - 50vw);
302
+ }
@@ -0,0 +1,90 @@
1
+ (function () {
2
+ 'use strict';
3
+
4
+ var cfg = window.__CMS_SITE__ && window.__CMS_SITE__.backToTop;
5
+ if (!cfg) return;
6
+
7
+ var BTN_ID = 'back-to-top-btn';
8
+ var threshold = cfg.scrollThreshold || 300;
9
+ var position = cfg.position || 'bottom-right';
10
+ var label = cfg.label || '';
11
+ var smooth = cfg.smooth !== false;
12
+ var offset = cfg.offset || 32;
13
+
14
+ var btn = document.createElement('button');
15
+ btn.id = BTN_ID;
16
+ btn.type = 'button';
17
+ btn.setAttribute('aria-label', 'Back to top');
18
+ btn.setAttribute('title', 'Back to top');
19
+
20
+ var svgNS = 'http://www.w3.org/2000/svg';
21
+ var svg = document.createElementNS(svgNS, 'svg');
22
+ svg.setAttribute('viewBox', '0 0 24 24');
23
+ svg.setAttribute('width', '20');
24
+ svg.setAttribute('height', '20');
25
+ svg.setAttribute('fill', 'none');
26
+ svg.setAttribute('stroke', 'currentColor');
27
+ svg.setAttribute('stroke-width', '2');
28
+ svg.setAttribute('stroke-linecap', 'round');
29
+ svg.setAttribute('stroke-linejoin', 'round');
30
+ var path = document.createElementNS(svgNS, 'path');
31
+ path.setAttribute('d', 'M12 19V5M5 12l7-7 7 7');
32
+ svg.appendChild(path);
33
+ btn.appendChild(svg);
34
+
35
+ if (label) {
36
+ var span = document.createElement('span');
37
+ span.textContent = label;
38
+ span.style.marginLeft = '6px';
39
+ btn.appendChild(span);
40
+ }
41
+
42
+ var isRight = position !== 'bottom-left';
43
+ Object.assign(btn.style, {
44
+ position: 'fixed',
45
+ bottom: offset + 'px',
46
+ right: isRight ? offset + 'px' : 'auto',
47
+ left: isRight ? 'auto' : offset + 'px',
48
+ zIndex: '9999',
49
+ display: 'flex',
50
+ alignItems: 'center',
51
+ padding: '10px',
52
+ borderRadius: '50%',
53
+ border: 'none',
54
+ cursor: 'pointer',
55
+ background: 'rgba(0,0,0,0.5)',
56
+ color: '#fff',
57
+ backdropFilter: 'blur(4px)',
58
+ opacity: '0',
59
+ transform: 'translateY(12px)',
60
+ transition: 'opacity .25s, transform .25s',
61
+ pointerEvents: 'none'
62
+ });
63
+
64
+ if (label) {
65
+ btn.style.borderRadius = '999px';
66
+ btn.style.padding = '10px 16px';
67
+ }
68
+
69
+ document.body.appendChild(btn);
70
+
71
+ function show() {
72
+ btn.style.opacity = '1';
73
+ btn.style.transform = 'translateY(0)';
74
+ btn.style.pointerEvents = 'auto';
75
+ }
76
+
77
+ function hide() {
78
+ btn.style.opacity = '0';
79
+ btn.style.transform = 'translateY(12px)';
80
+ btn.style.pointerEvents = 'none';
81
+ }
82
+
83
+ window.addEventListener('scroll', function () {
84
+ if (window.scrollY > threshold) { show(); } else { hide(); }
85
+ }, {passive: true});
86
+
87
+ btn.addEventListener('click', function () {
88
+ window.scrollTo({top: 0, behavior: smooth ? 'smooth' : 'auto'});
89
+ });
90
+ })();
@@ -0,0 +1,61 @@
1
+ (function () {
2
+ 'use strict';
3
+
4
+ if (!window.Domma || !window.Domma.components || !window.Domma.components.cookieConsent) return;
5
+
6
+ var s = window.__CMS_SITE__ && window.__CMS_SITE__.cookieConsent;
7
+ if (!s) return;
8
+
9
+ var categories = {
10
+ necessary: {
11
+ label: 'Necessary Cookies',
12
+ description: 'These cookies are essential for the website to function properly.',
13
+ required: true
14
+ }
15
+ };
16
+
17
+ if (s.showFunctional !== false) {
18
+ categories.functional = {
19
+ label: 'Functional Cookies',
20
+ description: 'These cookies enable personalised features and functionality.',
21
+ required: false
22
+ };
23
+ }
24
+
25
+ if (s.showAnalytics !== false) {
26
+ categories.analytics = {
27
+ label: 'Analytics Cookies',
28
+ description: 'These cookies help us understand how visitors interact with our website.',
29
+ required: false
30
+ };
31
+ }
32
+
33
+ if (s.showMarketing !== false) {
34
+ categories.marketing = {
35
+ label: 'Marketing Cookies',
36
+ description: 'These cookies are used to deliver relevant ads and marketing campaigns.',
37
+ required: false
38
+ };
39
+ }
40
+
41
+ var cfg = {
42
+ message: s.message,
43
+ acceptAllText: s.acceptAllText,
44
+ rejectAllText: s.rejectAllText,
45
+ customizeText: s.customizeText,
46
+ savePreferencesText: s.savePreferencesText,
47
+ privacyPolicyText: s.privacyPolicyText,
48
+ privacyPolicyUrl: s.privacyPolicyUrl || null,
49
+ cookiePolicyText: s.cookiePolicyText,
50
+ cookiePolicyUrl: s.cookiePolicyUrl || null,
51
+ position: s.position || 'bottom',
52
+ layout: s.layout || 'bar',
53
+ theme: s.theme || 'dark',
54
+ categories: categories,
55
+ consentVersion: s.consentVersion || '1.0',
56
+ autoShow: true,
57
+ animation: true
58
+ };
59
+
60
+ Domma.components.cookieConsent(cfg);
61
+ })();
package/public/js/site.js CHANGED
@@ -10,29 +10,93 @@ $(() => {
10
10
  // Initialise navbar
11
11
  const $navbar = $('#site-navbar');
12
12
  if ($navbar.length && nav.brand) {
13
+ // Build brand.html when an icon is configured — Domma navbar doesn't natively support data-icon spans
14
+ const brand = {...nav.brand};
15
+ if (brand.icon) {
16
+ let html = `<span data-icon="${brand.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`;
17
+ if (brand.text) html += `<span class="navbar-brand-text">${brand.text}</span>`;
18
+ brand.html = html;
19
+ }
13
20
  Domma.elements.navbar('#site-navbar', {
14
- brand: nav.brand,
21
+ brand,
15
22
  items: nav.items || [],
16
23
  variant: nav.variant || 'dark',
17
24
  position: nav.position || 'sticky',
18
25
  collapsible: true
19
26
  });
27
+ Domma.icons.scan('#site-navbar');
20
28
  }
21
29
 
22
30
  // Initialise footer
23
31
  const $footer = $('#site-footer');
24
- if ($footer.length && site.footer) {
25
- const footer = site.footer;
26
- let html = `<div class="footer-inner container"><p>${footer.copyright || ''}</p>`;
27
- if (footer.links?.length) {
28
- html += `<nav class="footer-links">`;
29
- footer.links.forEach(link => {
30
- html += `<a href="${link.url}">${link.text}</a>`;
31
- });
32
- html += `</nav>`;
33
- }
32
+ if ($footer.length) {
33
+ const social = site.social || {};
34
+ const SOCIAL_ICONS = {
35
+ twitter: {
36
+ label: 'X / Twitter',
37
+ svg: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'
38
+ },
39
+ facebook: {
40
+ label: 'Facebook',
41
+ svg: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'
42
+ },
43
+ instagram: {
44
+ label: 'Instagram',
45
+ svg: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'
46
+ },
47
+ linkedin: {
48
+ label: 'LinkedIn',
49
+ svg: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'
50
+ },
51
+ github: {
52
+ label: 'GitHub',
53
+ svg: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'
54
+ },
55
+ youtube: {
56
+ label: 'YouTube',
57
+ svg: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'
58
+ }
59
+ };
60
+
61
+ let html = '<div class="footer-inner container">';
62
+
63
+ if (site.footer) {
64
+ const footer = site.footer;
65
+ html += `<p>${footer.copyright || ''}</p>`;
66
+ if (footer.links?.length) {
67
+ html += `<nav class="footer-links">`;
68
+ footer.links.forEach(link => {
69
+ html += `<a href="${link.url}">${link.text}</a>`;
70
+ });
71
+ html += `</nav>`;
72
+ }
73
+ const activeSocial = Object.keys(SOCIAL_ICONS).filter(k => social[k]);
74
+ if (activeSocial.length) {
75
+ html += `<div class="footer-social">`;
76
+ activeSocial.forEach(k => {
77
+ const {label, svg} = SOCIAL_ICONS[k];
78
+ html += `<a href="${social[k]}" target="_blank" rel="noopener noreferrer" aria-label="${label}" class="footer-social-link">${svg}</a>`;
79
+ });
34
80
  html += `</div>`;
81
+ }
82
+ }
83
+
84
+ const storedPref = S.get('reduced_motion');
85
+ const motionOn = storedPref !== null
86
+ ? !!storedPref
87
+ : !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
88
+ html += `<label class="form-switch footer-motion-switch" title="Reduce motion">` +
89
+ `<input type="checkbox" class="form-switch-input" id="dm-motion-switch"${motionOn ? ' checked' : ''}>` +
90
+ `<span class="form-switch-label">Reduce motion</span>` +
91
+ `</label>`;
92
+
93
+ html += '</div>';
35
94
  $footer.html(html, { safe: false });
95
+
96
+ document.getElementById('dm-motion-switch').addEventListener('change', function () {
97
+ S.set('reduced_motion', this.checked);
98
+ window.location.reload();
99
+ });
36
100
  }
37
101
 
38
102
  // Initialise sidebar (auto-generate from headings if enabled)
@@ -50,34 +114,17 @@ $(() => {
50
114
  // Icons
51
115
  Domma.icons.scan();
52
116
 
53
- // Back to top
54
- if (document.querySelector('.site-main')) {
55
- Domma.elements.backToTop?.();
56
- }
57
-
58
117
  // -------------------------------------------------------
59
118
  // Auto-initialise Domma components inside page content
60
119
  // -------------------------------------------------------
61
120
  const $pageBody = $('.page-body');
62
121
  if ($pageBody.length) {
63
122
 
64
- // Reveal effects
65
- $pageBody.find('.dm-reveal').each(function () {
66
- const $el = $(this);
67
- const opts = {};
68
- const classes = $el.attr('class') || '';
69
- const m = classes.match(/dm-reveal-(fade|slide-up|slide-down|slide-left|slide-right|zoom|flip)/);
70
- if (m) opts.animation = m[1];
71
- if ($el.data('reveal-duration')) opts.duration = parseInt($el.data('reveal-duration'), 10);
72
- if ($el.data('reveal-delay')) opts.delay = parseInt($el.data('reveal-delay'), 10);
73
- if ($el.data('reveal-threshold')) opts.threshold = parseFloat($el.data('reveal-threshold'));
74
- if ($el.data('reveal-once') === 'false') opts.once = false;
75
- Domma.elements.reveal(this, opts);
76
- });
77
-
78
- // Accordion
123
+ // Accordion — reads data-multi attribute set by [accordion multiple="true"] shortcode
79
124
  $pageBody.find('.accordion').each(function () {
80
- Domma.elements.accordion(this);
125
+ Domma.elements.accordion(this, {
126
+ allowMultiple: this.dataset.multi === 'true'
127
+ });
81
128
  });
82
129
 
83
130
  // Tabs
@@ -85,9 +132,23 @@ $(() => {
85
132
  Domma.elements.tabs(this);
86
133
  });
87
134
 
88
- // Carousel
135
+ // Carousel — reads data attributes set by [carousel] shortcode
89
136
  $pageBody.find('.carousel').each(function () {
90
- Domma.elements.carousel(this);
137
+ Domma.elements.carousel(this, {
138
+ autoplay: this.dataset.autoplay === 'true',
139
+ interval: parseInt(this.dataset.interval, 10) || 5000,
140
+ loop: this.dataset.loop !== 'false',
141
+ animation: this.dataset.animation || 'slide'
142
+ });
143
+ });
144
+
145
+ // Countdown — initialises .dm-countdown elements via E.timer()
146
+ $pageBody.find('.dm-countdown').each(function () {
147
+ const opts = {autoStart: true};
148
+ if (this.dataset.to) opts.targetDate = new Date(this.dataset.to);
149
+ if (this.dataset.duration) opts.duration = parseInt(this.dataset.duration, 10);
150
+ if (this.dataset.format) opts.format = this.dataset.format;
151
+ Domma.elements.timer(this, opts);
91
152
  });
92
153
 
93
154
  // Tooltips
@@ -105,5 +166,39 @@ $(() => {
105
166
  header.addEventListener('click', () => this.classList.toggle('is-collapsed'));
106
167
  }
107
168
  });
169
+
170
+ // Slideover triggers — wired from [slideover] shortcode
171
+ $pageBody.find('.dm-so-trigger').each(function () {
172
+ this.addEventListener('click', () => {
173
+ const targetId = this.dataset.soTarget;
174
+ const contentEl = document.getElementById(targetId);
175
+ if (!contentEl) return;
176
+ const so = E.slideover({
177
+ title: contentEl.dataset.soTitle || '',
178
+ size: contentEl.dataset.soSize || 'md',
179
+ position: contentEl.dataset.soPosition || 'right'
180
+ });
181
+ contentEl.style.display = '';
182
+ so.setContent(contentEl);
183
+ so.open();
184
+ });
185
+ });
186
+ }
187
+
188
+ // DConfig — merge window.__CMS_DCONFIG__ (editor section) with inline [dconfig] shortcodes
189
+ // Inline shortcodes win on selector conflict (assigned last via Object.assign)
190
+ if (typeof $.setup === 'function') {
191
+ const merged = Object.assign({}, window.__CMS_DCONFIG__ || {});
192
+ document.querySelectorAll('.dm-page-config[data-config]').forEach(el => {
193
+ try {
194
+ const decoded = atob(el.dataset.config);
195
+ const parsed = JSON.parse(decoded);
196
+ Object.assign(merged, parsed);
197
+ } catch { /* skip malformed blocks */
198
+ }
199
+ });
200
+ if (Object.keys(merged).length) {
201
+ $.setup(merged);
202
+ }
108
203
  }
109
204
  });