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
@@ -1,15 +1,19 @@
1
1
  /**
2
2
  * Settings View
3
3
  */
4
- import { api } from '../api.js';
4
+ import {api, apiRequest} from '../api.js';
5
5
 
6
6
  export const settingsView = {
7
7
  templateUrl: '/admin/js/templates/settings.html',
8
8
 
9
9
  async onMount($container) {
10
+ E.tabs($container.find('#settings-tabs').get(0));
11
+
10
12
  const s = await api.settings.get().catch(() => ({}));
11
13
  $container.find('#field-site-title').val(s.title || '');
12
14
  $container.find('#field-tagline').val(s.tagline || '');
15
+ $container.find('#field-font-family').val(s.fontFamily || 'Roboto');
16
+ $container.find('#field-font-size').val(s.fontSize || 16);
13
17
  $container.find('#field-theme').val(s.theme || 'charcoal-dark');
14
18
  $container.find('#field-admin-theme').val(s.adminTheme || 'charcoal-dark');
15
19
  $container.find('#field-seo-title').val(s.seo?.defaultTitle || '');
@@ -17,7 +21,14 @@ export const settingsView = {
17
21
  $container.find('#field-seo-desc').val(s.seo?.defaultDescription || '');
18
22
  $container.find('#field-footer-copy').val(s.footer?.copyright || '');
19
23
 
20
- // SMTP fields
24
+ // Social fields
25
+ $container.find('#field-social-twitter').val(s.social?.twitter || '');
26
+ $container.find('#field-social-facebook').val(s.social?.facebook || '');
27
+ $container.find('#field-social-instagram').val(s.social?.instagram || '');
28
+ $container.find('#field-social-linkedin').val(s.social?.linkedin || '');
29
+ $container.find('#field-social-github').val(s.social?.github || '');
30
+ $container.find('#field-social-youtube').val(s.social?.youtube || '');
31
+
21
32
  $container.find('#field-smtp-host').val(s.smtp?.host || '');
22
33
  $container.find('#field-smtp-port').val(s.smtp?.port || 587);
23
34
  $container.find('#field-smtp-user').val(s.smtp?.user || '');
@@ -26,11 +37,50 @@ export const settingsView = {
26
37
  $container.find('#field-smtp-from-address').val(s.smtp?.fromAddress || '');
27
38
  $container.find('#field-smtp-from-name').val(s.smtp?.fromName || '');
28
39
 
40
+ // Back to Top
41
+ const btt = s.backToTop || {};
42
+ $container.find('#field-btt-enabled').prop('checked', btt.enabled !== false);
43
+ $container.find('#field-btt-threshold').val(btt.scrollThreshold ?? 300);
44
+ $container.find('#field-btt-position').val(btt.position || 'bottom-right');
45
+ $container.find('#field-btt-offset').val(btt.offset ?? 32);
46
+ $container.find('#field-btt-label').val(btt.label || '');
47
+ $container.find('#field-btt-smooth').prop('checked', btt.smooth !== false);
48
+
49
+ // Cookie Consent
50
+ const cc = s.cookieConsent || {};
51
+ $container.find('#field-cc-enabled').prop('checked', cc.enabled !== false);
52
+ $container.find('#field-cc-message').val(cc.message || '');
53
+ $container.find('#field-cc-accept-all').val(cc.acceptAllText || 'Accept All');
54
+ $container.find('#field-cc-reject-all').val(cc.rejectAllText || 'Reject All');
55
+ $container.find('#field-cc-customize').val(cc.customizeText || 'Customize');
56
+ $container.find('#field-cc-save-prefs').val(cc.savePreferencesText || 'Save Preferences');
57
+ $container.find('#field-cc-privacy-text').val(cc.privacyPolicyText || 'Privacy Policy');
58
+ $container.find('#field-cc-privacy-url').val(cc.privacyPolicyUrl || '');
59
+ $container.find('#field-cc-cookie-text').val(cc.cookiePolicyText || 'Cookie Policy');
60
+ $container.find('#field-cc-cookie-url').val(cc.cookiePolicyUrl || '');
61
+ $container.find('#field-cc-position').val(cc.position || 'bottom');
62
+ $container.find('#field-cc-layout').val(cc.layout || 'bar');
63
+ $container.find('#field-cc-theme').val(cc.theme || 'dark');
64
+ $container.find('#field-cc-show-functional').prop('checked', cc.showFunctional !== false);
65
+ $container.find('#field-cc-show-analytics').prop('checked', cc.showAnalytics !== false);
66
+ $container.find('#field-cc-show-marketing').prop('checked', cc.showMarketing !== false);
67
+ $container.find('#field-cc-version').val(cc.consentVersion || '1.0');
68
+
69
+ // Custom CSS
70
+ try {
71
+ const { css } = await apiRequest('/settings/custom-css');
72
+ $container.find('#field-custom-css').val(css || '');
73
+ } catch {
74
+ // non-fatal — textarea stays empty
75
+ }
76
+
29
77
  $container.find('#save-settings-btn').on('click', async () => {
30
78
  const adminTheme = $container.find('#field-admin-theme').val();
31
79
  const data = {
32
80
  title: $container.find('#field-site-title').val().trim(),
33
81
  tagline: $container.find('#field-tagline').val().trim(),
82
+ fontFamily: $container.find('#field-font-family').val() || 'Roboto',
83
+ fontSize: parseInt($container.find('#field-font-size').val(), 10) || 16,
34
84
  theme: $container.find('#field-theme').val(),
35
85
  adminTheme,
36
86
  seo: {
@@ -42,6 +92,14 @@ export const settingsView = {
42
92
  copyright: $container.find('#field-footer-copy').val().trim(),
43
93
  links: s.footer?.links || []
44
94
  },
95
+ social: {
96
+ twitter: $container.find('#field-social-twitter').val().trim(),
97
+ facebook: $container.find('#field-social-facebook').val().trim(),
98
+ instagram: $container.find('#field-social-instagram').val().trim(),
99
+ linkedin: $container.find('#field-social-linkedin').val().trim(),
100
+ github: $container.find('#field-social-github').val().trim(),
101
+ youtube: $container.find('#field-social-youtube').val().trim()
102
+ },
45
103
  smtp: {
46
104
  host: $container.find('#field-smtp-host').val().trim(),
47
105
  port: parseInt($container.find('#field-smtp-port').val(), 10) || 587,
@@ -50,14 +108,91 @@ export const settingsView = {
50
108
  secure: $container.find('#field-smtp-secure').prop('checked'),
51
109
  fromAddress: $container.find('#field-smtp-from-address').val().trim(),
52
110
  fromName: $container.find('#field-smtp-from-name').val().trim()
111
+ },
112
+ backToTop: {
113
+ enabled: $container.find('#field-btt-enabled').prop('checked'),
114
+ scrollThreshold: parseInt($container.find('#field-btt-threshold').val(), 10) || 300,
115
+ position: $container.find('#field-btt-position').val() || 'bottom-right',
116
+ offset: parseInt($container.find('#field-btt-offset').val(), 10) || 32,
117
+ label: $container.find('#field-btt-label').val().trim(),
118
+ smooth: $container.find('#field-btt-smooth').prop('checked')
119
+ },
120
+ cookieConsent: {
121
+ enabled: $container.find('#field-cc-enabled').prop('checked'),
122
+ message: $container.find('#field-cc-message').val().trim(),
123
+ acceptAllText: $container.find('#field-cc-accept-all').val().trim(),
124
+ rejectAllText: $container.find('#field-cc-reject-all').val().trim(),
125
+ customizeText: $container.find('#field-cc-customize').val().trim(),
126
+ savePreferencesText: $container.find('#field-cc-save-prefs').val().trim(),
127
+ privacyPolicyText: $container.find('#field-cc-privacy-text').val().trim(),
128
+ privacyPolicyUrl: $container.find('#field-cc-privacy-url').val().trim(),
129
+ cookiePolicyText: $container.find('#field-cc-cookie-text').val().trim(),
130
+ cookiePolicyUrl: $container.find('#field-cc-cookie-url').val().trim(),
131
+ position: $container.find('#field-cc-position').val(),
132
+ layout: $container.find('#field-cc-layout').val(),
133
+ theme: $container.find('#field-cc-theme').val(),
134
+ showFunctional: $container.find('#field-cc-show-functional').prop('checked'),
135
+ showAnalytics: $container.find('#field-cc-show-analytics').prop('checked'),
136
+ showMarketing: $container.find('#field-cc-show-marketing').prop('checked'),
137
+ consentVersion: $container.find('#field-cc-version').val().trim() || '1.0'
53
138
  }
54
139
  };
140
+ const $btn = $container.find('#save-settings-btn');
141
+ $btn.prop('disabled', true);
55
142
  try {
56
143
  await api.settings.save(data);
57
144
  Domma.theme.set(adminTheme);
58
145
  E.toast('Settings saved.', { type: 'success' });
59
146
  } catch {
60
147
  E.toast('Failed to save settings.', { type: 'error' });
148
+ } finally {
149
+ $btn.prop('disabled', false);
150
+ }
151
+ });
152
+
153
+ // Send test email
154
+ $container.find('#send-test-email-btn').on('click', async () => {
155
+ const to = $container.find('#field-test-email-to').val().trim();
156
+ const resultEl = $container.find('#test-email-result').get(0);
157
+ const $btn = $container.find('#send-test-email-btn');
158
+
159
+ $btn.prop('disabled', true);
160
+ if (resultEl) { resultEl.textContent = 'Sending…'; resultEl.style.color = ''; }
161
+
162
+ try {
163
+ const res = await apiRequest('/settings/test-email', {
164
+ method: 'POST',
165
+ body: JSON.stringify({ to: to || undefined })
166
+ });
167
+ if (resultEl) {
168
+ resultEl.textContent = res.message || 'Test email sent.';
169
+ resultEl.style.color = 'var(--success,#4ade80)';
170
+ }
171
+ } catch (err) {
172
+ if (resultEl) {
173
+ resultEl.textContent = err.message || 'Failed to send test email.';
174
+ resultEl.style.color = 'var(--danger,#f87171)';
175
+ }
176
+ } finally {
177
+ $btn.prop('disabled', false);
178
+ }
179
+ });
180
+
181
+ // Save Custom CSS
182
+ $container.find('#save-css-btn').on('click', async () => {
183
+ const css = $container.find('#field-custom-css').val();
184
+ const $btn = $container.find('#save-css-btn');
185
+ $btn.prop('disabled', true);
186
+ try {
187
+ await apiRequest('/settings/custom-css', {
188
+ method: 'PUT',
189
+ body: JSON.stringify({ css })
190
+ });
191
+ E.toast('Custom CSS saved.', { type: 'success' });
192
+ } catch (err) {
193
+ E.toast(err.message || 'Failed to save CSS.', { type: 'error' });
194
+ } finally {
195
+ $btn.prop('disabled', false);
61
196
  }
62
197
  });
63
198
  }
@@ -2,7 +2,7 @@
2
2
  * Users List View
3
3
  * Shows all users in a Domma table with role badges.
4
4
  */
5
- import { api, getUser } from '../api.js';
5
+ import {api, getUser} from '../api.js';
6
6
 
7
7
  const ROLE_BADGE = {
8
8
  admin: 'badge-danger',
@@ -22,18 +22,21 @@ export const usersView = {
22
22
  T.create('#users-table', {
23
23
  data,
24
24
  columns: [
25
- { key: 'name', label: 'Name' },
26
- { key: 'email', label: 'Email' },
27
- { key: 'role', label: 'Role',
25
+ {key: 'name', title: 'Name'},
26
+ {key: 'email', title: 'Email'},
27
+ {
28
+ key: 'role', title: 'Role',
28
29
  render: (val) => `<span class="badge ${ROLE_BADGE[val] || 'badge-secondary'}">${val}</span>` },
29
- { key: 'isActive', label: 'Status',
30
+ {
31
+ key: 'isActive', title: 'Status',
30
32
  render: (val) => val
31
33
  ? '<span class="badge badge-success">Active</span>'
32
34
  : '<span class="badge badge-secondary">Inactive</span>' },
33
- { key: 'lastLogin', label: 'Last login',
35
+ {
36
+ key: 'lastLogin', title: 'Last login',
34
37
  render: (val) => val ? D(val).format('DD MMM YYYY HH:mm') : 'Never' },
35
38
  {
36
- key: 'actions', label: 'Actions',
39
+ key: 'actions', title: 'Actions',
37
40
  render: (_, row) => {
38
41
  const isSelf = row.id === currentUser?.id;
39
42
  return `
package/bin/cli.js CHANGED
@@ -152,8 +152,26 @@ done();
152
152
  // ---------------------------------------------------------------------------
153
153
 
154
154
  step('Resetting plugin data');
155
- writeFileSync(path.join(target, 'plugins/example-analytics/stats.json'), '{}\n', 'utf8');
156
- writeFileSync(path.join(target, 'plugins/form-builder/data/submissions/contact.json'), '[]\n', 'utf8');
155
+ // Read scaffold.reset entries from each plugin.json and apply them
156
+ const copiedPluginsDir = path.join(target, 'plugins');
157
+ try {
158
+ const pluginDirs = readdirSync(copiedPluginsDir, {withFileTypes: true})
159
+ .filter(e => e.isDirectory())
160
+ .map(e => e.name);
161
+ for (const pluginName of pluginDirs) {
162
+ const manifestPath = path.join(copiedPluginsDir, pluginName, 'plugin.json');
163
+ try {
164
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
165
+ for (const {path: relPath, content} of (manifest.scaffold?.reset || [])) {
166
+ const absPath = path.join(copiedPluginsDir, pluginName, relPath);
167
+ mkdirSync(path.dirname(absPath), {recursive: true});
168
+ writeFileSync(absPath, content + '\n', 'utf8');
169
+ }
170
+ } catch { /* missing or invalid plugin.json — skip */
171
+ }
172
+ }
173
+ } catch { /* plugins dir not found — skip */
174
+ }
157
175
  done();
158
176
 
159
177
  // ---------------------------------------------------------------------------
@@ -189,14 +207,23 @@ const DEFAULT_NAV = {
189
207
  position: 'sticky'
190
208
  };
191
209
 
192
- // All plugins present but disabled by default in a fresh project
193
- const DEFAULT_PLUGINS = {
194
- 'example-analytics': {enabled: false, settings: {}},
195
- 'contact-form': {enabled: false, settings: {}},
196
- 'form-builder': {enabled: false, settings: {}},
197
- 'back-to-top': {enabled: false, settings: {}},
198
- 'cookie-consent': {enabled: false, settings: {}}
199
- };
210
+ // Discover all plugins and default each to disabled in a fresh project
211
+ const DEFAULT_PLUGINS = {};
212
+ try {
213
+ const pluginsRoot = path.join(target, 'plugins');
214
+ const pluginDirs = readdirSync(pluginsRoot, {withFileTypes: true})
215
+ .filter(e => e.isDirectory())
216
+ .map(e => e.name);
217
+ for (const name of pluginDirs) {
218
+ const manifestPath = path.join(pluginsRoot, name, 'plugin.json');
219
+ try {
220
+ JSON.parse(readFileSync(manifestPath, 'utf8')); // validate manifest exists
221
+ DEFAULT_PLUGINS[name] = {enabled: false, settings: {}};
222
+ } catch { /* no valid plugin.json — skip */
223
+ }
224
+ }
225
+ } catch { /* plugins dir not found — leave empty */
226
+ }
200
227
 
201
228
  step('Writing default config');
202
229
  writeFileSync(path.join(target, 'config/site.json'), JSON.stringify(DEFAULT_SITE, null, 4) + '\n', 'utf8');
package/config/auth.json CHANGED
@@ -15,6 +15,7 @@
15
15
  "layouts": ["admin", "manager"],
16
16
  "media": ["admin", "manager", "editor"],
17
17
  "users": ["admin", "manager"],
18
- "plugins": ["admin"]
18
+ "plugins": ["admin"],
19
+ "collections": ["admin", "manager"]
19
20
  }
20
21
  }
@@ -2,6 +2,7 @@
2
2
  "contentDir": "./content",
3
3
  "mediaDir": "./content/media",
4
4
  "usersDir": "./content/users",
5
+ "collectionsDir": "./content/collections",
5
6
  "pageDefaults": {
6
7
  "layout": "default",
7
8
  "status": "draft",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "brand": {
3
- "text": "My Site",
4
- "logo": null,
5
- "url": "/"
3
+ "text": "My Dead Good Site",
4
+ "url": "/",
5
+ "icon": "home"
6
6
  },
7
7
  "items": [
8
8
  {
@@ -54,10 +54,20 @@
54
54
  "text": "Shortcode Reference",
55
55
  "url": "/resources/shortcodes",
56
56
  "icon": "code"
57
+ },
58
+ {
59
+ "text": "Effects",
60
+ "url": "/resources/effects",
61
+ "icon": "zap"
62
+ },
63
+ {
64
+ "text": "Interactive",
65
+ "url": "/resources/interactive",
66
+ "icon": "mouse-pointer"
57
67
  }
58
68
  ]
59
69
  }
60
70
  ],
61
- "variant": "dark",
71
+ "variant": "transparent",
62
72
  "position": "sticky"
63
73
  }
@@ -17,24 +17,6 @@
17
17
  "fromName": "Website Forms"
18
18
  }
19
19
  },
20
- "back-to-top": {
21
- "enabled": true,
22
- "settings": {
23
- "scrollThreshold": 150,
24
- "position": "bottom-right",
25
- "label": "",
26
- "smooth": true,
27
- "offset": 32
28
- }
29
- },
30
- "cookie-consent": {
31
- "enabled": true,
32
- "settings": {}
33
- },
34
- "custom-css": {
35
- "enabled": true,
36
- "settings": {}
37
- },
38
20
  "domma-effects": {
39
21
  "enabled": true,
40
22
  "settings": {
@@ -4,31 +4,27 @@
4
4
  "description": "Standard page with navbar and footer",
5
5
  "navbar": true,
6
6
  "footer": true,
7
- "sidebar": false,
8
- "features": ["back-to-top", "icon-scan"]
7
+ "sidebar": false
9
8
  },
10
9
  "with-sidebar": {
11
10
  "label": "With Sidebar",
12
11
  "description": "Page with navbar, sidebar, and footer",
13
12
  "navbar": true,
14
13
  "footer": true,
15
- "sidebar": true,
16
- "features": ["back-to-top", "icon-scan", "scroll-spy"]
14
+ "sidebar": true
17
15
  },
18
16
  "minimal": {
19
17
  "label": "Minimal",
20
18
  "description": "Clean page with no navbar or footer",
21
19
  "navbar": false,
22
20
  "footer": false,
23
- "sidebar": false,
24
- "features": []
21
+ "sidebar": false
25
22
  },
26
23
  "landing": {
27
24
  "label": "Landing Page",
28
25
  "description": "Full-width landing page layout",
29
26
  "navbar": true,
30
27
  "footer": true,
31
- "sidebar": false,
32
- "features": ["back-to-top", "icon-scan"]
28
+ "sidebar": false
33
29
  }
34
30
  }
package/config/site.json CHANGED
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "title": "My Boss Site",
3
- "tagline": "My Dead Good Boss Site",
4
- "theme": "silver-dark",
3
+ "tagline": "My Dead Good Site",
4
+ "fontFamily": "Roboto",
5
+ "fontSize": 16,
6
+ "theme": "charcoal-dark",
5
7
  "adminTheme": "charcoal-dark",
6
8
  "seo": {
7
9
  "defaultTitle": "My Boss Site",
@@ -9,7 +11,7 @@
9
11
  "defaultDescription": "A site built with Domma CMS"
10
12
  },
11
13
  "footer": {
12
- "copyright": "© 2026 DCBW Consulting Ltd.",
14
+ "copyright": "© 2026 DCBW Consulting Ltd",
13
15
  "links": [
14
16
  {
15
17
  "text": "Privacy Policy",
@@ -18,9 +20,21 @@
18
20
  {
19
21
  "text": "Contact",
20
22
  "url": "/contact"
23
+ },
24
+ {
25
+ "text": "GDPR",
26
+ "url": "/gdpr"
21
27
  }
22
28
  ]
23
29
  },
30
+ "social": {
31
+ "twitter": "",
32
+ "facebook": "",
33
+ "instagram": "",
34
+ "linkedin": "",
35
+ "github": "https://github.com/pinpointzero73/",
36
+ "youtube": ""
37
+ },
24
38
  "smtp": {
25
39
  "host": "localhost",
26
40
  "port": 1025,
@@ -29,5 +43,32 @@
29
43
  "secure": false,
30
44
  "fromAddress": "",
31
45
  "fromName": ""
46
+ },
47
+ "backToTop": {
48
+ "enabled": true,
49
+ "scrollThreshold": 150,
50
+ "position": "bottom-right",
51
+ "label": "",
52
+ "smooth": true,
53
+ "offset": 48
54
+ },
55
+ "cookieConsent": {
56
+ "enabled": true,
57
+ "message": "We use cookies to enhance your browsing experience, serve personalised content, and analyse our traffic. By clicking \"Accept All\", you consent to our use of cookies.",
58
+ "acceptAllText": "Accept All",
59
+ "rejectAllText": "Reject All",
60
+ "customizeText": "Customize",
61
+ "savePreferencesText": "Save Preferences",
62
+ "privacyPolicyText": "Privacy Policy",
63
+ "privacyPolicyUrl": "/privacy-policy",
64
+ "cookiePolicyText": "Cookie Policy",
65
+ "cookiePolicyUrl": "",
66
+ "position": "bottom",
67
+ "layout": "bar",
68
+ "theme": "dark",
69
+ "showFunctional": true,
70
+ "showAnalytics": true,
71
+ "showMarketing": true,
72
+ "consentVersion": "1.0"
32
73
  }
33
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -17,6 +17,7 @@
17
17
  "scripts/"
18
18
  ],
19
19
  "scripts": {
20
+ "build": "node scripts/build.js",
20
21
  "start": "pm2 start server/server.js -i max --name domma-cms",
21
22
  "dev": "PORT=3050 node --watch server/server.js",
22
23
  "prod": "node server/server.js",
@@ -54,7 +55,7 @@
54
55
  "@fastify/multipart": "^9.3.0",
55
56
  "@fastify/static": "^8.1.0",
56
57
  "bcryptjs": "^3.0.3",
57
- "domma-js": "^0.18.2",
58
+ "domma-js": "^0.19.1",
58
59
  "dotenv": "^17.2.3",
59
60
  "fastify": "^5.7.3",
60
61
  "gray-matter": "^4.0.3",
@@ -63,5 +64,8 @@
63
64
  "sanitize-html": "^2.17.0",
64
65
  "sharp": "^0.34.5",
65
66
  "uuid": "^13.0.0"
67
+ },
68
+ "devDependencies": {
69
+ "esbuild": "^0.27.3"
66
70
  }
67
71
  }
@@ -176,12 +176,101 @@ Content here.
176
176
  <!-- Text tab -->
177
177
  <div id="tab-text" class="effects-tab-panel">
178
178
  <h3 class="mb-2">Typewriter (Scribe)</h3>
179
- <p class="text-muted mb-2">Types text character by character. Perfect for hero taglines or code
180
- demonstrations.</p>
181
- <pre class="code-block mb-3">[scribe speed="50" cursor="true" loop="false"]
179
+ <p class="text-muted mb-2">Types text character by character. Supports a simple one-shot mode or a full
180
+ action script for sequenced multi-step animations.</p>
181
+
182
+ <h4 class="mb-1" style="font-size:.9rem;">Simple mode</h4>
183
+ <pre class="code-block mb-2">[scribe speed="50" cursor="true" loop="false"]
182
184
  Your text appears letter by letter.
183
185
  [/scribe]</pre>
184
186
 
187
+ <h4 class="mb-1" style="font-size:.9rem;">Script mode — action shortcodes</h4>
188
+ <pre class="code-block mb-2">[scribe loop="true" loop-delay="2000"]
189
+ [render effect="fade"]Hello, world![/render]
190
+ [wait]1500[/wait]
191
+ [undo /]
192
+ [render]Something else entirely.[/render]
193
+ [/scribe]</pre>
194
+ <p class="text-muted mb-2" style="font-size:.85rem;">When any <code>[render]</code>, <code>[wait]</code>, or
195
+ <code>[undo]</code> shortcode is present inside the body, script mode activates automatically.</p>
196
+ <table class="table mb-2">
197
+ <thead>
198
+ <tr>
199
+ <th>Action</th>
200
+ <th>Syntax</th>
201
+ <th>Description</th>
202
+ </tr>
203
+ </thead>
204
+ <tbody>
205
+ <tr>
206
+ <td><code>[render]</code></td>
207
+ <td><code>[render effect="fade"]Text[/render]</code></td>
208
+ <td>Type the text. Optional <code>effect</code>: fade, slide (for appearance)</td>
209
+ </tr>
210
+ <tr>
211
+ <td><code>[wait]</code></td>
212
+ <td><code>[wait]1500[/wait]</code></td>
213
+ <td>Pause for N milliseconds (number) or a CSS duration string (e.g. <code>2s</code>)</td>
214
+ </tr>
215
+ <tr>
216
+ <td><code>[undo]</code></td>
217
+ <td><code>[undo /]</code> or <code>[undo count="3" /]</code> or <code>[undo all="true" /]</code></td>
218
+ <td>Delete the last render action, N renders, or all renders</td>
219
+ </tr>
220
+ </tbody>
221
+ </table>
222
+ <table class="table mb-3">
223
+ <thead>
224
+ <tr>
225
+ <th>Attribute</th>
226
+ <th>Default</th>
227
+ <th>Description</th>
228
+ </tr>
229
+ </thead>
230
+ <tbody>
231
+ <tr>
232
+ <td><code>speed</code></td>
233
+ <td>50</td>
234
+ <td>Typing speed (ms per character)</td>
235
+ </tr>
236
+ <tr>
237
+ <td><code>delete-speed</code></td>
238
+ <td>30</td>
239
+ <td>Deletion speed (ms per character)</td>
240
+ </tr>
241
+ <tr>
242
+ <td><code>cursor</code></td>
243
+ <td>true</td>
244
+ <td>Show blinking cursor (true/false)</td>
245
+ </tr>
246
+ <tr>
247
+ <td><code>cursor-char</code></td>
248
+ <td>|</td>
249
+ <td>Character to use as cursor</td>
250
+ </tr>
251
+ <tr>
252
+ <td><code>cursor-type</code></td>
253
+ <td>—</td>
254
+ <td>Cursor style (block, underline, beam)</td>
255
+ </tr>
256
+ <tr>
257
+ <td><code>loop</code></td>
258
+ <td>false</td>
259
+ <td>Repeat script continuously (true/false)</td>
260
+ </tr>
261
+ <tr>
262
+ <td><code>loop-delay</code></td>
263
+ <td>1000</td>
264
+ <td>Pause before repeating (ms)</td>
265
+ </tr>
266
+ <tr>
267
+ <td><code>pause-on-hover</code></td>
268
+ <td>false</td>
269
+ <td>Pause animation while hovered (true/false)</td>
270
+ </tr>
271
+ </tbody>
272
+ </table>
273
+
185
274
  <h3 class="mb-2">Scramble</h3>
186
275
  <p class="text-muted mb-2">Reveals text through a character-scramble animation. Great for tech/data
187
276
  aesthetics.</p>