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,427 @@
1
+ # Tooltip Sweep & Back-to-Top Plugin
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Add `data-tooltip` to all remaining icon-only admin buttons, and build a complete `back-to-top` CMS plugin.
6
+
7
+ **Architecture:** Tooltip sweep targets two files (`pages.js`, `navigation.js`) - replace `title=""` with
8
+ `data-tooltip=""` and call `E.tooltip()` after icons scan. Plugin follows the exact `contact-form` structure:
9
+ `plugin.json` + `plugin.js` + `config.js` + admin view/template + public `inject-body.html` IIFE.
10
+
11
+ ---
12
+
13
+ ## Task 1: Tooltip sweep - pages.js
14
+
15
+ **Modify:** `admin/js/views/pages.js`
16
+
17
+ Two changes, both inside `renderTable()`:
18
+
19
+ **Step 1: Replace `title` with `data-tooltip` on the view link (line 26)**
20
+
21
+ Old:
22
+
23
+ ```html
24
+ <a href="${row.urlPath}" target="_blank" class="btn btn-sm btn-ghost" title="View"><span data-icon="external-link"></span></a>
25
+ ```
26
+
27
+ New:
28
+
29
+ ```html
30
+ <a href="${row.urlPath}" target="_blank" class="btn btn-sm btn-ghost" data-tooltip="View"><span data-icon="external-link"></span></a>
31
+ ```
32
+
33
+ **Step 2: Add `E.tooltip()` call after `Domma.icons.scan()` (line 33)**
34
+
35
+ Old:
36
+
37
+ ```js
38
+ Domma.icons.scan();
39
+ Domma.effects.reveal('.card', { animation: 'fade', duration: 350 });
40
+ ```
41
+
42
+ New:
43
+
44
+ ```js
45
+ Domma.icons.scan();
46
+ E.tooltip('[data-tooltip]', { placement: 'top' });
47
+ Domma.effects.reveal('.card', { animation: 'fade', duration: 350 });
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Task 2: Tooltip sweep - navigation.js
53
+
54
+ **Modify:** `admin/js/views/navigation.js`
55
+
56
+ Two changes, both inside `renderItems()`:
57
+
58
+ **Step 1: Replace `title` with `data-tooltip` on the Remove button (line 90)**
59
+
60
+ Old:
61
+
62
+ ```html
63
+ <button class="btn btn-sm btn-danger btn-remove-item" data-id="${item._id}" title="Remove"><span data-icon="trash"></span></button>
64
+ ```
65
+
66
+ New:
67
+
68
+ ```html
69
+ <button class="btn btn-sm btn-danger btn-remove-item" data-id="${item._id}" data-tooltip="Remove"><span data-icon="trash"></span></button>
70
+ ```
71
+
72
+ **Step 2: Add `E.tooltip()` call after `Domma.icons.scan('#nav-items-list')` (line 95)**
73
+
74
+ Old:
75
+
76
+ ```js
77
+ Domma.icons.scan('#nav-items-list');
78
+ ```
79
+
80
+ New:
81
+
82
+ ```js
83
+ Domma.icons.scan('#nav-items-list');
84
+ E.tooltip('[data-tooltip]', { placement: 'top' });
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Task 3: Create plugins/back-to-top/plugin.json
90
+
91
+ **New file:** `plugins/back-to-top/plugin.json`
92
+
93
+ ```json
94
+ {
95
+ "name": "back-to-top",
96
+ "displayName": "Back to Top",
97
+ "version": "1.0.0",
98
+ "description": "Injects a configurable scroll-to-top button into every public page.",
99
+ "author": "Darryl Waterhouse",
100
+ "date": "2026-03-02",
101
+ "icon": "arrow-up",
102
+ "admin": {
103
+ "sidebar": [
104
+ { "id": "back-to-top", "text": "Back to Top", "icon": "arrow-up", "url": "#/plugins/back-to-top", "section": "#/plugins/back-to-top" }
105
+ ],
106
+ "routes": [
107
+ { "path": "/plugins/back-to-top", "view": "plugin-back-to-top-settings", "title": "Back to Top - Domma CMS" }
108
+ ],
109
+ "views": {
110
+ "plugin-back-to-top-settings": {
111
+ "entry": "back-to-top/admin/views/back-to-top-settings.js",
112
+ "exportName": "backToTopSettingsView"
113
+ }
114
+ }
115
+ },
116
+ "inject": {
117
+ "bodyEnd": "public/inject-body.html"
118
+ }
119
+ }
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Task 4: Create plugins/back-to-top/config.js
125
+
126
+ **New file:** `plugins/back-to-top/config.js`
127
+
128
+ ```js
129
+ /**
130
+ * Back to Top Plugin - Default Configuration
131
+ */
132
+ export default {
133
+ scrollThreshold: 300,
134
+ position: 'bottom-right',
135
+ label: '',
136
+ smooth: true,
137
+ offset: 32
138
+ };
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Task 5: Create plugins/back-to-top/plugin.js
144
+
145
+ **New file:** `plugins/back-to-top/plugin.js`
146
+
147
+ ```js
148
+ /**
149
+ * Back to Top Plugin - Server
150
+ *
151
+ * Endpoints (prefix: /api/plugins/back-to-top):
152
+ * GET /settings - public (called from public site IIFE)
153
+ * PUT /settings - admin-auth - saves user overrides
154
+ */
155
+ import { getPluginSettings, savePluginState } from '../../server/services/plugins.js';
156
+
157
+ export default async function backToTopPlugin(fastify, options) {
158
+ const { authenticate, requireAdmin } = options.auth;
159
+
160
+ // GET /settings - no auth, public read (returns non-sensitive display config)
161
+ fastify.get('/settings', async () => {
162
+ return getPluginSettings('back-to-top');
163
+ });
164
+
165
+ // PUT /settings - admin only
166
+ fastify.put('/settings', { preHandler: [authenticate, requireAdmin] }, async (request) => {
167
+ const body = request.body || {};
168
+ savePluginState('back-to-top', { settings: body });
169
+ return { ok: true };
170
+ });
171
+ }
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Task 6: Create admin template
177
+
178
+ **New file:** `plugins/back-to-top/admin/templates/back-to-top-settings.html`
179
+
180
+ ```html
181
+ <div class="view-header">
182
+ <h1><span data-icon="arrow-up"></span> Back to Top</h1>
183
+ <div>
184
+ <button id="save-settings-btn" class="btn btn-primary">
185
+ <span data-icon="save"></span> Save
186
+ </button>
187
+ </div>
188
+ </div>
189
+
190
+ <div class="card mb-4">
191
+ <div class="card-header"><h2>Appearance</h2></div>
192
+ <div class="card-body">
193
+ <div class="row mb-3">
194
+ <div class="col-6">
195
+ <label class="form-label">Position</label>
196
+ <select id="field-position" class="form-select">
197
+ <option value="bottom-right">Bottom right</option>
198
+ <option value="bottom-left">Bottom left</option>
199
+ </select>
200
+ </div>
201
+ <div class="col-6">
202
+ <label class="form-label">Edge offset (px)</label>
203
+ <input id="field-offset" type="number" class="form-input" min="0" max="200" placeholder="32">
204
+ </div>
205
+ </div>
206
+ <div class="row">
207
+ <div class="col">
208
+ <label class="form-label">Button label <span class="text-muted">(optional text beside icon)</span></label>
209
+ <input id="field-label" type="text" class="form-input" placeholder="Leave empty for icon only">
210
+ </div>
211
+ </div>
212
+ </div>
213
+ </div>
214
+
215
+ <div class="card mb-4">
216
+ <div class="card-header"><h2>Behaviour</h2></div>
217
+ <div class="card-body">
218
+ <div class="row mb-3">
219
+ <div class="col-6">
220
+ <label class="form-label">Scroll threshold (px)</label>
221
+ <input id="field-threshold" type="number" class="form-input" min="0" max="9999" placeholder="300">
222
+ <span class="form-hint">Button appears after scrolling this far down.</span>
223
+ </div>
224
+ </div>
225
+ <div class="row">
226
+ <div class="col">
227
+ <label class="form-check-label">
228
+ <input id="field-smooth" type="checkbox">
229
+ Smooth scroll
230
+ </label>
231
+ <span class="form-hint">Animates scroll to top instead of jumping instantly.</span>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ </div>
236
+ ```
237
+
238
+ ---
239
+
240
+ ## Task 7: Create admin settings view
241
+
242
+ **New file:** `plugins/back-to-top/admin/views/back-to-top-settings.js`
243
+
244
+ ```js
245
+ /**
246
+ * Back to Top Plugin - Admin Settings View
247
+ */
248
+ export const backToTopSettingsView = {
249
+ templateUrl: '/plugins/back-to-top/admin/templates/back-to-top-settings.html',
250
+
251
+ async onMount($container) {
252
+ let settings = {};
253
+ try {
254
+ const res = await fetch('/api/plugins/back-to-top/settings', {
255
+ headers: { 'Authorization': 'Bearer ' + (S.get('auth_token') || '') }
256
+ });
257
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
258
+ settings = await res.json();
259
+ } catch {
260
+ E.toast('Could not load settings.', { type: 'error' });
261
+ }
262
+
263
+ $container.find('#field-threshold').val(settings.scrollThreshold ?? 300);
264
+ $container.find('#field-position').val(settings.position || 'bottom-right');
265
+ $container.find('#field-label').val(settings.label || '');
266
+ $container.find('#field-smooth').prop('checked', settings.smooth !== false);
267
+ $container.find('#field-offset').val(settings.offset ?? 32);
268
+
269
+ $container.find('#save-settings-btn').off('click').on('click', async () => {
270
+ const data = {
271
+ scrollThreshold: parseInt($container.find('#field-threshold').val(), 10) || 300,
272
+ position: $container.find('#field-position').val(),
273
+ label: $container.find('#field-label').val().trim(),
274
+ smooth: $container.find('#field-smooth').prop('checked'),
275
+ offset: parseInt($container.find('#field-offset').val(), 10) || 32
276
+ };
277
+ try {
278
+ const res = await fetch('/api/plugins/back-to-top/settings', {
279
+ method: 'PUT',
280
+ headers: {
281
+ 'Content-Type': 'application/json',
282
+ 'Authorization': 'Bearer ' + (S.get('auth_token') || '')
283
+ },
284
+ body: JSON.stringify(data)
285
+ });
286
+ if (!res.ok) throw new Error('Save failed');
287
+ E.toast('Settings saved.', { type: 'success' });
288
+ } catch {
289
+ E.toast('Failed to save settings.', { type: 'error' });
290
+ }
291
+ });
292
+
293
+ Domma.icons.scan();
294
+ }
295
+ };
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Task 8: Create public inject-body.html
301
+
302
+ **New file:** `plugins/back-to-top/public/inject-body.html`
303
+
304
+ Self-contained IIFE. No Domma dependency. Inline SVG for the arrow-up icon (path from Domma's built-in icon set).
305
+
306
+ ```html
307
+ <script>
308
+ (function () {
309
+ 'use strict';
310
+
311
+ var BTN_ID = 'back-to-top-btn';
312
+
313
+ fetch('/api/plugins/back-to-top/settings')
314
+ .then(function (r) { return r.json(); })
315
+ .then(function (cfg) {
316
+ var threshold = cfg.scrollThreshold || 300;
317
+ var position = cfg.position || 'bottom-right';
318
+ var label = cfg.label || '';
319
+ var smooth = cfg.smooth !== false;
320
+ var offset = cfg.offset || 32;
321
+
322
+ var btn = document.createElement('button');
323
+ btn.id = BTN_ID;
324
+ btn.type = 'button';
325
+ btn.setAttribute('aria-label', 'Back to top');
326
+ btn.setAttribute('title', 'Back to top');
327
+
328
+ // Inline SVG - arrow-up (Domma icon set path)
329
+ var svgNS = 'http://www.w3.org/2000/svg';
330
+ var svg = document.createElementNS(svgNS, 'svg');
331
+ svg.setAttribute('viewBox', '0 0 24 24');
332
+ svg.setAttribute('width', '20');
333
+ svg.setAttribute('height', '20');
334
+ svg.setAttribute('fill', 'none');
335
+ svg.setAttribute('stroke', 'currentColor');
336
+ svg.setAttribute('stroke-width', '2');
337
+ svg.setAttribute('stroke-linecap', 'round');
338
+ svg.setAttribute('stroke-linejoin', 'round');
339
+ var path = document.createElementNS(svgNS, 'path');
340
+ path.setAttribute('d', 'M12 19V5M5 12l7-7 7 7');
341
+ svg.appendChild(path);
342
+ btn.appendChild(svg);
343
+
344
+ if (label) {
345
+ var span = document.createElement('span');
346
+ span.textContent = label;
347
+ span.style.marginLeft = '6px';
348
+ btn.appendChild(span);
349
+ }
350
+
351
+ // Inline styles - self-contained, no site.css dependency
352
+ var isRight = position !== 'bottom-left';
353
+ Object.assign(btn.style, {
354
+ position: 'fixed',
355
+ bottom: offset + 'px',
356
+ right: isRight ? offset + 'px' : 'auto',
357
+ left: isRight ? 'auto' : offset + 'px',
358
+ zIndex: '9999',
359
+ display: 'flex',
360
+ alignItems: 'center',
361
+ padding: '10px',
362
+ borderRadius: '50%',
363
+ border: 'none',
364
+ cursor: 'pointer',
365
+ background: 'rgba(0,0,0,0.5)',
366
+ color: '#fff',
367
+ backdropFilter: 'blur(4px)',
368
+ opacity: '0',
369
+ transform: 'translateY(12px)',
370
+ transition: 'opacity .25s, transform .25s',
371
+ pointerEvents: 'none'
372
+ });
373
+
374
+ if (label) {
375
+ btn.style.borderRadius = '999px';
376
+ btn.style.padding = '10px 16px';
377
+ }
378
+
379
+ document.body.appendChild(btn);
380
+
381
+ function show() {
382
+ btn.style.opacity = '1';
383
+ btn.style.transform = 'translateY(0)';
384
+ btn.style.pointerEvents = 'auto';
385
+ }
386
+ function hide() {
387
+ btn.style.opacity = '0';
388
+ btn.style.transform = 'translateY(12px)';
389
+ btn.style.pointerEvents = 'none';
390
+ }
391
+
392
+ window.addEventListener('scroll', function () {
393
+ if (window.scrollY > threshold) { show(); } else { hide(); }
394
+ }, { passive: true });
395
+
396
+ btn.addEventListener('click', function () {
397
+ window.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' });
398
+ });
399
+ })
400
+ .catch(function () {
401
+ // Settings fetch failed - don't inject button
402
+ });
403
+ })();
404
+ </script>
405
+ ```
406
+
407
+ ---
408
+
409
+ ## Verification
410
+
411
+ After all tasks:
412
+
413
+ 1. **Pages view tooltips** - navigate to Pages. Hover the external-link icon on a row. Tooltip "View" should appear.
414
+
415
+ 2. **Navigation view tooltips** - navigate to Navigation. Hover the trash button on an item. Tooltip "Remove" should
416
+ appear.
417
+
418
+ 3. **Plugin appears in sidebar** - restart the server. Navigate to Plugins. The "Back to Top" plugin card should appear.
419
+ The sidebar should show "Back to Top" under the Plugins section.
420
+
421
+ 4. **Settings page loads** - click the Back to Top sidebar link. Settings form should show with defaults (threshold 300,
422
+ position bottom-right, smooth checked, offset 32).
423
+
424
+ 5. **Save settings** - change threshold to 200, click Save. Toast "Settings saved." should appear.
425
+
426
+ 6. **Public injection** - visit the public site (e.g. `/`). Scroll down more than 300px. The back-to-top button should
427
+ appear in the bottom-right corner. Clicking it should scroll back to the top.
@@ -0,0 +1,115 @@
1
+ # Cookie Consent + Back to Top - Migrate to Core
2
+
3
+ **Date:** 2026-03-05
4
+
5
+ ## Problem
6
+
7
+ Cookie Consent and Back to Top are currently implemented as plugins. They are universal features needed on every site - not optional add-ons. Living as plugins creates friction: they must be individually enabled, their settings are separate from Site Settings, and they clutter the plugin list with non-optional items. The plugin directories will be deleted entirely.
8
+
9
+ ## Approach
10
+
11
+ **Piggyback on `window.__CMS_SITE__`** (Approach B).
12
+
13
+ The renderer already injects `window.__CMS_SITE__` (the full site config) into every public page. The new scripts will read their configuration directly from that global, eliminating any HTTP fetch on page load. The renderer conditionally injects each `<script>` tag based on the feature's `enabled` flag.
14
+
15
+ ## Data Model
16
+
17
+ Two new top-level keys added to `config/site.json`:
18
+
19
+ ```json
20
+ "backToTop": {
21
+ "enabled": false,
22
+ "scrollThreshold": 300,
23
+ "position": "bottom-right",
24
+ "label": "Back to top",
25
+ "smooth": true,
26
+ "offset": 40
27
+ },
28
+ "cookieConsent": {
29
+ "enabled": false,
30
+ "message": "We use cookies to improve your experience.",
31
+ "acceptButton": "Accept All",
32
+ "declineButton": "Decline",
33
+ "policyLink": "",
34
+ "policyText": "Privacy Policy",
35
+ "position": "bottom",
36
+ "layout": "bar",
37
+ "theme": "dark",
38
+ "consentVersion": "1",
39
+ "categories": {
40
+ "functional": true,
41
+ "analytics": false,
42
+ "marketing": false
43
+ }
44
+ }
45
+ ```
46
+
47
+ `enabled: false` means the script tag is not injected - no runtime overhead for disabled features.
48
+
49
+ ## Renderer Changes
50
+
51
+ `server/services/renderer.js` - after assembling `bodyEndInject`, conditionally append script tags:
52
+
53
+ ```js
54
+ if (site.backToTop?.enabled) {
55
+ bodyEndInject += '\n<script src="/public/js/btt.js"></script>';
56
+ }
57
+ if (site.cookieConsent?.enabled) {
58
+ bodyEndInject += '\n<script src="/public/js/cookie-consent.js"></script>';
59
+ }
60
+ ```
61
+
62
+ The scripts are served as static files from `public/js/` via Fastify's existing static serving.
63
+
64
+ ## Public Scripts
65
+
66
+ ### `public/js/btt.js`
67
+
68
+ Ported from `plugins/back-to-top/public/inject-body.html`. Key change: reads config from `window.__CMS_SITE__.backToTop` instead of fetching `/api/plugins/back-to-top/settings`. Same scroll-listener + button-inject logic.
69
+
70
+ ### `public/js/cookie-consent.js`
71
+
72
+ Ported from `plugins/cookie-consent/public/inject-body.html`. Key change: reads config from `window.__CMS_SITE__.cookieConsent` instead of fetching `/api/plugins/cookie-consent/settings`. Same consent-bar + localStorage logic.
73
+
74
+ ## Admin - Site Settings Tabs
75
+
76
+ Two new tabs added to `admin/js/templates/settings.html` alongside General / SEO / Footer / Email:
77
+
78
+ - **Back to Top** - fields: Enabled toggle, Scroll threshold, Position (bottom-right/bottom-left), Label, Smooth scroll, Bottom offset
79
+ - **Cookie Consent** - fields: Enabled toggle, Message, Accept/Decline button text, Policy link + text, Position (bottom/top), Layout (bar/modal), Theme (dark/light), Consent version, Category toggles (functional/analytics/marketing)
80
+
81
+ `admin/js/views/settings.js` - extends existing save/load logic to include `backToTop` and `cookieConsent` keys when reading/writing `config/site.json` via `PUT /api/settings/site`.
82
+
83
+ ## Deletions
84
+
85
+ - Delete `plugins/back-to-top/` directory entirely
86
+ - Delete `plugins/cookie-consent/` directory entirely
87
+ - Remove their entries from `config/plugins.json`
88
+
89
+ No migration of existing plugin settings is needed - defaults in `config/site.json` will be used, and admins can re-enable and configure via Site Settings.
90
+
91
+ ## Files Summary
92
+
93
+ ### New Files
94
+ - `public/js/btt.js` - Back to Top script (reads from `window.__CMS_SITE__`)
95
+ - `public/js/cookie-consent.js` - Cookie Consent script (reads from `window.__CMS_SITE__`)
96
+
97
+ ### Modified Files
98
+ - `config/site.json` - Add `backToTop` and `cookieConsent` keys
99
+ - `server/services/renderer.js` - Conditionally inject script tags
100
+ - `admin/js/templates/settings.html` - Add Back to Top and Cookie Consent tab panels
101
+ - `admin/js/views/settings.js` - Handle new settings sections on load/save
102
+
103
+ ### Deleted
104
+ - `plugins/back-to-top/` (entire directory)
105
+ - `plugins/cookie-consent/` (entire directory)
106
+ - Entries for both in `config/plugins.json`
107
+
108
+ ## Verification
109
+
110
+ 1. Enable Back to Top in Site Settings → save → visit public page → scroll down → button appears
111
+ 2. Disable Back to Top → save → visit public page → no button, no script tag in source
112
+ 3. Enable Cookie Consent → save → visit public page in private window → consent bar appears
113
+ 4. Disable Cookie Consent → save → visit public page → no consent bar, no script tag
114
+ 5. Plugin list in admin → back-to-top and cookie-consent no longer appear
115
+ 6. Site Settings → two new tabs visible (Back to Top, Cookie Consent) with correct fields
@@ -0,0 +1,35 @@
1
+ # Custom CSS - Migrate to Core
2
+
3
+ **Date:** 2026-03-05
4
+ **Status:** Approved
5
+
6
+ ## Goal
7
+
8
+ Move the Custom CSS plugin into core, eliminating the plugin directory. The feature becomes a tab in Site Settings backed by a content file and core API routes.
9
+
10
+ ## Storage
11
+
12
+ CSS is stored at `content/custom.css` - a plain text file, consistent with other content files. Empty by default.
13
+
14
+ ## Renderer
15
+
16
+ `server/services/renderer.js` reads `content/custom.css` at render time. If the file is non-empty, an inline `<style>` block is injected into the `headLate` slot. The string `</style>` inside CSS values is escaped to `<\/style>` to prevent injection.
17
+
18
+ ## Admin API
19
+
20
+ Two new routes added to `server/routes/api/settings.js`:
21
+
22
+ - `GET /api/settings/custom-css` - authenticated, returns `{ css: "..." }`
23
+ - `PUT /api/settings/custom-css` - requireAdmin, validates string type + 100 KB limit, writes `content/custom.css`
24
+
25
+ ## Admin UI
26
+
27
+ A new **Custom CSS** tab added to `admin/js/templates/settings.html` containing a full-height monospace `<textarea id="field-custom-css">`.
28
+
29
+ Since CSS is a file rather than part of the JSON config blob, it gets its own **Save CSS** button inside the tab. `admin/js/views/settings.js` wires up the tab: loads CSS on mount, saves via the new API endpoint on button click.
30
+
31
+ ## Cleanup
32
+
33
+ - Delete `plugins/custom-css/` directory entirely
34
+ - Remove `custom-css` entry from `config/plugins.json`
35
+ - No sidebar entry needed - accessible via Settings → Custom CSS tab