domma-cms 0.1.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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +469 -0
  3. package/admin/css/admin.css +1123 -0
  4. package/admin/index.html +72 -0
  5. package/admin/js/api.js +210 -0
  6. package/admin/js/app.js +270 -0
  7. package/admin/js/config/sidebar-config.js +107 -0
  8. package/admin/js/lib/card.js +63 -0
  9. package/admin/js/lib/image-editor.js +869 -0
  10. package/admin/js/lib/markdown-toolbar.js +421 -0
  11. package/admin/js/templates/dashboard.html +50 -0
  12. package/admin/js/templates/documentation.html +237 -0
  13. package/admin/js/templates/layouts.html +11 -0
  14. package/admin/js/templates/login.html +58 -0
  15. package/admin/js/templates/media.html +16 -0
  16. package/admin/js/templates/navigation.html +50 -0
  17. package/admin/js/templates/page-editor.html +126 -0
  18. package/admin/js/templates/pages.html +18 -0
  19. package/admin/js/templates/plugins.html +12 -0
  20. package/admin/js/templates/settings.html +190 -0
  21. package/admin/js/templates/tutorials.html +233 -0
  22. package/admin/js/templates/user-editor.html +12 -0
  23. package/admin/js/templates/users.html +10 -0
  24. package/admin/js/views/dashboard.js +48 -0
  25. package/admin/js/views/documentation.js +12 -0
  26. package/admin/js/views/index.js +33 -0
  27. package/admin/js/views/layouts.js +49 -0
  28. package/admin/js/views/login.js +254 -0
  29. package/admin/js/views/media.js +240 -0
  30. package/admin/js/views/navigation.js +152 -0
  31. package/admin/js/views/page-editor.js +479 -0
  32. package/admin/js/views/pages.js +64 -0
  33. package/admin/js/views/plugins.js +100 -0
  34. package/admin/js/views/settings.js +64 -0
  35. package/admin/js/views/tutorials.js +12 -0
  36. package/admin/js/views/user-editor.js +88 -0
  37. package/admin/js/views/users.js +73 -0
  38. package/bin/cli.js +334 -0
  39. package/config/auth.json +20 -0
  40. package/config/content.json +10 -0
  41. package/config/navigation.json +63 -0
  42. package/config/plugins.json +47 -0
  43. package/config/presets.json +34 -0
  44. package/config/server.json +6 -0
  45. package/config/site.json +33 -0
  46. package/package.json +67 -0
  47. package/plugins/back-to-top/admin/templates/back-to-top-settings.html +55 -0
  48. package/plugins/back-to-top/admin/views/back-to-top-settings.js +44 -0
  49. package/plugins/back-to-top/config.js +10 -0
  50. package/plugins/back-to-top/plugin.js +24 -0
  51. package/plugins/back-to-top/plugin.json +36 -0
  52. package/plugins/back-to-top/public/inject-body.html +105 -0
  53. package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +113 -0
  54. package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +73 -0
  55. package/plugins/cookie-consent/config.js +30 -0
  56. package/plugins/cookie-consent/plugin.js +24 -0
  57. package/plugins/cookie-consent/plugin.json +36 -0
  58. package/plugins/cookie-consent/public/inject-body.html +69 -0
  59. package/plugins/custom-css/admin/templates/custom-css.html +17 -0
  60. package/plugins/custom-css/admin/views/custom-css.js +35 -0
  61. package/plugins/custom-css/config.js +1 -0
  62. package/plugins/custom-css/data/custom.css +0 -0
  63. package/plugins/custom-css/plugin.js +63 -0
  64. package/plugins/custom-css/plugin.json +32 -0
  65. package/plugins/custom-css/public/inject-head.html +1 -0
  66. package/plugins/domma-effects/admin/templates/domma-effects.html +488 -0
  67. package/plugins/domma-effects/admin/views/domma-effects.js +56 -0
  68. package/plugins/domma-effects/config.js +9 -0
  69. package/plugins/domma-effects/plugin.js +22 -0
  70. package/plugins/domma-effects/plugin.json +36 -0
  71. package/plugins/domma-effects/public/celebrations/core/canvas.js +111 -0
  72. package/plugins/domma-effects/public/celebrations/core/particles.js +144 -0
  73. package/plugins/domma-effects/public/celebrations/core/physics.js +166 -0
  74. package/plugins/domma-effects/public/celebrations/index.js +535 -0
  75. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1805 -0
  76. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1477 -0
  77. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1837 -0
  78. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1175 -0
  79. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1258 -0
  80. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1754 -0
  81. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1290 -0
  82. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1361 -0
  83. package/plugins/domma-effects/public/inject-body.html +268 -0
  84. package/plugins/example-analytics/admin/templates/analytics.html +10 -0
  85. package/plugins/example-analytics/admin/views/analytics.js +51 -0
  86. package/plugins/example-analytics/config.js +6 -0
  87. package/plugins/example-analytics/plugin.js +58 -0
  88. package/plugins/example-analytics/plugin.json +27 -0
  89. package/plugins/example-analytics/public/inject-body.html +13 -0
  90. package/plugins/example-analytics/public/inject-head.html +1 -0
  91. package/plugins/example-analytics/stats.json +1 -0
  92. package/plugins/form-builder/admin/templates/form-editor.html +158 -0
  93. package/plugins/form-builder/admin/templates/form-settings.html +29 -0
  94. package/plugins/form-builder/admin/templates/form-submissions.html +30 -0
  95. package/plugins/form-builder/admin/templates/forms-list.html +17 -0
  96. package/plugins/form-builder/admin/views/form-editor.js +817 -0
  97. package/plugins/form-builder/admin/views/form-settings.js +38 -0
  98. package/plugins/form-builder/admin/views/form-submissions.js +295 -0
  99. package/plugins/form-builder/admin/views/forms-list.js +164 -0
  100. package/plugins/form-builder/config.js +9 -0
  101. package/plugins/form-builder/data/forms/contact-details.json +63 -0
  102. package/plugins/form-builder/data/forms/contact.json +52 -0
  103. package/plugins/form-builder/data/submissions/contact-details.json +1 -0
  104. package/plugins/form-builder/data/submissions/contact.json +14 -0
  105. package/plugins/form-builder/email.js +103 -0
  106. package/plugins/form-builder/plugin.js +454 -0
  107. package/plugins/form-builder/plugin.json +56 -0
  108. package/plugins/form-builder/public/inject-body.html +270 -0
  109. package/plugins/form-builder/public/inject-head.html +42 -0
  110. package/public/css/site.css +189 -0
  111. package/public/js/site.js +109 -0
  112. package/scripts/copy-domma.js +48 -0
  113. package/scripts/fresh.js +41 -0
  114. package/scripts/reset.js +124 -0
  115. package/scripts/seed.js +666 -0
  116. package/scripts/setup.js +263 -0
  117. package/server/config.js +56 -0
  118. package/server/middleware/auth.js +97 -0
  119. package/server/routes/api/auth.js +116 -0
  120. package/server/routes/api/layouts.js +25 -0
  121. package/server/routes/api/media.js +93 -0
  122. package/server/routes/api/navigation.js +37 -0
  123. package/server/routes/api/pages.js +118 -0
  124. package/server/routes/api/plugins.js +46 -0
  125. package/server/routes/api/settings.js +25 -0
  126. package/server/routes/api/users.js +110 -0
  127. package/server/routes/public.js +108 -0
  128. package/server/server.js +169 -0
  129. package/server/services/content.js +298 -0
  130. package/server/services/images.js +334 -0
  131. package/server/services/markdown.js +297 -0
  132. package/server/services/plugins.js +246 -0
  133. package/server/services/renderer.js +80 -0
  134. package/server/services/users.js +212 -0
  135. package/server/templates/page.html +78 -0
