domma-cms 1.6.60 → 1.7.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 (137) hide show
  1. package/CHANGELOG.md +4344 -0
  2. package/README.md +1 -1
  3. package/admin/dist/domma/domma-tools.css +3 -3
  4. package/admin/dist/domma/domma-tools.min.js +3 -3
  5. package/admin/js/lib/effect-defs.js +1 -1
  6. package/admin/js/lib/effects-builder.js +2 -2
  7. package/admin/js/lib/page-editor-reference.js +5 -5
  8. package/admin/js/lib/shortcode-tags.js +1 -1
  9. package/admin/js/templates/docs/usage-shortcodes.html +39 -0
  10. package/docs/DommaFeatures.md +3005 -0
  11. package/docs/MultiTenant.md +176 -0
  12. package/docs/api-reference.md +1667 -0
  13. package/docs/cache.md +96 -0
  14. package/docs/configuration.md +1811 -0
  15. package/docs/editions-and-licences.md +190 -0
  16. package/docs/getting-started.md +201 -0
  17. package/docs/guides/form-triggers.md +260 -0
  18. package/docs/guides/mongodb-storage-adapter.md +282 -0
  19. package/docs/guides/multi-page-forms.md +121 -0
  20. package/docs/images/scribe-composer.png +0 -0
  21. package/docs/markdown-shortcodes.md +1738 -0
  22. package/docs/pentest-report-2026-04-03.md +181 -0
  23. package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
  24. package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
  25. package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
  26. package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
  27. package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
  28. package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
  29. package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
  30. package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
  31. package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
  32. package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
  33. package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
  34. package/docs/plans/2026-03-05-user-types-design.md +139 -0
  35. package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
  36. package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
  37. package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
  38. package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
  39. package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
  40. package/docs/plans/2026-06-10-core-project-design.md +240 -0
  41. package/docs/plugin-development.md +827 -0
  42. package/docs/recruitment-recipe.md +501 -0
  43. package/docs/scaffolding.md +312 -0
  44. package/docs/screenshots/calculator-floating.png +0 -0
  45. package/docs/screenshots/calculator-standard.png +0 -0
  46. package/docs/screenshots/email-settings.png +0 -0
  47. package/docs/screenshots/messages-count.png +0 -0
  48. package/docs/structure/user-types.md +107 -0
  49. package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
  50. package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
  51. package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
  52. package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
  53. package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
  54. package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
  55. package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
  56. package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
  57. package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
  58. package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
  59. package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
  60. package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
  61. package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
  62. package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
  63. package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
  64. package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
  65. package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
  66. package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
  67. package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
  68. package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
  69. package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
  70. package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
  71. package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
  72. package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
  73. package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
  74. package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
  75. package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
  76. package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
  77. package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
  78. package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
  79. package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
  80. package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
  81. package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
  82. package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
  83. package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
  84. package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
  85. package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
  86. package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
  87. package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
  88. package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
  89. package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
  90. package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
  91. package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
  92. package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
  93. package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
  94. package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
  95. package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
  96. package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
  97. package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
  98. package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
  99. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
  100. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
  101. package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
  102. package/docs/theming.md +320 -0
  103. package/package.json +2 -2
  104. package/public/js/celebrations/core/canvas.js +1 -1
  105. package/public/js/celebrations/index.js +1 -1
  106. package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
  107. package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
  108. package/public/js/celebrations/message-attrs.mjs +1 -0
  109. package/public/js/celebrations/themes/anniversary.js +1 -0
  110. package/public/js/celebrations/themes/autumn.js +1 -0
  111. package/public/js/celebrations/themes/birthday.js +1 -0
  112. package/public/js/celebrations/themes/butterfly-flight.js +1 -0
  113. package/public/js/celebrations/themes/calendar.js +1 -0
  114. package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
  115. package/public/js/celebrations/themes/chinese-new-year.js +1 -0
  116. package/public/js/celebrations/themes/christmas.js +1 -1
  117. package/public/js/celebrations/themes/easter.js +1 -0
  118. package/public/js/celebrations/themes/garden-drawing.js +1 -0
  119. package/public/js/celebrations/themes/garden.js +1 -0
  120. package/public/js/celebrations/themes/graduation.js +1 -0
  121. package/public/js/celebrations/themes/greeting.js +1 -0
  122. package/public/js/celebrations/themes/make-it-rain.js +1 -0
  123. package/public/js/celebrations/themes/money-rain.js +1 -0
  124. package/public/js/celebrations/themes/new-year.js +1 -0
  125. package/public/js/celebrations/themes/occasion-drawing.js +1 -0
  126. package/public/js/celebrations/themes/occasions.js +1 -0
  127. package/public/js/celebrations/themes/rain-control.js +1 -0
  128. package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
  129. package/public/js/celebrations/themes/sparkler-font.js +1 -0
  130. package/public/js/celebrations/themes/sparkler-writing.js +1 -0
  131. package/public/js/celebrations/themes/summer-garden.js +1 -0
  132. package/public/js/celebrations/themes/winter.js +1 -0
  133. package/public/js/celebrations/trait-data.mjs +1 -1
  134. package/public/js/effects.js +1 -1
  135. package/scripts/build.js +4 -0
  136. package/server/services/markdown.js +5 -2
  137. package/server/templates/page.html +1 -1