@@ -0,0 +1,270 @@
1
+ <script>
2
+ (function () {
3
+ 'use strict';
4
+
5
+ var targets = document.querySelectorAll('[data-form]');
6
+ if (!targets.length) return;
7
+
8
+ function showMessage(wrapper, message, type) {
9
+ var existing = wrapper.querySelector('.fb-form-success, .fb-form-error');
10
+ if (existing) existing.remove();
11
+ var div = document.createElement('div');
12
+ div.className = type === 'success' ? 'fb-form-success' : 'fb-form-error';
13
+ div.textContent = message;
14
+ wrapper.appendChild(div);
15
+ }
16
+
17
+ function buildBlueprintFromFields(fields) {
18
+ var blueprint = {};
19
+ fields.forEach(function (field) {
20
+ if (field.type === 'page-break') return;
21
+ var def = {
22
+ type: field.type,
23
+ label: field.label || field.name,
24
+ required: field.required || false,
25
+ placeholder: field.placeholder || ''
26
+ };
27
+ if (field.helper) def.helper = field.helper;
28
+ if (field.options) def.options = field.options;
29
+ if (field.formConfig) Object.assign(def, field.formConfig);
30
+ if (field.minLength) def.minLength = field.minLength;
31
+ if (field.maxLength) def.maxLength = field.maxLength;
32
+ if (field.min !== undefined) def.min = field.min;
33
+ if (field.max !== undefined) def.max = field.max;
34
+ blueprint[field.name] = def;
35
+ });
36
+ return blueprint;
37
+ }
38
+
39
+ /**
40
+ * Split a fields array at page-break entries into wizard steps.
41
+ * Each step: { title, description, fieldGroup[] }
42
+ */
43
+ function buildWizardSteps(fields, formTitle) {
44
+ var steps = [];
45
+ var group = [];
46
+ var title = formTitle || 'Step 1';
47
+ var desc = '';
48
+
49
+ fields.forEach(function (field) {
50
+ if (field.type === 'page-break') {
51
+ steps.push({ title: title, description: desc, fieldGroup: group });
52
+ group = [];
53
+ title = field.label || ('Step ' + (steps.length + 1));
54
+ desc = field.description || '';
55
+ } else {
56
+ group.push(field);
57
+ }
58
+ });
59
+
60
+ // Always push the last (or only) group
61
+ steps.push({ title: title, description: desc, fieldGroup: group });
62
+ return steps;
63
+ }
64
+
65
+ function initFormTarget(target) {
66
+ var slug = target.getAttribute('data-form');
67
+ if (!slug) return;
68
+
69
+ fetch('/api/plugins/form-builder/forms/' + slug + '/public')
70
+ .then(function (res) {
71
+ if (!res.ok) throw new Error('Form not found: ' + slug);
72
+ return res.json();
73
+ })
74
+ .then(function (form) {
75
+ var fields = form.fields || [];
76
+ var settings = form.settings || {};
77
+
78
+ var wrapper = document.createElement('div');
79
+ wrapper.className = 'fb-form-wrapper';
80
+ target.appendChild(wrapper);
81
+
82
+ var hasPageBreaks = fields.some(function (f) { return f.type === 'page-break'; });
83
+
84
+ if (typeof Domma !== 'undefined' && Domma.forms) {
85
+
86
+ if (hasPageBreaks && Domma.forms.wizard) {
87
+ // --- Wizard mode ---
88
+ var rawSteps = buildWizardSteps(fields, form.title);
89
+ var steps = rawSteps.map(function (step) {
90
+ return {
91
+ title: step.title,
92
+ description: step.description,
93
+ fields: buildBlueprintFromFields(step.fieldGroup)
94
+ };
95
+ });
96
+
97
+ Domma.forms.wizard(wrapper, {
98
+ schema: { steps: steps },
99
+ onSubmit: function (data) {
100
+ return submitForm(slug, data, settings, wrapper, null);
101
+ }
102
+ });
103
+
104
+ // Inject honeypot into the first step's form element
105
+ if (settings.honeypot) {
106
+ var firstForm = wrapper.querySelector('form');
107
+ if (firstForm) {
108
+ injectHoneypot(firstForm);
109
+ }
110
+ }
111
+
112
+ } else if (Domma.forms.render) {
113
+ // --- Standard single-step mode ---
114
+ var blueprint = buildBlueprintFromFields(fields);
115
+
116
+ Domma.forms.render(wrapper, blueprint, {}, {
117
+ submitText: settings.submitText || 'Submit',
118
+ layout: settings.layout || 'stacked',
119
+ onSubmit: function (data) {
120
+ return submitForm(slug, data, settings, wrapper, null);
121
+ }
122
+ });
123
+
124
+ if (settings.honeypot) {
125
+ var formEl = wrapper.querySelector('form');
126
+ if (formEl) injectHoneypot(formEl);
127
+ }
128
+ }
129
+
130
+ } else {
131
+ // Fallback: manual form rendering (no Domma dependency)
132
+ renderManualForm(wrapper, fields.filter(function (f) { return f.type !== 'page-break'; }), settings, slug);
133
+ }
134
+ })
135
+ .catch(function (err) {
136
+ var msg = document.createElement('p');
137
+ msg.textContent = 'Form unavailable.';
138
+ msg.style.cssText = 'color:#f87171;font-style:italic;';
139
+ target.appendChild(msg);
140
+ console.warn('[form-builder]', err.message);
141
+ });
142
+ }
143
+
144
+ function injectHoneypot(formEl) {
145
+ var hpWrap = document.createElement('div');
146
+ hpWrap.className = 'fb-form-honeypot';
147
+ hpWrap.setAttribute('aria-hidden', 'true');
148
+ var hpInput = document.createElement('input');
149
+ hpInput.name = '_hp';
150
+ hpInput.type = 'text';
151
+ hpInput.tabIndex = -1;
152
+ hpInput.autocomplete = 'off';
153
+ hpWrap.appendChild(hpInput);
154
+ formEl.appendChild(hpWrap);
155
+ }
156
+
157
+ function submitForm(slug, data, settings, wrapper, formEl) {
158
+ var hp = '';
159
+ if (formEl) {
160
+ var hpEl = formEl.querySelector('[name="_hp"]');
161
+ hp = hpEl ? hpEl.value : '';
162
+ } else {
163
+ var hpEl2 = wrapper.querySelector('[name="_hp"]');
164
+ hp = hpEl2 ? hpEl2.value : '';
165
+ }
166
+
167
+ var payload = Object.assign({}, data, { _hp: hp });
168
+
169
+ return fetch('/api/plugins/form-builder/submit/' + slug, {
170
+ method: 'POST',
171
+ headers: { 'Content-Type': 'application/json' },
172
+ body: JSON.stringify(payload)
173
+ })
174
+ .then(function (res) { return res.json().then(function (body) { return { ok: res.ok, body: body }; }); })
175
+ .then(function (result) {
176
+ if (result.ok && result.body.ok) {
177
+ wrapper.textContent = '';
178
+ showMessage(wrapper, result.body.message || settings.successMessage || 'Thank you.', 'success');
179
+ } else {
180
+ showMessage(wrapper, result.body.error || 'Something went wrong.', 'error');
181
+ }
182
+ })
183
+ .catch(function () {
184
+ showMessage(wrapper, 'Unable to submit. Please check your connection.', 'error');
185
+ });
186
+ }
187
+
188
+ // Manual form renderer — used when Domma forms API is unavailable
189
+ function renderManualForm(wrapper, fields, settings, slug) {
190
+ var form = document.createElement('form');
191
+ form.noValidate = true;
192
+
193
+ fields.forEach(function (field) {
194
+ var group = document.createElement('div');
195
+ group.className = 'form-group';
196
+ group.style.marginBottom = '1.25rem';
197
+
198
+ var label = document.createElement('label');
199
+ label.className = 'form-label';
200
+ label.textContent = field.label || field.name;
201
+ if (field.required) {
202
+ var req = document.createElement('span');
203
+ req.textContent = ' *';
204
+ req.style.color = '#f87171';
205
+ label.appendChild(req);
206
+ }
207
+
208
+ var input;
209
+ if (field.type === 'textarea') {
210
+ input = document.createElement('textarea');
211
+ input.rows = field.formConfig?.rows || 4;
212
+ input.className = 'form-input';
213
+ } else if (field.type === 'select') {
214
+ input = document.createElement('select');
215
+ input.className = 'form-input';
216
+ (field.options || []).forEach(function (opt) {
217
+ var o = document.createElement('option');
218
+ o.value = opt.value;
219
+ o.textContent = opt.label || opt.value;
220
+ input.appendChild(o);
221
+ });
222
+ } else {
223
+ input = document.createElement('input');
224
+ input.type = field.type || 'text';
225
+ input.className = 'form-input';
226
+ if (field.placeholder) input.placeholder = field.placeholder;
227
+ }
228
+ input.name = field.name;
229
+ input.required = field.required || false;
230
+
231
+ group.appendChild(label);
232
+ group.appendChild(input);
233
+ form.appendChild(group);
234
+ });
235
+
236
+ // Honeypot
237
+ if (settings.honeypot) {
238
+ injectHoneypot(form);
239
+ }
240
+
241
+ var submitBtn = document.createElement('button');
242
+ submitBtn.type = 'submit';
243
+ submitBtn.className = 'btn btn-primary';
244
+ submitBtn.textContent = settings.submitText || 'Submit';
245
+ form.appendChild(submitBtn);
246
+
247
+ form.addEventListener('submit', function (e) {
248
+ e.preventDefault();
249
+ wrapper.classList.add('fb-form-loading');
250
+ submitBtn.disabled = true;
251
+
252
+ var data = {};
253
+ fields.forEach(function (field) {
254
+ var el = form.querySelector('[name="' + field.name + '"]');
255
+ if (el) data[field.name] = el.value;
256
+ });
257
+
258
+ submitForm(slug, data, settings, wrapper, form).finally(function () {
259
+ wrapper.classList.remove('fb-form-loading');
260
+ submitBtn.disabled = false;
261
+ });
262
+ });
263
+
264
+ wrapper.appendChild(form);
265
+ }
266
+
267
+ // Initialise all form targets on the page
268
+ targets.forEach(initFormTarget);
269
+ })();
270
+ </script>
@@ -0,0 +1,42 @@
1
+ <style>
2
+ .fb-form-wrapper {
3
+ max-width: 600px;
4
+ margin: 2rem auto;
5
+ }
6
+
7
+ .fb-form-honeypot {
8
+ display: none !important;
9
+ visibility: hidden;
10
+ }
11
+
12
+ .fb-form-success {
13
+ padding: 1rem 1.25rem;
14
+ border-radius: 6px;
15
+ background: rgba(52, 211, 153, 0.12);
16
+ border: 1px solid rgba(52, 211, 153, 0.3);
17
+ color: #34d399;
18
+ font-size: 0.95rem;
19
+ margin-top: 0.5rem;
20
+ }
21
+
22
+ .fb-form-error {
23
+ padding: 0.75rem 1rem;
24
+ border-radius: 6px;
25
+ background: rgba(239, 68, 68, 0.1);
26
+ border: 1px solid rgba(239, 68, 68, 0.25);
27
+ color: #f87171;
28
+ font-size: 0.875rem;
29
+ margin-top: 0.75rem;
30
+ }
31
+
32
+ .fb-form-loading {
33
+ opacity: 0.6;
34
+ pointer-events: none;
35
+ }
36
+
37
+ .wizard-progress { height: 4px; background: var(--border-color, #333); border-radius: 2px; margin-bottom: 1rem; }
38
+ .wizard-progress-bar { height: 100%; background: var(--primary, #5b8cff); border-radius: 2px; transition: width .3s ease; }
39
+ .wizard-step-title { font-size: 1.1rem; font-weight: 600; margin-bottom: .25rem; }
40
+ .wizard-step-description { font-size: .875rem; color: var(--text-muted, #888); margin-bottom: 1rem; }
41
+ .wizard-nav { display: flex; justify-content: space-between; gap: .5rem; margin-top: 1rem; }
42
+ </style>
@@ -0,0 +1,189 @@
1
+ /* =========================================================
2
+ Domma CMS - Public Site Styles
3
+ ========================================================= */
4
+
5
+ /* Typography */
6
+ body, button, input, select, textarea {
7
+ font-family: 'Roboto', sans-serif;
8
+ }
9
+
10
+ /* Layout */
11
+ .site-main {
12
+ min-height: calc(100vh - 60px);
13
+ padding-top: 2rem;
14
+ padding-bottom: 4rem;
15
+ }
16
+
17
+ .site-main.with-sidebar {
18
+ display: grid;
19
+ grid-template-columns: 260px 1fr;
20
+ gap: 0;
21
+ }
22
+
23
+ .site-sidebar {
24
+ min-height: 100%;
25
+ border-right: 1px solid var(--border-color, rgba(255,255,255,.08));
26
+ }
27
+
28
+ .site-content {
29
+ overflow: hidden;
30
+ }
31
+
32
+ .container {
33
+ max-width: 860px;
34
+ margin: 0 auto;
35
+ padding: 0 1.5rem;
36
+ }
37
+
38
+ /* Page title */
39
+ .page-title {
40
+ font-size: 2rem;
41
+ font-weight: 700;
42
+ margin-bottom: 1.5rem;
43
+ line-height: 1.2;
44
+ }
45
+
46
+ /* Page body typography */
47
+ .page-body {
48
+ line-height: 1.7;
49
+ font-size: 1rem;
50
+ }
51
+
52
+ .page-body h1,
53
+ .page-body h2,
54
+ .page-body h3,
55
+ .page-body h4 {
56
+ margin-top: 2rem;
57
+ margin-bottom: .75rem;
58
+ font-weight: 600;
59
+ }
60
+
61
+ .page-body h2 { font-size: 1.5rem; }
62
+ .page-body h3 { font-size: 1.25rem; }
63
+
64
+ .page-body p { margin-bottom: 1rem; }
65
+ .page-body ul,
66
+ .page-body ol { margin-bottom: 1rem; padding-left: 1.5rem; }
67
+
68
+ .page-body a { color: var(--primary, #5b8cff); }
69
+ .page-body a:hover { text-decoration: underline; }
70
+
71
+ .page-body code {
72
+ font-family: 'Fira Code', 'Courier New', monospace;
73
+ font-size: .9em;
74
+ background: rgba(255,255,255,.06);
75
+ padding: .15em .35em;
76
+ border-radius: 3px;
77
+ }
78
+
79
+ .page-body pre {
80
+ background: rgba(0,0,0,.3);
81
+ border: 1px solid rgba(255,255,255,.08);
82
+ border-radius: 6px;
83
+ padding: 1rem;
84
+ overflow-x: auto;
85
+ margin-bottom: 1rem;
86
+ }
87
+
88
+ .page-body pre code {
89
+ background: none;
90
+ padding: 0;
91
+ }
92
+
93
+ .page-body img {
94
+ max-width: 100%;
95
+ border-radius: 6px;
96
+ }
97
+
98
+ .page-body blockquote {
99
+ border-left: 3px solid var(--primary, #5b8cff);
100
+ margin: 1.5rem 0;
101
+ padding: .75rem 1rem;
102
+ background: rgba(91,140,255,.06);
103
+ border-radius: 0 6px 6px 0;
104
+ }
105
+
106
+ /* Card shortcode overrides */
107
+ .page-body .card-header h2 {
108
+ margin: 0;
109
+ font-size: 1rem;
110
+ font-weight: 600;
111
+ line-height: 1.4;
112
+ }
113
+
114
+ .card[data-collapsible] .card-header {
115
+ cursor: pointer;
116
+ user-select: none;
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: space-between;
120
+ }
121
+
122
+ .card[data-collapsible] .card-header::after {
123
+ content: '▾';
124
+ font-size: 1.1em;
125
+ line-height: 1;
126
+ display: inline-block;
127
+ transition: transform .25s ease;
128
+ flex-shrink: 0;
129
+ }
130
+
131
+ .card[data-collapsible].is-collapsed .card-header::after {
132
+ transform: rotate(-90deg);
133
+ }
134
+
135
+ .card[data-collapsible] .card-body {
136
+ overflow: hidden;
137
+ max-height: 4000px;
138
+ opacity: 1;
139
+ transition: max-height .3s ease, opacity .25s ease;
140
+ }
141
+
142
+ .card[data-collapsible].is-collapsed .card-body {
143
+ max-height: 0;
144
+ opacity: 0;
145
+ }
146
+
147
+ /* Navbar icon tweaks */
148
+ .navbar-link span[data-icon],
149
+ .navbar-link svg,
150
+ .navbar-dropdown-item span[data-icon],
151
+ .navbar-dropdown-item svg {
152
+ width: 13px !important;
153
+ height: 13px !important;
154
+ margin-right: 10px !important;
155
+ }
156
+
157
+ /* Footer */
158
+ .page-footer {
159
+ border-top: 1px solid var(--border-color, rgba(255,255,255,.08));
160
+ padding: 1.5rem 0;
161
+ }
162
+
163
+ .footer-inner {
164
+ display: flex;
165
+ align-items: center;
166
+ justify-content: space-between;
167
+ flex-wrap: wrap;
168
+ gap: 1rem;
169
+ }
170
+
171
+ .footer-inner p { margin: 0; color: var(--text-muted, #888); font-size: .875rem; }
172
+
173
+ .footer-links {
174
+ display: flex;
175
+ gap: 1.25rem;
176
+ }
177
+
178
+ .footer-links a {
179
+ color: var(--text-muted, #888);
180
+ font-size: .875rem;
181
+ text-decoration: none;
182
+ }
183
+
184
+ .footer-links a:hover { color: var(--text, #eee); }
185
+
186
+ @media (max-width: 768px) {
187
+ .site-main.with-sidebar { grid-template-columns: 1fr; }
188
+ .site-sidebar { display: none; }
189
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Public Site - Client-side Domma init
3
+ * Reads config injected by the server and initialises navbar, footer, and features.
4
+ */
5
+
6
+ $(() => {
7
+ const nav = window.__CMS_NAV__ || {};
8
+ const site = window.__CMS_SITE__ || {};
9
+
10
+ // Initialise navbar
11
+ const $navbar = $('#site-navbar');
12
+ if ($navbar.length && nav.brand) {
13
+ Domma.elements.navbar('#site-navbar', {
14
+ brand: nav.brand,
15
+ items: nav.items || [],
16
+ variant: nav.variant || 'dark',
17
+ position: nav.position || 'sticky',
18
+ collapsible: true
19
+ });
20
+ }
21
+
22
+ // Initialise footer
23
+ 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
+ }
34
+ html += `</div>`;
35
+ $footer.html(html, { safe: false });
36
+ }
37
+
38
+ // Initialise sidebar (auto-generate from headings if enabled)
39
+ const $sidebar = $('#site-sidebar');
40
+ if ($sidebar.length) {
41
+ Domma.elements.sidebar('#site-sidebar', {
42
+ autoGenerate: true,
43
+ selector: 'h2, h3',
44
+ collapsible: false,
45
+ push: true,
46
+ contentSelector: '.site-content'
47
+ });
48
+ }
49
+
50
+ // Icons
51
+ Domma.icons.scan();
52
+
53
+ // Back to top
54
+ if (document.querySelector('.site-main')) {
55
+ Domma.elements.backToTop?.();
56
+ }
57
+
58
+ // -------------------------------------------------------
59
+ // Auto-initialise Domma components inside page content
60
+ // -------------------------------------------------------
61
+ const $pageBody = $('.page-body');
62
+ if ($pageBody.length) {
63
+
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
79
+ $pageBody.find('.accordion').each(function () {
80
+ Domma.elements.accordion(this);
81
+ });
82
+
83
+ // Tabs
84
+ $pageBody.find('.tabs').each(function () {
85
+ Domma.elements.tabs(this);
86
+ });
87
+
88
+ // Carousel
89
+ $pageBody.find('.carousel').each(function () {
90
+ Domma.elements.carousel(this);
91
+ });
92
+
93
+ // Tooltips
94
+ $pageBody.find('[data-tooltip]').each(function () {
95
+ Domma.elements.tooltip(this, {
96
+ content: $(this).data('tooltip'),
97
+ position: $(this).data('tooltip-position') || 'top'
98
+ });
99
+ });
100
+
101
+ // Collapsible cards
102
+ $pageBody.find('.card[data-collapsible]').each(function () {
103
+ const header = this.querySelector('.card-header');
104
+ if (header) {
105
+ header.addEventListener('click', () => this.classList.toggle('is-collapsed'));
106
+ }
107
+ });
108
+ }
109
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Copy Domma dist files to admin and public directories.
3
+ *
4
+ * NOTE: This script is now OPTIONAL.
5
+ * The server serves Domma directly from node_modules/domma-js/public/dist at /dist/domma/.
6
+ * Only run this if you need the files as static copies (e.g. for deployment without node_modules).
7
+ *
8
+ * Run: node scripts/copy-domma.js
9
+ */
10
+ import fs from 'fs/promises';
11
+ import path from 'path';
12
+ import { fileURLToPath } from 'url';
13
+ import { createRequire } from 'module';
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const ROOT = path.resolve(__dirname, '..');
17
+ const require = createRequire(import.meta.url);
18
+
19
+ // Resolve from npm package
20
+ const dommaPackageDir = path.dirname(require.resolve('domma-js/package.json'));
21
+ const DOMMA_DIST = path.join(dommaPackageDir, 'public', 'dist');
22
+
23
+ const TARGETS = [
24
+ path.join(ROOT, 'admin', 'dist', 'domma'),
25
+ path.join(ROOT, 'public', 'dist', 'domma')
26
+ ];
27
+
28
+ async function copyDir(src, dest) {
29
+ await fs.mkdir(dest, { recursive: true });
30
+ const entries = await fs.readdir(src, { withFileTypes: true });
31
+ for (const entry of entries) {
32
+ const srcPath = path.join(src, entry.name);
33
+ const destPath = path.join(dest, entry.name);
34
+ if (entry.isDirectory()) {
35
+ await copyDir(srcPath, destPath);
36
+ } else {
37
+ await fs.copyFile(srcPath, destPath);
38
+ }
39
+ }
40
+ }
41
+
42
+ console.log(`Source: ${DOMMA_DIST}`);
43
+ console.log('Copying Domma dist files...');
44
+ for (const target of TARGETS) {
45
+ await copyDir(DOMMA_DIST, target);
46
+ console.log(` ✓ → ${path.relative(ROOT, target)}`);
47
+ }
48
+ console.log('Done.');
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Domma CMS — Fresh Install
4
+ * Chains: reset → setup → seed
5
+ * Gives you a fully configured CMS with demo content in one command.
6
+ * Run: npm run fresh
7
+ */
8
+ import { spawnSync } from 'node:child_process';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const ROOT = path.resolve(__dirname, '..');
14
+
15
+ function run(script) {
16
+ const result = spawnSync(process.execPath, [path.join('scripts', script)], {
17
+ cwd: ROOT,
18
+ stdio: 'inherit'
19
+ });
20
+ if (result.status !== 0) {
21
+ process.exit(result.status ?? 1);
22
+ }
23
+ }
24
+
25
+ console.log('');
26
+ console.log(' ╔══════════════════════════════════════╗');
27
+ console.log(' ║ Domma CMS — Fresh Install ║');
28
+ console.log(' ║ reset → setup → seed ║');
29
+ console.log(' ╚══════════════════════════════════════╝');
30
+ console.log('');
31
+
32
+ run('reset.js');
33
+ run('setup.js');
34
+ run('seed.js');
35
+
36
+ console.log('');
37
+ console.log(' ══════════════════════════════════════════');
38
+ console.log(' Fresh install complete! Start the server:');
39
+ console.log(' npm start');
40
+ console.log(' ══════════════════════════════════════════');
41
+ console.log('');