@@ -0,0 +1,453 @@
1
+ # Cookie Consent Plugin
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Build a CMS plugin that injects Domma's built-in cookie consent banner into every public page, with all
6
+ wording, appearance, and category settings configurable from the admin panel.
7
+
8
+ **Architecture:** Follows the exact `back-to-top` and `contact-form` plugin structure. Server exposes `GET /settings` (
9
+ public, called by the IIFE) and `PUT /settings` (admin-auth). The public `inject-body.html` IIFE fetches settings and
10
+ calls `Domma.components.cookieConsent(cfg)` - Domma is already loaded on all public pages before `{{bodyEndInject}}` is
11
+ injected, so no extra dependencies are needed.
12
+
13
+ **Tech Stack:** Fastify 5 (ESM), Domma SPA (admin view), `Domma.components.cookieConsent()` (public banner).
14
+
15
+ ---
16
+
17
+ ## Task 1: Create plugin.json
18
+
19
+ **File:** `plugins/cookie-consent/plugin.json`
20
+
21
+ ```json
22
+ {
23
+ "name": "cookie-consent",
24
+ "displayName": "Cookie Consent",
25
+ "version": "1.0.0",
26
+ "description": "Injects a configurable GDPR cookie consent banner into every public page.",
27
+ "author": "Darryl Waterhouse",
28
+ "date": "2026-03-02",
29
+ "icon": "shield",
30
+ "admin": {
31
+ "sidebar": [
32
+ { "id": "cookie-consent", "text": "Cookie Consent", "icon": "shield", "url": "#/plugins/cookie-consent", "section": "#/plugins/cookie-consent" }
33
+ ],
34
+ "routes": [
35
+ { "path": "/plugins/cookie-consent", "view": "plugin-cookie-consent-settings", "title": "Cookie Consent - Domma CMS" }
36
+ ],
37
+ "views": {
38
+ "plugin-cookie-consent-settings": {
39
+ "entry": "cookie-consent/admin/views/cookie-consent-settings.js",
40
+ "exportName": "cookieConsentSettingsView"
41
+ }
42
+ }
43
+ },
44
+ "inject": {
45
+ "bodyEnd": "public/inject-body.html"
46
+ }
47
+ }
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Task 2: Create config.js
53
+
54
+ **File:** `plugins/cookie-consent/config.js`
55
+
56
+ ```js
57
+ /**
58
+ * Cookie Consent Plugin - Default Configuration
59
+ */
60
+ export default {
61
+ // Wording
62
+ 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.',
63
+ acceptAllText: 'Accept All',
64
+ rejectAllText: 'Reject All',
65
+ customizeText: 'Customize',
66
+ savePreferencesText: 'Save Preferences',
67
+
68
+ // Policy links (empty cookiePolicyUrl hides the cookie policy link)
69
+ privacyPolicyText: 'Privacy Policy',
70
+ privacyPolicyUrl: '/privacy-policy',
71
+ cookiePolicyText: 'Cookie Policy',
72
+ cookiePolicyUrl: '',
73
+
74
+ // Appearance
75
+ position: 'bottom',
76
+ layout: 'bar',
77
+ theme: 'dark',
78
+
79
+ // Categories (Necessary is always shown and always required)
80
+ showFunctional: true,
81
+ showAnalytics: true,
82
+ showMarketing: true,
83
+
84
+ // Consent version - increment to re-prompt users who already accepted
85
+ consentVersion: '1.0'
86
+ };
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Task 3: Create plugin.js (server)
92
+
93
+ **File:** `plugins/cookie-consent/plugin.js`
94
+
95
+ ```js
96
+ /**
97
+ * Cookie Consent Plugin - Server
98
+ *
99
+ * Endpoints (prefix: /api/plugins/cookie-consent):
100
+ * GET /settings - public (called by inject-body IIFE)
101
+ * PUT /settings - admin-auth - saves user overrides
102
+ */
103
+ import { getPluginSettings, savePluginState } from '../../server/services/plugins.js';
104
+
105
+ export default async function cookieConsentPlugin(fastify, options) {
106
+ const { authenticate, requireAdmin } = options.auth;
107
+
108
+ // GET /settings - no auth, public read
109
+ fastify.get('/settings', async () => {
110
+ return getPluginSettings('cookie-consent');
111
+ });
112
+
113
+ // PUT /settings - admin only
114
+ fastify.put('/settings', { preHandler: [authenticate, requireAdmin] }, async (request) => {
115
+ const body = request.body || {};
116
+ savePluginState('cookie-consent', { settings: body });
117
+ return { ok: true };
118
+ });
119
+ }
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Task 4: Create admin HTML template
125
+
126
+ **File:** `plugins/cookie-consent/admin/templates/cookie-consent-settings.html`
127
+
128
+ ```html
129
+ <div class="view-header">
130
+ <h1><span data-icon="shield"></span> Cookie Consent</h1>
131
+ <div>
132
+ <button id="save-settings-btn" class="btn btn-primary">
133
+ <span data-icon="save"></span> Save
134
+ </button>
135
+ </div>
136
+ </div>
137
+
138
+ <div class="card mb-4">
139
+ <div class="card-header"><h2>Banner Wording</h2></div>
140
+ <div class="card-body">
141
+ <div class="row mb-3">
142
+ <div class="col">
143
+ <label class="form-label">Message</label>
144
+ <textarea id="field-message" class="form-input" rows="3"></textarea>
145
+ </div>
146
+ </div>
147
+ <div class="row mb-3">
148
+ <div class="col-6">
149
+ <label class="form-label">Accept All button</label>
150
+ <input id="field-accept-all-text" type="text" class="form-input">
151
+ </div>
152
+ <div class="col-6">
153
+ <label class="form-label">Reject All button</label>
154
+ <input id="field-reject-all-text" type="text" class="form-input">
155
+ </div>
156
+ </div>
157
+ <div class="row mb-3">
158
+ <div class="col-6">
159
+ <label class="form-label">Customize button</label>
160
+ <input id="field-customize-text" type="text" class="form-input">
161
+ </div>
162
+ <div class="col-6">
163
+ <label class="form-label">Save Preferences button</label>
164
+ <input id="field-save-preferences-text" type="text" class="form-input">
165
+ </div>
166
+ </div>
167
+ <div class="row mb-3">
168
+ <div class="col-6">
169
+ <label class="form-label">Privacy Policy link text</label>
170
+ <input id="field-privacy-policy-text" type="text" class="form-input">
171
+ </div>
172
+ <div class="col-6">
173
+ <label class="form-label">Privacy Policy URL</label>
174
+ <input id="field-privacy-policy-url" type="text" class="form-input" placeholder="/privacy-policy">
175
+ </div>
176
+ </div>
177
+ <div class="row">
178
+ <div class="col-6">
179
+ <label class="form-label">Cookie Policy link text</label>
180
+ <input id="field-cookie-policy-text" type="text" class="form-input">
181
+ </div>
182
+ <div class="col-6">
183
+ <label class="form-label">Cookie Policy URL <span class="text-muted">(leave empty to hide)</span></label>
184
+ <input id="field-cookie-policy-url" type="text" class="form-input" placeholder="/cookie-policy">
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+
190
+ <div class="card mb-4">
191
+ <div class="card-header"><h2>Appearance &amp; Categories</h2></div>
192
+ <div class="card-body">
193
+ <div class="row mb-3">
194
+ <div class="col-4">
195
+ <label class="form-label">Position</label>
196
+ <select id="field-position" class="form-select">
197
+ <option value="bottom">Bottom</option>
198
+ <option value="top">Top</option>
199
+ </select>
200
+ </div>
201
+ <div class="col-4">
202
+ <label class="form-label">Layout</label>
203
+ <select id="field-layout" class="form-select">
204
+ <option value="bar">Bar</option>
205
+ <option value="modal">Modal</option>
206
+ </select>
207
+ </div>
208
+ <div class="col-4">
209
+ <label class="form-label">Theme</label>
210
+ <select id="field-theme" class="form-select">
211
+ <option value="dark">Dark</option>
212
+ <option value="light">Light</option>
213
+ </select>
214
+ </div>
215
+ </div>
216
+ <div class="row mb-3">
217
+ <div class="col">
218
+ <label class="form-label">Cookie categories shown in Customize panel</label>
219
+ <span class="form-hint">Necessary cookies are always shown and always required.</span>
220
+ <div style="margin-top:.75rem;display:flex;flex-direction:column;gap:.5rem;">
221
+ <label class="form-check-label">
222
+ <input id="field-show-functional" type="checkbox"> Functional Cookies
223
+ </label>
224
+ <label class="form-check-label">
225
+ <input id="field-show-analytics" type="checkbox"> Analytics Cookies
226
+ </label>
227
+ <label class="form-check-label">
228
+ <input id="field-show-marketing" type="checkbox"> Marketing Cookies
229
+ </label>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ <div class="row">
234
+ <div class="col-4">
235
+ <label class="form-label">Consent version</label>
236
+ <input id="field-consent-version" type="text" class="form-input" placeholder="1.0">
237
+ <span class="form-hint">Increment to re-prompt users who already accepted.</span>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ ```
243
+
244
+ ---
245
+
246
+ ## Task 5: Create admin settings view
247
+
248
+ **File:** `plugins/cookie-consent/admin/views/cookie-consent-settings.js`
249
+
250
+ ```js
251
+ /**
252
+ * Cookie Consent Plugin - Admin Settings View
253
+ */
254
+ export const cookieConsentSettingsView = {
255
+ templateUrl: '/plugins/cookie-consent/admin/templates/cookie-consent-settings.html',
256
+
257
+ async onMount($container) {
258
+ let settings = {};
259
+ try {
260
+ const res = await fetch('/api/plugins/cookie-consent/settings', {
261
+ headers: { 'Authorization': 'Bearer ' + (S.get('auth_token') || '') }
262
+ });
263
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
264
+ settings = await res.json();
265
+ } catch {
266
+ E.toast('Could not load settings.', { type: 'error' });
267
+ }
268
+
269
+ // Wording
270
+ $container.find('#field-message').val(settings.message || '');
271
+ $container.find('#field-accept-all-text').val(settings.acceptAllText || '');
272
+ $container.find('#field-reject-all-text').val(settings.rejectAllText || '');
273
+ $container.find('#field-customize-text').val(settings.customizeText || '');
274
+ $container.find('#field-save-preferences-text').val(settings.savePreferencesText || '');
275
+ $container.find('#field-privacy-policy-text').val(settings.privacyPolicyText || '');
276
+ $container.find('#field-privacy-policy-url').val(settings.privacyPolicyUrl || '');
277
+ $container.find('#field-cookie-policy-text').val(settings.cookiePolicyText || '');
278
+ $container.find('#field-cookie-policy-url').val(settings.cookiePolicyUrl || '');
279
+
280
+ // Appearance
281
+ $container.find('#field-position').val(settings.position || 'bottom');
282
+ $container.find('#field-layout').val(settings.layout || 'bar');
283
+ $container.find('#field-theme').val(settings.theme || 'dark');
284
+
285
+ // Categories
286
+ $container.find('#field-show-functional').prop('checked', settings.showFunctional !== false);
287
+ $container.find('#field-show-analytics').prop('checked', settings.showAnalytics !== false);
288
+ $container.find('#field-show-marketing').prop('checked', settings.showMarketing !== false);
289
+
290
+ // Version
291
+ $container.find('#field-consent-version').val(settings.consentVersion || '1.0');
292
+
293
+ $container.find('#save-settings-btn').off('click').on('click', async () => {
294
+ const data = {
295
+ message: $container.find('#field-message').val().trim(),
296
+ acceptAllText: $container.find('#field-accept-all-text').val().trim(),
297
+ rejectAllText: $container.find('#field-reject-all-text').val().trim(),
298
+ customizeText: $container.find('#field-customize-text').val().trim(),
299
+ savePreferencesText: $container.find('#field-save-preferences-text').val().trim(),
300
+ privacyPolicyText: $container.find('#field-privacy-policy-text').val().trim(),
301
+ privacyPolicyUrl: $container.find('#field-privacy-policy-url').val().trim(),
302
+ cookiePolicyText: $container.find('#field-cookie-policy-text').val().trim(),
303
+ cookiePolicyUrl: $container.find('#field-cookie-policy-url').val().trim(),
304
+ position: $container.find('#field-position').val(),
305
+ layout: $container.find('#field-layout').val(),
306
+ theme: $container.find('#field-theme').val(),
307
+ showFunctional: $container.find('#field-show-functional').prop('checked'),
308
+ showAnalytics: $container.find('#field-show-analytics').prop('checked'),
309
+ showMarketing: $container.find('#field-show-marketing').prop('checked'),
310
+ consentVersion: $container.find('#field-consent-version').val().trim() || '1.0'
311
+ };
312
+ try {
313
+ const res = await fetch('/api/plugins/cookie-consent/settings', {
314
+ method: 'PUT',
315
+ headers: {
316
+ 'Content-Type': 'application/json',
317
+ 'Authorization': 'Bearer ' + (S.get('auth_token') || '')
318
+ },
319
+ body: JSON.stringify(data)
320
+ });
321
+ if (!res.ok) throw new Error('Save failed');
322
+ E.toast('Settings saved.', { type: 'success' });
323
+ } catch {
324
+ E.toast('Failed to save settings.', { type: 'error' });
325
+ }
326
+ });
327
+
328
+ Domma.icons.scan();
329
+ }
330
+ };
331
+ ```
332
+
333
+ ---
334
+
335
+ ## Task 6: Create public inject-body.html
336
+
337
+ **File:** `plugins/cookie-consent/public/inject-body.html`
338
+
339
+ `Domma.components.cookieConsent()` is available because `domma.min.js` is loaded and `Domma.init()` runs before
340
+ `{{bodyEndInject}}` in `server/templates/page.html`.
341
+
342
+ The IIFE maps the flat config to the Domma component's `categories` object - only including categories that are enabled
343
+ in settings. Necessary is always included.
344
+
345
+ ```html
346
+ <script>
347
+ (function () {
348
+ 'use strict';
349
+
350
+ if (!window.Domma || !window.Domma.components || !window.Domma.components.cookieConsent) return;
351
+
352
+ fetch('/api/plugins/cookie-consent/settings')
353
+ .then(function (r) { return r.json(); })
354
+ .then(function (s) {
355
+ var categories = {
356
+ necessary: {
357
+ label: 'Necessary Cookies',
358
+ description: 'These cookies are essential for the website to function properly.',
359
+ required: true
360
+ }
361
+ };
362
+
363
+ if (s.showFunctional !== false) {
364
+ categories.functional = {
365
+ label: 'Functional Cookies',
366
+ description: 'These cookies enable personalised features and functionality.',
367
+ required: false
368
+ };
369
+ }
370
+
371
+ if (s.showAnalytics !== false) {
372
+ categories.analytics = {
373
+ label: 'Analytics Cookies',
374
+ description: 'These cookies help us understand how visitors interact with our website.',
375
+ required: false
376
+ };
377
+ }
378
+
379
+ if (s.showMarketing !== false) {
380
+ categories.marketing = {
381
+ label: 'Marketing Cookies',
382
+ description: 'These cookies are used to deliver relevant ads and marketing campaigns.',
383
+ required: false
384
+ };
385
+ }
386
+
387
+ var cfg = {
388
+ message: s.message,
389
+ acceptAllText: s.acceptAllText,
390
+ rejectAllText: s.rejectAllText,
391
+ customizeText: s.customizeText,
392
+ savePreferencesText: s.savePreferencesText,
393
+ privacyPolicyText: s.privacyPolicyText,
394
+ privacyPolicyUrl: s.privacyPolicyUrl || null,
395
+ cookiePolicyText: s.cookiePolicyText,
396
+ cookiePolicyUrl: s.cookiePolicyUrl || null,
397
+ position: s.position || 'bottom',
398
+ layout: s.layout || 'bar',
399
+ theme: s.theme || 'dark',
400
+ categories: categories,
401
+ consentVersion: s.consentVersion || '1.0',
402
+ autoShow: true,
403
+ animation: true
404
+ };
405
+
406
+ Domma.components.cookieConsent(cfg);
407
+ })
408
+ .catch(function () {
409
+ // Settings fetch failed - don't show banner
410
+ });
411
+ })();
412
+ </script>
413
+ ```
414
+
415
+ ---
416
+
417
+ ## Task 7: Enable plugin in config/plugins.json
418
+
419
+ **Modify:** `config/plugins.json`
420
+
421
+ Add the `cookie-consent` entry:
422
+
423
+ ```json
424
+ {
425
+ "example-analytics": { "enabled": true, "settings": {} },
426
+ "contact-form": { "enabled": true, "settings": {} },
427
+ "back-to-top": { "enabled": true, "settings": {} },
428
+ "cookie-consent": { "enabled": true, "settings": {} }
429
+ }
430
+ ```
431
+
432
+ ---
433
+
434
+ ## Verification
435
+
436
+ Restart the server (`node server/server.js`) and check:
437
+
438
+ 1. **Server log** - `[plugins] Loaded 4 plugins: back-to-top, contact-form, cookie-consent, example-analytics`
439
+
440
+ 2. **Settings endpoint** - `curl http://localhost:4096/api/plugins/cookie-consent/settings` returns the full defaults
441
+ JSON.
442
+
443
+ 3. **Public injection** - `curl http://localhost:4096/ | grep cookie-consent` shows the IIFE script in the page HTML.
444
+
445
+ 4. **Banner appears** - visit the public site in a browser (clear localStorage first). The cookie consent bar should
446
+ appear at the bottom of the page. Clicking "Customize" should open the category panel with Necessary, Functional,
447
+ Analytics, and Marketing options.
448
+
449
+ 5. **Admin settings** - navigate to `#/plugins/cookie-consent` in the admin. The settings form should load with all
450
+ defaults populated. Change the message, save, reload the public site - the new message should appear.
451
+
452
+ 6. **PUT protected** - `curl -X PUT http://localhost:4096/api/plugins/cookie-consent/settings` without a token returns
453
+ `401`.
@@ -0,0 +1,126 @@
1
+ # Tooltip Sweep & Back-to-Top Plugin
2
+
3
+ **Date:** 2026-03-02
4
+
5
+ ---
6
+
7
+ ## 1. Tooltip Sweep
8
+
9
+ ### Problem
10
+
11
+ Icon-only interactive buttons across the admin have no accessible labels or hover hints. The sidebar and topbar already
12
+ have tooltips; the rest of the UI does not.
13
+
14
+ ### Scope
15
+
16
+ Add `data-tooltip` to icon-only buttons in:
17
+
18
+ - **Pages table** - Edit, Delete row actions (rendered by `pages.js` via `T.create`)
19
+ - **User editor** - Save/Cancel header buttons (icon + text, but Save has an icon)
20
+ - **Navigation view** - Add/Delete/move row action buttons
21
+ - **Page editor header** - Save/Cancel buttons already have text; no change needed
22
+ - **Plugin cards** - Toggle enable/disable buttons (if icon-only)
23
+
24
+ Out of scope: buttons that already have visible text labels.
25
+
26
+ ### Implementation
27
+
28
+ Each view that renders icon-only actions via `T.create` columns or dynamic HTML passes `data-tooltip="..."` on the
29
+ element, then calls `E.tooltip('[data-tooltip]', { placement: 'top' })` at the end of the render function - matching the
30
+ pattern already used in `media.js`.
31
+
32
+ ---
33
+
34
+ ## 2. Back-to-Top Plugin
35
+
36
+ ### Overview
37
+
38
+ A lightweight CMS plugin that injects a scroll-to-top button into every public page. Configurable from the admin panel.
39
+ Follows the established plugin pattern (contact-form as reference).
40
+
41
+ ### File Structure
42
+
43
+ ```
44
+ plugins/back-to-top/
45
+ ├── plugin.json - metadata, admin routes, inject references
46
+ ├── plugin.js - Fastify plugin: GET/PUT /settings
47
+ ├── config.js - default settings
48
+ ├── admin/
49
+ │ └── views/
50
+ │ └── back-to-top-settings.js - admin SPA view
51
+ │ └── templates/
52
+ │ └── back-to-top-settings.html - admin template
53
+ └── public/
54
+ └── inject-body.html - IIFE injected at </body> on every public page
55
+ ```
56
+
57
+ ### Config Defaults (`config.js`)
58
+
59
+ | Key | Default | Description |
60
+ |-------------------|------------------|-----------------------------------------------|
61
+ | `scrollThreshold` | `300` | px scrolled before button appears |
62
+ | `position` | `'bottom-right'` | `'bottom-right'` or `'bottom-left'` |
63
+ | `label` | `''` | Optional text next to icon; empty = icon only |
64
+ | `smooth` | `true` | Smooth scroll vs instant |
65
+ | `offset` | `32` | px from bottom/side edge |
66
+
67
+ ### Server (`plugin.js`)
68
+
69
+ Two endpoints under `/api/plugins/back-to-top/`:
70
+
71
+ - `GET /settings` - admin-auth - returns merged settings
72
+ - `PUT /settings` - admin-auth - saves user overrides via `savePluginState`
73
+
74
+ No public endpoints needed.
75
+
76
+ ### Public Injection (`inject-body.html`)
77
+
78
+ Self-contained IIFE:
79
+
80
+ 1. Fetches `/api/plugins/back-to-top/settings` (unauthenticated - settings are public read)
81
+ 2. Creates a `<button id="back-to-top-btn">` with inline styles driven by config
82
+ 3. Uses `IntersectionObserver` on a sentinel element (or `scroll` event) to show/hide
83
+ 4. Clicking scrolls to top using `window.scrollTo` (smooth or instant per config)
84
+ 5. Uses a Domma `arrow-up` icon (`data-icon` pattern won't work outside admin - renders a plain SVG inline)
85
+
86
+ The button is self-styled (no dependency on site.css) so it works regardless of theme.
87
+
88
+ ### Admin Panel (`back-to-top-settings.js`)
89
+
90
+ Simple settings form matching the contact-form settings pattern:
91
+
92
+ - Number input: Scroll threshold (px)
93
+ - Select: Position (Bottom right / Bottom left)
94
+ - Text input: Button label (optional)
95
+ - Checkbox: Smooth scroll
96
+ - Number input: Edge offset (px)
97
+ - Save button with success toast
98
+
99
+ ### plugin.json Admin Section
100
+
101
+ ```json
102
+ "admin": {
103
+ "sidebar": [{ "id": "back-to-top", "text": "Back to Top", "icon": "arrow-up", "url": "#/plugins/back-to-top" }],
104
+ "routes": [{ "path": "/plugins/back-to-top", "view": "plugin-back-to-top-settings", "title": "Back to Top - Domma CMS" }],
105
+ "views": {
106
+ "plugin-back-to-top-settings": {
107
+ "entry": "back-to-top/admin/views/back-to-top-settings.js",
108
+ "exportName": "backToTopSettingsView"
109
+ }
110
+ }
111
+ }
112
+ ```
113
+
114
+ ### Note on Settings Endpoint Auth
115
+
116
+ The `GET /settings` endpoint is called from the public site (unauthenticated). It returns only non-sensitive display
117
+ config (threshold, position, label, smooth, offset) - no credentials. This is intentional and safe.
118
+
119
+ ---
120
+
121
+ ## Decisions
122
+
123
+ - Plugin follows contact-form structure exactly (no new patterns)
124
+ - Public button is self-contained (no Domma dependency on public side)
125
+ - Settings fetched client-side (no server-side variable substitution needed)
126
+ - Tooltip sweep is targeted (icon-only buttons only, not all interactive elements)