domma-cms 0.75.0 → 0.77.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 (150) hide show
  1. package/CLAUDE.md +1 -1
  2. package/admin/js/api.js +1 -1
  3. package/admin/js/app.js +4 -4
  4. package/admin/js/lib/action-editor-arrange.js +1 -0
  5. package/admin/js/lib/api-endpoint-editor-arrange.js +2 -0
  6. package/admin/js/lib/api-tokens-arrange.js +3 -0
  7. package/admin/js/lib/block-editor-arrange.js +2 -0
  8. package/admin/js/lib/collection-editor-arrange.js +3 -0
  9. package/admin/js/lib/collection-entries-arrange.js +1 -0
  10. package/admin/js/lib/component-editor-arrange.js +27 -0
  11. package/admin/js/lib/context-menu-editor-arrange.js +1 -0
  12. package/admin/js/lib/dashboard-arrange.js +1 -0
  13. package/admin/js/lib/effects-arrange.js +3 -0
  14. package/admin/js/lib/form-editor-arrange.js +1 -0
  15. package/admin/js/lib/form-submissions-arrange.js +2 -0
  16. package/admin/js/lib/layouts-arrange.js +1 -0
  17. package/admin/js/lib/menu-editor-arrange.js +2 -0
  18. package/admin/js/lib/my-profile-arrange.js +1 -0
  19. package/admin/js/lib/notifications-arrange.js +1 -0
  20. package/admin/js/lib/page-editor-arrange.js +1 -0
  21. package/admin/js/lib/plugin-code-arrange.js +1 -0
  22. package/admin/js/lib/plugins-arrange.js +2 -0
  23. package/admin/js/lib/project-page-arrange.js +1 -0
  24. package/admin/js/lib/role-editor-arrange.js +3 -0
  25. package/admin/js/lib/roles-arrange.js +1 -0
  26. package/admin/js/lib/search-settings-arrange.js +1 -0
  27. package/admin/js/lib/sidebar-renderer.js +2 -2
  28. package/admin/js/lib/site-settings-arrange.js +1 -0
  29. package/admin/js/lib/user-editor-arrange.js +1 -0
  30. package/admin/js/lib/users-arrange.js +1 -0
  31. package/admin/js/lib/view-editor-arrange.js +14 -0
  32. package/admin/js/templates/action-editor.html +223 -147
  33. package/admin/js/templates/api-endpoint-editor.html +172 -117
  34. package/admin/js/templates/api-tokens.html +83 -10
  35. package/admin/js/templates/block-editor.html +194 -203
  36. package/admin/js/templates/collection-editor.html +191 -113
  37. package/admin/js/templates/collection-entries.html +99 -36
  38. package/admin/js/templates/component-editor.html +128 -126
  39. package/admin/js/templates/context-menu-editor.html +208 -148
  40. package/admin/js/templates/dashboard.html +184 -22
  41. package/admin/js/templates/effects.html +155 -72
  42. package/admin/js/templates/form-editor.html +41 -26
  43. package/admin/js/templates/form-submissions.html +78 -26
  44. package/admin/js/templates/layouts.html +126 -43
  45. package/admin/js/templates/menu-editor.html +57 -17
  46. package/admin/js/templates/menu-locations.html +54 -11
  47. package/admin/js/templates/my-profile.html +100 -12
  48. package/admin/js/templates/notifications.html +70 -10
  49. package/admin/js/templates/page-editor.html +47 -26
  50. package/admin/js/templates/plugin-code.html +91 -73
  51. package/admin/js/templates/plugins.html +100 -28
  52. package/admin/js/templates/project-detail.html +83 -145
  53. package/admin/js/templates/project-settings.html +88 -53
  54. package/admin/js/templates/role-editor.html +158 -83
  55. package/admin/js/templates/roles.html +78 -7
  56. package/admin/js/templates/search.html +193 -216
  57. package/admin/js/templates/settings.html +404 -472
  58. package/admin/js/templates/user-editor.html +150 -14
  59. package/admin/js/templates/users.html +69 -9
  60. package/admin/js/templates/view-editor.html +203 -180
  61. package/admin/js/templates/view-preview.html +45 -45
  62. package/admin/js/views/action-editor.js +7 -1
  63. package/admin/js/views/api-endpoint-editor.js +2 -3
  64. package/admin/js/views/api-tokens.js +62 -8
  65. package/admin/js/views/block-editor.js +14 -8
  66. package/admin/js/views/collection-editor.js +64 -5
  67. package/admin/js/views/collection-entries.js +50 -1
  68. package/admin/js/views/component-editor.js +2 -28
  69. package/admin/js/views/context-menu-editor.js +54 -55
  70. package/admin/js/views/dashboard.js +7 -1
  71. package/admin/js/views/effects.js +31 -1
  72. package/admin/js/views/form-editor.js +7 -7
  73. package/admin/js/views/form-submissions.js +11 -1
  74. package/admin/js/views/index.js +1 -1
  75. package/admin/js/views/layouts.js +81 -1
  76. package/admin/js/views/login.js +2 -2
  77. package/admin/js/views/menu-editor.js +21 -43
  78. package/admin/js/views/menu-locations.js +16 -1
  79. package/admin/js/views/my-profile.js +1 -1
  80. package/admin/js/views/notifications.js +18 -1
  81. package/admin/js/views/page-editor.js +26 -26
  82. package/admin/js/views/plugin-code.js +23 -4
  83. package/admin/js/views/plugins.js +97 -39
  84. package/admin/js/views/project-detail.js +9 -1
  85. package/admin/js/views/project-settings.js +1 -1
  86. package/admin/js/views/role-editor.js +11 -3
  87. package/admin/js/views/roles.js +49 -4
  88. package/admin/js/views/search.js +7 -1
  89. package/admin/js/views/settings.js +13 -3
  90. package/admin/js/views/user-editor.js +1 -1
  91. package/admin/js/views/users.js +13 -4
  92. package/admin/js/views/view-editor.js +1 -1
  93. package/admin/js/views/view-preview.js +1 -1
  94. package/package.json +1 -1
  95. package/plugins/_lib/admin/mail/compose-window.js +97 -40
  96. package/plugins/_lib/admin/mail/contacts.js +4 -1
  97. package/plugins/_lib/admin/mail/diagnostics-section.js +4 -1
  98. package/plugins/_lib/admin/mail/folder-tree.js +1 -1
  99. package/plugins/_lib/admin/mail/image-senders-section.js +69 -90
  100. package/plugins/_lib/admin/mail/mail.css +1 -1
  101. package/plugins/_lib/admin/mail/mail.html +1 -1
  102. package/plugins/_lib/admin/mail/reader-view.js +303 -44
  103. package/plugins/_lib/admin/mail/when-closed.js +26 -0
  104. package/plugins/_lib/mail/readRoutes.js +43 -1
  105. package/plugins/_lib/mail/unreadBadge.js +42 -0
  106. package/plugins/free-tier.lock.json +5 -5
  107. package/plugins/mail-reader/CLAUDE.md +1 -0
  108. package/plugins/mail-reader/plugin.js +3 -1
  109. package/plugins/mail-reader/plugin.json +5 -4
  110. package/server/routes/api/api-tokens.js +56 -0
  111. package/server/routes/api/auth.js +7 -1
  112. package/server/routes/api/collections.js +13 -1
  113. package/server/routes/api/dashboard.js +32 -4
  114. package/server/routes/api/effects.js +68 -13
  115. package/server/routes/api/forms.js +104 -13
  116. package/server/routes/api/layouts.js +93 -2
  117. package/server/routes/api/notifications.js +140 -14
  118. package/server/routes/api/plugins.js +135 -4
  119. package/server/routes/api/roles.js +111 -0
  120. package/server/routes/api/search.js +38 -4
  121. package/server/routes/api/settings.js +52 -0
  122. package/server/routes/api/users.js +65 -3
  123. package/server/routes/api/views.js +55 -1
  124. package/server/server.js +12 -0
  125. package/server/services/components.js +9 -0
  126. package/server/services/plugins.js +7 -0
  127. package/server/services/roles.js +3 -0
  128. package/server/services/search.js +58 -0
  129. package/server/services/users.js +16 -2
  130. package/server/services/views.js +14 -0
  131. package/admin/js/templates/dashboard/activity-feed.html +0 -3
  132. package/admin/js/templates/dashboard/cache.html +0 -32
  133. package/admin/js/templates/dashboard/health-detail.html +0 -2
  134. package/admin/js/templates/dashboard/journeys.html +0 -17
  135. package/admin/js/templates/dashboard/kpi-strip.html +0 -34
  136. package/admin/js/templates/dashboard/spike-feed.html +0 -3
  137. package/admin/js/templates/dashboard/top-pages.html +0 -3
  138. package/admin/js/templates/dashboard/traffic-chart.html +0 -3
  139. package/admin/js/templates/plugin-marketplace.html +0 -29
  140. package/admin/js/views/block-editor-enhance.js +0 -1
  141. package/admin/js/views/dashboard/lib/escape.js +0 -1
  142. package/admin/js/views/dashboard/widgets/activity-feed.js +0 -1
  143. package/admin/js/views/dashboard/widgets/cache.js +0 -1
  144. package/admin/js/views/dashboard/widgets/health-detail.js +0 -1
  145. package/admin/js/views/dashboard/widgets/journeys.js +0 -1
  146. package/admin/js/views/dashboard/widgets/kpi-strip.js +0 -1
  147. package/admin/js/views/dashboard/widgets/spike-feed.js +0 -6
  148. package/admin/js/views/dashboard/widgets/top-pages.js +0 -1
  149. package/admin/js/views/dashboard/widgets/traffic-chart.js +0 -1
  150. package/admin/js/views/plugin-marketplace.js +0 -1
@@ -1,498 +1,430 @@
1
- <div class="view-header">
2
- <h1><span data-icon="settings"></span> Site Settings</h1>
3
- <button id="save-settings-btn" class="btn btn-primary"><span data-icon="save"></span> Save</button>
4
- </div>
5
-
6
- <div class="tabs" id="settings-tabs">
7
- <div class="tab-list">
8
- <button class="tab-item active">General</button>
9
- <button class="tab-item">SEO</button>
10
- <button class="tab-item">Footer</button>
11
- <button class="tab-item">Email / SMTP</button>
12
- <button class="tab-item">Back to Top</button>
13
- <button class="tab-item">Cookie Consent</button>
14
- <button class="tab-item">Breadcrumbs</button>
15
- <button class="tab-item">Custom CSS</button>
16
- <button class="tab-item">Cache</button>
17
- </div>
18
- <div class="tab-content">
19
-
20
- <!-- General -->
21
- <div class="tab-panel active">
22
- <div class="row mb-3">
23
- <div class="col-6">
24
- <label class="form-label">Site Title</label>
25
- <input id="field-site-title" type="text" class="form-input" placeholder="My Site">
26
- </div>
27
- <div class="col-6">
28
- <label class="form-label">Tagline</label>
29
- <input id="field-tagline" type="text" class="form-input" placeholder="Built with Domma CMS">
30
- </div>
31
- </div>
32
- <div class="row mb-3">
33
- <div class="col">
34
- <div class="card" style="background:var(--dm-surface-raised);">
35
- <div class="card-body" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap;">
36
- <span data-icon="droplet"></span>
37
- <div style="flex:1 1 240px;">
38
- <strong>Themes, fonts and element overrides moved.</strong>
39
- <p class="text-muted" style="margin:.25rem 0 0;font-size:.85rem;">
40
- The site theme, admin theme, automatic day/night pair and typography now live in
41
- Content &rsaquo; Theme, alongside per-element theme overrides.
42
- </p>
43
- </div>
44
- <a href="#/theme" class="btn btn-ghost btn-sm">Open Theme</a>
45
- </div>
46
- </div>
47
- </div>
48
- </div>
49
- <div class="row mb-3">
50
- <div class="col-6">
51
- <label class="form-label" for="field-admin-home">Admin home</label>
52
- <select id="field-admin-home" class="form-select"></select>
53
- <small class="form-hint">The screen the admin opens on, and where Dashboard goes. Anyone without the
54
- permission for it gets the built-in dashboard; a confined role keeps its own home (Roles &rsaquo; Admin area).</small>
55
- </div>
56
- </div>
57
- <div class="row mt-3">
58
- <div class="col-3">
59
- <label class="form-label">Default Spacer Size (px)</label>
60
- <input id="field-spacer-size" type="number" class="form-input" min="0" max="500" placeholder="40">
61
- <small class="form-hint">Used as the default height when inserting a <code>[spacer]</code> shortcode from the
62
- editor toolbar.</small>
63
- </div>
64
- </div>
65
- </div>
1
+ <!-- System › Site Settings. Bound with M.applyBindings - see admin/js/views/settings.js, which also inserts
2
+ the cache-entry row template (the router renders a templateUrl through _.render, eating any {{…}}).
3
+ No page heading: the Tool banner above is the heading; Save is a banner action (and Ctrl+S). -->
4
+ <div class="stx">
5
+ <div class="stx-alert is-bad" data-bind-hidden="!errorText.value">
6
+ <span data-icon="x-circle" data-icon-size="16"></span>
7
+ <span><strong>Not saved.</strong> <span data-bind-text="errorText.value"></span></span></div>
8
+ <p class="stx-alert is-dirty" data-bind-hidden="!dirty.value">
9
+ <span data-icon="edit-3" data-icon-size="16"></span>
10
+ <span>Unsaved changes - Save (Ctrl+S) to apply them. Leave and they are kept as a draft for next time
11
+ (never the SMTP password).</span>
12
+ <span class="stx-spacer"></span>
13
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="discard">Discard</button>
14
+ <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="saving.value">Save</button></p>
66
15
 
67
- <!-- SEO -->
68
- <div class="tab-panel">
69
- <div class="row mb-3">
70
- <div class="col-8">
71
- <label class="form-label">Default Site Title</label>
72
- <input id="field-seo-title" type="text" class="form-input" placeholder="My Site">
73
- </div>
74
- <div class="col-4">
75
- <label class="form-label">Title Separator</label>
76
- <input id="field-seo-separator" type="text" class="form-input" placeholder=" | ">
77
- </div>
78
- </div>
79
- <div class="row">
80
- <div class="col">
81
- <label class="form-label">Default Meta Description</label>
82
- <input id="field-seo-desc" type="text" class="form-input" placeholder="Brief description of your site">
16
+ <div class="tabs stx-tabs">
17
+ <div class="tab-list">
18
+ <button type="button" class="tab-item active">General <i class="stx-dot" data-bind-hidden="!tabs.value.general" title="Changed"></i></button>
19
+ <button type="button" class="tab-item">SEO <i class="stx-dot" data-bind-hidden="!tabs.value.seo" title="Changed"></i></button>
20
+ <button type="button" class="tab-item">Footer <i class="stx-dot" data-bind-hidden="!tabs.value.footer" title="Changed"></i></button>
21
+ <button type="button" class="tab-item">Email <i class="stx-dot" data-bind-hidden="!tabs.value.email" title="Changed"></i><i class="stx-dot is-bad" data-bind-hidden="!noSmtp.value || tabs.value.email" title="No SMTP host"></i></button>
22
+ <button type="button" class="tab-item">Back to top <i class="stx-dot" data-bind-hidden="!tabs.value.backToTop" title="Changed"></i></button>
23
+ <button type="button" class="tab-item">Cookie consent <i class="stx-dot" data-bind-hidden="!tabs.value.cookies" title="Changed"></i></button>
24
+ <button type="button" class="tab-item">Breadcrumbs <i class="stx-dot" data-bind-hidden="!tabs.value.breadcrumbs" title="Changed"></i></button>
25
+ <button type="button" class="tab-item">Custom CSS <i class="stx-dot" data-bind-hidden="!tabs.value.css" title="Changed"></i></button>
26
+ <button type="button" class="tab-item">Cache</button>
83
27
  </div>
84
- </div>
85
- </div>
86
28
 
87
- <!-- Footer -->
88
- <div class="tab-panel">
89
- <div class="row mb-4">
90
- <div class="col">
91
- <label class="form-label">Copyright Text</label>
92
- <input id="field-footer-copy" type="text" class="form-input" placeholder="© 2026 My Site">
93
- </div>
94
- </div>
95
- <div class="row mb-4">
96
- <div class="col">
97
- <label class="form-check-label" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
98
- <input id="field-footer-motion-toggle" type="checkbox" class="form-check"> Show &ldquo;Reduce motion&rdquo; switch
99
- </label>
100
- <p class="form-hint text-muted" style="margin-top:.35rem;font-size:.8rem;">Adds an accessibility switch to the
101
- footer letting visitors turn animations off. Turning this off only hides the control - visitors who already
102
- set the preference, and anyone whose browser asks for reduced motion, still get a calmer site.</p>
103
- </div>
104
- </div>
105
- <h3 class="mb-3" style="font-size:1rem;font-weight:600;">Social Media Icons</h3>
106
- <p class="text-muted mb-3" style="font-size:.875rem;">Enter the full URL for each platform to show its icon in the
107
- footer. Leave blank to hide.</p>
108
- <div class="row mb-3">
109
- <div class="col-6">
110
- <label class="form-label">X / Twitter</label>
111
- <input id="field-social-twitter" type="url" class="form-input" placeholder="https://x.com/yourhandle">
112
- </div>
113
- <div class="col-6">
114
- <label class="form-label">Facebook</label>
115
- <input id="field-social-facebook" type="url" class="form-input" placeholder="https://facebook.com/yourpage">
116
- </div>
117
- </div>
118
- <div class="row mb-3">
119
- <div class="col-6">
120
- <label class="form-label">Instagram</label>
121
- <input id="field-social-instagram" type="url" class="form-input"
122
- placeholder="https://instagram.com/yourhandle">
123
- </div>
124
- <div class="col-6">
125
- <label class="form-label">LinkedIn</label>
126
- <input id="field-social-linkedin" type="url" class="form-input"
127
- placeholder="https://linkedin.com/company/yourcompany">
128
- </div>
129
- </div>
130
- <div class="row">
131
- <div class="col-6">
132
- <label class="form-label">GitHub</label>
133
- <input id="field-social-github" type="url" class="form-input" placeholder="https://github.com/yourorg">
134
- </div>
135
- <div class="col-6">
136
- <label class="form-label">YouTube</label>
137
- <input id="field-social-youtube" type="url" class="form-input" placeholder="https://youtube.com/@yourchannel">
29
+ <!-- General -->
30
+ <div class="tab-panel active">
31
+ <div class="stx-card" data-bind-hidden="!problemsText.value">
32
+ <div class="stx-card-title">Needs attention
33
+ <span data-help-title="Needs attention" data-help="Settings that are saved but quietly wrong - the same list the badge on Site Settings in the sidebar counts. They go away once fixed and saved."></span></div>
34
+ <div class="stx-problems" data-each="problems.value key=text"></div>
35
+ </div>
36
+ <div class="stx-card">
37
+ <div class="stx-card-title">Site</div>
38
+ <div class="stx-grid">
39
+ <div>
40
+ <label class="form-label" for="stx-title">Site title
41
+ <span data-help="The site's name - in the browser tab after each page's own title, in search results and in the emails the site sends."></span></label>
42
+ <input id="stx-title" type="text" class="form-input" placeholder="My Site" data-model="title.value">
43
+ </div>
44
+ <div>
45
+ <label class="form-label" for="stx-tagline">Tagline</label>
46
+ <input id="stx-tagline" type="text" class="form-input" placeholder="Built with Domma CMS" data-model="tagline.value">
47
+ </div>
48
+ </div>
49
+ </div>
50
+ <div class="stx-card">
51
+ <div class="stx-card-title">Admin</div>
52
+ <div class="stx-grid">
53
+ <div>
54
+ <label class="form-label" for="stx-admin-home">Admin home
55
+ <span data-help-title="Admin home" data-help="The screen the admin opens on, and where Dashboard goes. Anyone without the permission for it gets the built-in dashboard; a role confined to part of the admin keeps its own home (Roles › Admin area)."></span></label>
56
+ <select id="stx-admin-home" class="form-select" data-model="adminHome.value"></select>
57
+ </div>
58
+ <div>
59
+ <label class="form-label" for="stx-spacer">Default spacer size (px)
60
+ <span data-help="The height a [spacer] shortcode gets when it is inserted from the page editor's toolbar."></span></label>
61
+ <input id="stx-spacer" type="number" class="form-input" min="0" max="500" placeholder="40" data-model="spacerSize.value">
62
+ </div>
63
+ </div>
64
+ </div>
65
+ <div class="stx-card stx-moved">
66
+ <span data-icon="droplet"></span>
67
+ <div class="stx-moved-text">
68
+ <strong>Themes, fonts and element overrides</strong>
69
+ <small>The site and admin themes, day/night switching and typography live in Content › Theme.</small>
70
+ </div>
71
+ <a href="#/theme" class="btn btn-sm btn-ghost">Open Theme</a>
72
+ </div>
138
73
  </div>
139
- </div>
140
- </div>
141
74
 
142
- <!-- Email / SMTP -->
143
- <div class="tab-panel">
144
- <!-- Shown by settings.js when smtp.host is empty. Without a host the CMS
145
- falls back to an Ethereal test inbox, which ACCEPTS the mail and
146
- reports success - so nothing anywhere else looks wrong. -->
147
- <div id="smtp-unconfigured-notice" class="card mb-3" style="display:none;">
148
- <div class="card-body" style="color:var(--dm-warning,#f59e0b);display:flex;align-items:flex-start;gap:.6rem;">
149
- <span data-icon="warning"></span>
150
- <span><strong>No SMTP host configured.</strong> Mail is going to a throwaway test inbox and will
151
- <strong>not</strong> reach anyone - password resets and form notifications will look like they
152
- sent. Set a host below and save.</span>
153
- </div>
154
- </div>
155
- <div class="row mb-3">
156
- <div class="col-8">
157
- <label class="form-label">SMTP Host</label>
158
- <input id="field-smtp-host" type="text" class="form-input" placeholder="smtp.example.com">
159
- </div>
160
- <div class="col-4">
161
- <label class="form-label">Port</label>
162
- <input id="field-smtp-port" type="number" class="form-input" placeholder="587" value="587">
163
- </div>
164
- </div>
165
- <div class="row mb-3">
166
- <div class="col-6">
167
- <label class="form-label">Username</label>
168
- <input id="field-smtp-user" type="text" class="form-input" placeholder="user@example.com" autocomplete="off">
169
- </div>
170
- <div class="col-6">
171
- <label class="form-label">Password</label>
172
- <input id="field-smtp-pass" type="password" class="form-input" placeholder="••••••••"
173
- autocomplete="new-password">
174
- </div>
175
- </div>
176
- <div class="row mb-3">
177
- <div class="col">
178
- <label class="form-check-label" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
179
- <input id="field-smtp-secure" type="checkbox" class="form-check"> Use TLS (port 465)
180
- </label>
181
- <p class="form-hint text-muted" style="margin-top:.35rem;font-size:.8rem;">Enable for SMTPS. Leave off for
182
- STARTTLS (port 587).</p>
183
- </div>
184
- </div>
185
- <div class="row mb-4">
186
- <div class="col-6">
187
- <label class="form-label">From Address</label>
188
- <input id="field-smtp-from-address" type="email" class="form-input" placeholder="noreply@example.com">
189
- </div>
190
- <div class="col-6">
191
- <label class="form-label">From Name</label>
192
- <input id="field-smtp-from-name" type="text" class="form-input" placeholder="My Website">
193
- </div>
194
- </div>
195
- <div style="border-top:1px solid var(--dm-border,#333);padding-top:1rem;">
196
- <p class="form-label" style="margin-bottom:.5rem;">Send Test Email</p>
197
- <p class="text-muted" style="font-size:.8rem;margin-bottom:.75rem;">Save your SMTP settings first, then send a test to verify the configuration.</p>
198
- <div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;">
199
- <input id="field-test-email-to" type="email" class="form-input" placeholder="recipient@example.com" style="max-width:280px;">
200
- <button id="send-test-email-btn" class="btn btn-ghost btn-sm">
201
- <span data-icon="mail"></span> Send Test
202
- </button>
203
- <span id="test-email-result" style="font-size:.85rem;"></span>
75
+ <!-- SEO -->
76
+ <div class="tab-panel">
77
+ <div class="stx-card">
78
+ <div class="stx-card-title">Search engines
79
+ <span data-help-title="SEO defaults" data-help="Used on every page that does not set its own. A page's title becomes “Page title” + separator + default site title."></span></div>
80
+ <div class="stx-grid">
81
+ <div>
82
+ <label class="form-label" for="stx-seo-title">Default site title</label>
83
+ <input id="stx-seo-title" type="text" class="form-input" placeholder="My Site" data-model="seoTitle.value">
84
+ </div>
85
+ <div>
86
+ <label class="form-label" for="stx-seo-sep">Title separator</label>
87
+ <input id="stx-seo-sep" type="text" class="form-input" placeholder=" | " data-model="seoSeparator.value">
88
+ </div>
89
+ <div class="stx-wide">
90
+ <label class="form-label" for="stx-seo-desc">Default meta description
91
+ <span data-help="The summary search engines show under a page that has no description of its own. About 150 characters is what they display."></span></label>
92
+ <input id="stx-seo-desc" type="text" class="form-input" placeholder="Brief description of your site" data-model="seoDescription.value">
93
+ <small class="stx-hint" data-bind-text="seoLength.value"></small>
94
+ </div>
95
+ </div>
96
+ </div>
204
97
  </div>
205
- </div>
206
- </div>
207
98
 
208
-
209
- <!-- Back to Top -->
210
- <div class="tab-panel">
211
- <div class="mb-3">
212
- <label class="form-label">Label <span class="text-muted" style="font-weight:400;">(optional)</span></label>
213
- <input id="field-btt-label" type="text" class="form-input" placeholder="Back to top">
214
- <p class="form-hint text-muted" style="margin-top:.35rem;font-size:.8rem;">Leave blank for icon-only button.</p>
215
- </div>
216
- <div class="grid grid-cols-2 gap-3 mb-4">
217
- <div>
218
- <label class="form-check-label" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
219
- <input id="field-btt-enabled" type="checkbox" class="form-check"> Enable Back to Top button
220
- </label>
221
- </div>
222
- <div>
223
- <label class="form-check-label" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
224
- <input id="field-btt-smooth" type="checkbox" class="form-check"> Smooth scroll
225
- </label>
226
- </div>
227
- </div>
228
- <div class="grid grid-cols-2 gap-3">
229
- <div>
230
- <label class="form-label">Scroll Threshold (px)</label>
231
- <input id="field-btt-threshold" type="number" class="form-input" placeholder="300">
232
- <p class="form-hint text-muted" style="margin-top:.35rem;font-size:.8rem;">Show button after scrolling this many pixels.</p>
233
- </div>
234
- <div>
235
- <label class="form-label">Position</label>
236
- <select id="field-btt-position" class="form-select">
237
- <option value="bottom-right">Bottom Right</option>
238
- <option value="bottom-left">Bottom Left</option>
239
- </select>
240
- </div>
241
- <div>
242
- <label class="form-label">Side Offset (px)</label>
243
- <input id="field-btt-offset" type="number" class="form-input" placeholder="32">
244
- <p class="form-hint text-muted" style="margin-top:.35rem;font-size:.8rem;">Distance from the left or right
245
- edge.</p>
246
- </div>
247
- <div>
248
- <label class="form-label">Bottom Offset (px)</label>
249
- <input id="field-btt-bottom-offset" type="number" class="form-input" placeholder="32">
250
- <p class="form-hint text-muted" style="margin-top:.35rem;font-size:.8rem;">Distance from the bottom edge.</p>
99
+ <!-- Footer -->
100
+ <div class="tab-panel">
101
+ <div class="stx-card">
102
+ <div class="stx-card-title">Footer</div>
103
+ <div class="stx-grid">
104
+ <div class="stx-wide">
105
+ <label class="form-label" for="stx-footer-copy">Copyright text</label>
106
+ <input id="stx-footer-copy" type="text" class="form-input" placeholder="© 2026 My Site" data-model="footerCopyright.value">
107
+ </div>
108
+ <div class="stx-wide stx-setting">
109
+ <label class="form-label" for="stx-motion">Show a “Reduce motion” switch
110
+ <span data-help="An accessibility switch in the footer that lets visitors turn animations off. Turning this off only hides the switch - visitors who already chose it, and anyone whose browser asks for reduced motion, still get a calmer site."></span></label>
111
+ <input id="stx-motion" type="checkbox" class="form-switch" data-model="footerMotionToggle.value">
112
+ </div>
113
+ </div>
114
+ </div>
115
+ <div class="stx-card">
116
+ <div class="stx-card-title">Social media icons
117
+ <span data-help="The full address of each profile - its icon then shows in the footer. Leave one empty to hide its icon."></span></div>
118
+ <div class="stx-grid">
119
+ <div><label class="form-label" for="stx-s-tw">X / Twitter</label>
120
+ <input id="stx-s-tw" type="url" class="form-input" placeholder="https://x.com/yourhandle" data-model="socialTwitter.value"></div>
121
+ <div><label class="form-label" for="stx-s-fb">Facebook</label>
122
+ <input id="stx-s-fb" type="url" class="form-input" placeholder="https://facebook.com/yourpage" data-model="socialFacebook.value"></div>
123
+ <div><label class="form-label" for="stx-s-ig">Instagram</label>
124
+ <input id="stx-s-ig" type="url" class="form-input" placeholder="https://instagram.com/yourhandle" data-model="socialInstagram.value"></div>
125
+ <div><label class="form-label" for="stx-s-li">LinkedIn</label>
126
+ <input id="stx-s-li" type="url" class="form-input" placeholder="https://linkedin.com/company/yourcompany" data-model="socialLinkedin.value"></div>
127
+ <div><label class="form-label" for="stx-s-gh">GitHub</label>
128
+ <input id="stx-s-gh" type="url" class="form-input" placeholder="https://github.com/yourorg" data-model="socialGithub.value"></div>
129
+ <div><label class="form-label" for="stx-s-yt">YouTube</label>
130
+ <input id="stx-s-yt" type="url" class="form-input" placeholder="https://youtube.com/@yourchannel" data-model="socialYoutube.value"></div>
131
+ </div>
132
+ </div>
251
133
  </div>
252
- </div>
253
- </div>
254
134
 
255
- <!-- Cookie Consent -->
256
- <div class="tab-panel">
257
- <div class="row mb-3">
258
- <div class="col">
259
- <label class="form-check-label" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
260
- <input id="field-cc-enabled" type="checkbox" class="form-check"> Enable Cookie Consent banner
261
- </label>
262
- </div>
263
- </div>
264
- <div class="row mb-3">
265
- <div class="col">
266
- <label class="form-label">Message</label>
267
- <textarea id="field-cc-message" class="form-input" rows="3" placeholder="We use cookies…"></textarea>
268
- </div>
269
- </div>
270
- <div class="row mb-3">
271
- <div class="col-3">
272
- <label class="form-label">Accept All Text</label>
273
- <input id="field-cc-accept-all" type="text" class="form-input" placeholder="Accept All">
274
- </div>
275
- <div class="col-3">
276
- <label class="form-label">Reject All Text</label>
277
- <input id="field-cc-reject-all" type="text" class="form-input" placeholder="Reject All">
278
- </div>
279
- <div class="col-3">
280
- <label class="form-label">Customise Text</label>
281
- <input id="field-cc-customize" type="text" class="form-input" placeholder="Customize">
282
- </div>
283
- <div class="col-3">
284
- <label class="form-label">Save Preferences Text</label>
285
- <input id="field-cc-save-prefs" type="text" class="form-input" placeholder="Save Preferences">
286
- </div>
287
- </div>
288
- <div class="row mb-3">
289
- <div class="col-6">
290
- <label class="form-label">Privacy Policy Text</label>
291
- <input id="field-cc-privacy-text" type="text" class="form-input" placeholder="Privacy Policy">
292
- </div>
293
- <div class="col-6">
294
- <label class="form-label">Privacy Policy URL</label>
295
- <input id="field-cc-privacy-url" type="text" class="form-input" placeholder="/privacy-policy">
296
- </div>
297
- </div>
298
- <div class="row mb-3">
299
- <div class="col-6">
300
- <label class="form-label">Cookie Policy Text</label>
301
- <input id="field-cc-cookie-text" type="text" class="form-input" placeholder="Cookie Policy">
302
- </div>
303
- <div class="col-6">
304
- <label class="form-label">Cookie Policy URL <span class="text-muted" style="font-weight:400;">(leave blank to hide)</span></label>
305
- <input id="field-cc-cookie-url" type="text" class="form-input" placeholder="/cookie-policy">
306
- </div>
307
- </div>
308
- <div class="row mb-3">
309
- <div class="col-4">
310
- <label class="form-label">Position</label>
311
- <select id="field-cc-position" class="form-select">
312
- <option value="bottom">Bottom</option>
313
- <option value="top">Top</option>
314
- </select>
315
- </div>
316
- <div class="col-4">
317
- <label class="form-label">Layout</label>
318
- <select id="field-cc-layout" class="form-select">
319
- <option value="bar">Bar</option>
320
- <option value="modal">Modal</option>
321
- </select>
322
- </div>
323
- <div class="col-4">
324
- <label class="form-label">Theme</label>
325
- <select id="field-cc-theme" class="form-select">
326
- <option value="dark">Dark</option>
327
- <option value="light">Light</option>
328
- </select>
329
- </div>
330
- </div>
331
- <div class="row mb-3">
332
- <div class="col">
333
- <label class="form-label" style="margin-bottom:.5rem;">Cookie Categories</label>
334
- <p class="text-muted" style="font-size:.8rem;margin-bottom:.75rem;">Necessary cookies are always shown and required. Toggle additional categories below.</p>
335
- <div style="display:flex;flex-direction:column;gap:.5rem;">
336
- <label class="form-check-label" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
337
- <input id="field-cc-show-functional" type="checkbox" class="form-check"> Show Functional category
338
- </label>
339
- <label class="form-check-label" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
340
- <input id="field-cc-show-analytics" type="checkbox" class="form-check"> Show Analytics category
341
- </label>
342
- <label class="form-check-label" style="display:flex;align-items:center;gap:.5rem;cursor:pointer;">
343
- <input id="field-cc-show-marketing" type="checkbox" class="form-check"> Show Marketing category
344
- </label>
345
- </div>
346
- </div>
347
- </div>
348
- <div class="row">
349
- <div class="col-4">
350
- <label class="form-label">Consent Version</label>
351
- <input id="field-cc-version" type="text" class="form-input" placeholder="1.0">
352
- <p class="form-hint text-muted" style="margin-top:.35rem;font-size:.8rem;">Increment to re-prompt users who already accepted.</p>
135
+ <!-- Email -->
136
+ <div class="tab-panel">
137
+ <!-- Without a host the CMS falls back to an Ethereal test inbox, which ACCEPTS the mail and reports
138
+ success - so nothing anywhere else looks wrong. Follows the field, not only the saved value. -->
139
+ <p class="stx-alert is-warn" data-bind-hidden="!noSmtp.value">
140
+ <span data-icon="alert-triangle" data-icon-size="16"></span>
141
+ <span><strong>No SMTP host.</strong> Mail is going to a throwaway test inbox and will <strong>not</strong>
142
+ reach anyone - password resets and form notifications will look like they sent. Set a host and save.</span></p>
143
+ <div class="stx-card">
144
+ <div class="stx-card-title">SMTP server
145
+ <span data-help-title="SMTP" data-help="The mail server the site sends through - your email provider's (for example smtp.gmail.com, or your host's). Its username and password are the mail account's. The password is never shown again once saved: leave it empty to keep it, type a new one to change it."></span></div>
146
+ <div class="stx-grid">
147
+ <div>
148
+ <label class="form-label" for="stx-smtp-host">SMTP host</label>
149
+ <input id="stx-smtp-host" type="text" class="form-input" placeholder="smtp.example.com" data-model="smtpHost.value">
150
+ </div>
151
+ <div>
152
+ <label class="form-label" for="stx-smtp-port">Port
153
+ <span data-help="587 for STARTTLS (most providers), 465 with “Use TLS” on, 25 only for a server on your own network."></span></label>
154
+ <input id="stx-smtp-port" type="number" class="form-input" placeholder="587" data-model="smtpPort.value">
155
+ </div>
156
+ <div>
157
+ <label class="form-label" for="stx-smtp-user">Username</label>
158
+ <input id="stx-smtp-user" type="text" class="form-input" placeholder="user@example.com" autocomplete="off" data-model="smtpUser.value">
159
+ </div>
160
+ <div>
161
+ <label class="form-label" for="stx-smtp-pass">Password</label>
162
+ <input id="stx-smtp-pass" type="password" class="form-input" placeholder="Leave empty to keep the saved one" autocomplete="new-password" data-model="smtpPass.value">
163
+ </div>
164
+ <div class="stx-wide stx-setting">
165
+ <label class="form-label" for="stx-smtp-secure">Use TLS (port 465)
166
+ <span data-help="On for SMTPS (port 465). Off for STARTTLS (port 587), which upgrades to an encrypted connection anyway."></span></label>
167
+ <input id="stx-smtp-secure" type="checkbox" class="form-switch" data-model="smtpSecure.value">
168
+ </div>
169
+ </div>
170
+ </div>
171
+ <div class="stx-card">
172
+ <div class="stx-card-title">Sender
173
+ <span data-help="Who the site's mail comes from. Most providers only accept a From address on the account (or domain) you sign in with."></span></div>
174
+ <div class="stx-grid">
175
+ <div>
176
+ <label class="form-label" for="stx-from-addr">From address</label>
177
+ <input id="stx-from-addr" type="email" class="form-input" placeholder="noreply@example.com" data-model="smtpFromAddress.value">
178
+ </div>
179
+ <div>
180
+ <label class="form-label" for="stx-from-name">From name</label>
181
+ <input id="stx-from-name" type="text" class="form-input" placeholder="My Website" data-model="smtpFromName.value">
182
+ </div>
183
+ </div>
184
+ </div>
185
+ <div class="stx-card">
186
+ <div class="stx-card-title">Send a test email
187
+ <span data-help="Sends through the SAVED settings - Save first if you have changed anything above. Leave the address empty to send to the From address."></span></div>
188
+ <div class="stx-inline">
189
+ <input type="email" class="form-input stx-test-to" placeholder="recipient@example.com" aria-label="Send the test to" data-model="testTo.value">
190
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="sendTest" data-bind-disabled="testing.value">
191
+ <span data-icon="mail" data-icon-size="14"></span> Send test</button>
192
+ <span class="stx-result" data-bind-class="testTone.value" data-bind-text="testResult.value"></span>
193
+ </div>
194
+ <small class="stx-hint" data-bind-hidden="!dirtyEmail.value">You have unsaved email changes - the test uses the saved ones.</small>
195
+ </div>
353
196
  </div>
354
- </div>
355
- </div>
356
197
 
357
- <!-- Custom CSS -->
358
- <!-- Breadcrumbs -->
359
- <div class="tab-panel">
360
- <div class="row mb-3">
361
- <div class="col">
362
- <div class="card">
363
- <div class="card-header"><strong>Breadcrumb Navigation</strong></div>
364
- <div class="card-body">
365
- <p class="text-muted mb-3" style="font-size:.875rem;">
366
- When enabled, a frosted pill breadcrumb trail is fixed to your chosen viewport corner on all public
367
- pages.
368
- Individual pages can opt-out via "Show Breadcrumbs" in the page editor.
369
- </p>
370
- <div class="row mb-4">
371
- <div class="col-auto" style="display:flex;align-items:center;gap:.75rem;">
372
- <input id="field-breadcrumbs-enabled" type="checkbox" class="form-check">
373
- <label for="field-breadcrumbs-enabled">Enable breadcrumbs on all pages</label>
198
+ <!-- Back to top -->
199
+ <div class="tab-panel">
200
+ <div class="stx-card">
201
+ <div class="stx-card-title">Back to top button
202
+ <span data-help="A button that appears once a visitor has scrolled down, and takes them back to the top of the page."></span></div>
203
+ <div class="stx-grid">
204
+ <div class="stx-setting"><label class="form-label" for="stx-btt-on">Show the button</label>
205
+ <input id="stx-btt-on" type="checkbox" class="form-switch" data-model="bttEnabled.value"></div>
206
+ <div class="stx-setting"><label class="form-label" for="stx-btt-smooth">Smooth scroll</label>
207
+ <input id="stx-btt-smooth" type="checkbox" class="form-switch" data-model="bttSmooth.value"></div>
208
+ <div class="stx-wide">
209
+ <label class="form-label" for="stx-btt-label">Label <small class="stx-muted">(optional - empty is icon-only)</small></label>
210
+ <input id="stx-btt-label" type="text" class="form-input" placeholder="Back to top" data-model="bttLabel.value">
211
+ </div>
212
+ <div>
213
+ <label class="form-label" for="stx-btt-thr">Appears after (px)
214
+ <span data-help="How far a visitor has to scroll down before the button shows."></span></label>
215
+ <input id="stx-btt-thr" type="number" class="form-input" placeholder="300" data-model="bttThreshold.value">
216
+ </div>
217
+ <div>
218
+ <label class="form-label" for="stx-btt-pos">Position</label>
219
+ <select id="stx-btt-pos" class="form-select" data-model="bttPosition.value">
220
+ <option value="bottom-right">Bottom right</option>
221
+ <option value="bottom-left">Bottom left</option>
222
+ </select>
223
+ </div>
224
+ <div>
225
+ <label class="form-label" for="stx-btt-off">Side offset (px)</label>
226
+ <input id="stx-btt-off" type="number" class="form-input" placeholder="32" data-model="bttOffset.value">
227
+ </div>
228
+ <div>
229
+ <label class="form-label" for="stx-btt-boff">Bottom offset (px)</label>
230
+ <input id="stx-btt-boff" type="number" class="form-input" placeholder="32" data-model="bttBottomOffset.value">
231
+ </div>
374
232
  </div>
375
- </div>
233
+ </div>
234
+ </div>
376
235
 
377
- <div class="row mb-4" style="align-items:flex-end;gap:0;">
378
- <div class="col-4">
379
- <label class="form-label">Home label</label>
380
- <input id="field-breadcrumbs-home-label" type="text" class="form-input" placeholder="Home">
236
+ <!-- Cookie consent -->
237
+ <div class="tab-panel">
238
+ <div class="stx-card">
239
+ <div class="stx-card-title">Cookie consent banner
240
+ <span data-help-title="Cookie consent" data-help="The banner that asks visitors which cookies they accept. Necessary cookies are always allowed; the categories below are the ones visitors can turn off. Raise the consent version to ask everyone again - after changing what the site stores, for example."></span></div>
241
+ <div class="stx-grid">
242
+ <div class="stx-wide stx-setting"><label class="form-label" for="stx-cc-on">Show the banner</label>
243
+ <input id="stx-cc-on" type="checkbox" class="form-switch" data-model="ccEnabled.value"></div>
244
+ <div class="stx-wide">
245
+ <label class="form-label" for="stx-cc-msg">Message</label>
246
+ <textarea id="stx-cc-msg" class="form-input" rows="3" placeholder="We use cookies…" data-model="ccMessage.value"></textarea>
247
+ </div>
248
+ <div><label class="form-label" for="stx-cc-pos">Position</label>
249
+ <select id="stx-cc-pos" class="form-select" data-model="ccPosition.value">
250
+ <option value="bottom">Bottom</option><option value="top">Top</option></select></div>
251
+ <div><label class="form-label" for="stx-cc-layout">Layout</label>
252
+ <select id="stx-cc-layout" class="form-select" data-model="ccLayout.value">
253
+ <option value="bar">Bar</option><option value="modal">Modal</option></select></div>
254
+ <div><label class="form-label" for="stx-cc-theme">Colours</label>
255
+ <select id="stx-cc-theme" class="form-select" data-model="ccTheme.value">
256
+ <option value="dark">Dark</option><option value="light">Light</option></select></div>
257
+ <div><label class="form-label" for="stx-cc-ver">Consent version</label>
258
+ <input id="stx-cc-ver" type="text" class="form-input" placeholder="1.0" data-model="ccVersion.value"></div>
381
259
  </div>
382
- </div>
383
-
384
- <label class="form-label" style="margin-bottom:.6rem;">Pill Position &amp; Offsets</label>
385
- <div class="row" style="align-items:flex-start;gap:0;">
386
-
387
- <!-- Corner picker -->
388
- <div class="col-auto" style="margin-right:1.5rem;margin-bottom:1rem;">
389
- <div id="bc-pos-grid"
390
- style="display:grid;grid-template-columns:1fr 1fr;gap:2px;border:1px solid var(--dm-border,rgba(255,255,255,.1));border-radius:10px;overflow:hidden;background:var(--dm-border,rgba(255,255,255,.07));width:fit-content;">
391
- <button type="button" class="bc-pos-btn" data-pos="TL"
392
- style="display:flex;flex-direction:column;align-items:flex-start;padding:.55rem .8rem;background:none;border:none;cursor:pointer;font-size:.78rem;color:var(--dm-text-muted,#888);border-right:1px solid var(--dm-border,rgba(255,255,255,.08));border-bottom:1px solid var(--dm-border,rgba(255,255,255,.08));border-radius:0;gap:.05rem;user-select:none;transition:background .15s,color .15s;min-width:72px;">
393
- <span
394
- style="font-size:.6rem;opacity:.55;letter-spacing:.06em;text-transform:uppercase;line-height:1.2;">Top</span>
395
- <span style="font-weight:600;">↖ Left</span>
396
- </button>
397
- <button type="button" class="bc-pos-btn" data-pos="TR"
398
- style="display:flex;flex-direction:column;align-items:flex-end;padding:.55rem .8rem;background:none;border:none;cursor:pointer;font-size:.78rem;color:var(--dm-text-muted,#888);border-bottom:1px solid var(--dm-border,rgba(255,255,255,.08));border-radius:0;gap:.05rem;user-select:none;transition:background .15s,color .15s;min-width:72px;">
399
- <span
400
- style="font-size:.6rem;opacity:.55;letter-spacing:.06em;text-transform:uppercase;line-height:1.2;">Top</span>
401
- <span style="font-weight:600;">Right ↗</span>
402
- </button>
403
- <button type="button" class="bc-pos-btn" data-pos="BL"
404
- style="display:flex;flex-direction:column;align-items:flex-start;padding:.55rem .8rem;background:none;border:none;cursor:pointer;font-size:.78rem;color:var(--dm-text-muted,#888);border-right:1px solid var(--dm-border,rgba(255,255,255,.08));border-radius:0;gap:.05rem;user-select:none;transition:background .15s,color .15s;min-width:72px;">
405
- <span style="font-weight:600;">↙ Left</span>
406
- <span
407
- style="font-size:.6rem;opacity:.55;letter-spacing:.06em;text-transform:uppercase;line-height:1.2;">Bottom</span>
408
- </button>
409
- <button type="button" class="bc-pos-btn" data-pos="BR"
410
- style="display:flex;flex-direction:column;align-items:flex-end;padding:.55rem .8rem;background:none;border:none;cursor:pointer;font-size:.78rem;color:var(--dm-text-muted,#888);border-radius:0;gap:.05rem;user-select:none;transition:background .15s,color .15s;min-width:72px;">
411
- <span style="font-weight:600;">Right ↘</span>
412
- <span
413
- style="font-size:.6rem;opacity:.55;letter-spacing:.06em;text-transform:uppercase;line-height:1.2;">Bottom</span>
414
- </button>
415
- </div>
416
- <input type="hidden" id="field-breadcrumbs-position" value="TL">
260
+ </div>
261
+ <div class="stx-card">
262
+ <div class="stx-card-title">Categories</div>
263
+ <div class="stx-grid">
264
+ <div class="stx-setting"><label class="form-label" for="stx-cc-fn">Functional</label>
265
+ <input id="stx-cc-fn" type="checkbox" class="form-switch" data-model="ccShowFunctional.value"></div>
266
+ <div class="stx-setting"><label class="form-label" for="stx-cc-an">Analytics</label>
267
+ <input id="stx-cc-an" type="checkbox" class="form-switch" data-model="ccShowAnalytics.value"></div>
268
+ <div class="stx-setting"><label class="form-label" for="stx-cc-mk">Marketing</label>
269
+ <input id="stx-cc-mk" type="checkbox" class="form-switch" data-model="ccShowMarketing.value"></div>
417
270
  </div>
418
-
419
- <!-- Offsets -->
420
- <div class="col-3" style="margin-right:1rem;">
421
- <label class="form-label" for="field-bc-offset-x">Horizontal offset</label>
422
- <div style="display:flex;align-items:center;gap:.4rem;">
423
- <input id="field-bc-offset-x" type="number" class="form-input" min="0" max="500" placeholder="8"
424
- style="max-width:90px;">
425
- <span class="text-muted" style="font-size:.8rem;">px</span>
426
- </div>
427
- <p class="form-hint text-muted" style="margin-top:.35rem;font-size:.75rem;">Distance from left or
428
- right edge.</p>
271
+ </div>
272
+ <div class="stx-card">
273
+ <div class="stx-card-title">Links and button text</div>
274
+ <div class="stx-grid">
275
+ <div><label class="form-label" for="stx-cc-pt">Privacy policy text</label>
276
+ <input id="stx-cc-pt" type="text" class="form-input" placeholder="Privacy Policy" data-model="ccPrivacyText.value"></div>
277
+ <div><label class="form-label" for="stx-cc-pu">Privacy policy address
278
+ <span data-help="The page the banner's privacy link opens, such as /privacy. A banner with no privacy policy is flagged under Needs attention."></span></label>
279
+ <input id="stx-cc-pu" type="text" class="form-input" placeholder="/privacy-policy" data-model="ccPrivacyUrl.value"></div>
280
+ <div><label class="form-label" for="stx-cc-ct">Cookie policy text</label>
281
+ <input id="stx-cc-ct" type="text" class="form-input" placeholder="Cookie Policy" data-model="ccCookieText.value"></div>
282
+ <div><label class="form-label" for="stx-cc-cu">Cookie policy address <small class="stx-muted">(empty hides it)</small></label>
283
+ <input id="stx-cc-cu" type="text" class="form-input" placeholder="/cookie-policy" data-model="ccCookieUrl.value"></div>
284
+ <div><label class="form-label" for="stx-cc-aa">Accept all</label>
285
+ <input id="stx-cc-aa" type="text" class="form-input" placeholder="Accept All" data-model="ccAcceptAll.value"></div>
286
+ <div><label class="form-label" for="stx-cc-ra">Reject all</label>
287
+ <input id="stx-cc-ra" type="text" class="form-input" placeholder="Reject All" data-model="ccRejectAll.value"></div>
288
+ <div><label class="form-label" for="stx-cc-cz">Customise</label>
289
+ <input id="stx-cc-cz" type="text" class="form-input" placeholder="Customize" data-model="ccCustomize.value"></div>
290
+ <div><label class="form-label" for="stx-cc-sp">Save preferences</label>
291
+ <input id="stx-cc-sp" type="text" class="form-input" placeholder="Save Preferences" data-model="ccSavePrefs.value"></div>
429
292
  </div>
293
+ </div>
294
+ </div>
430
295
 
431
- <div class="col-3">
432
- <label class="form-label" for="field-bc-offset-y">Vertical offset</label>
433
- <div style="display:flex;align-items:center;gap:.4rem;">
434
- <input id="field-bc-offset-y" type="number" class="form-input" min="0" max="500" placeholder="60"
435
- style="max-width:90px;">
436
- <span class="text-muted" style="font-size:.8rem;">px</span>
437
- </div>
438
- <p class="form-hint text-muted" style="margin-top:.35rem;font-size:.75rem;">Distance from top or
439
- bottom edge.</p>
296
+ <!-- Breadcrumbs -->
297
+ <div class="tab-panel">
298
+ <div class="stx-card">
299
+ <div class="stx-card-title">Breadcrumb trail
300
+ <span data-help-title="Breadcrumbs" data-help="A small frosted pill fixed to a corner of every public page, showing where the page sits (Home › Services › Web). A page can switch it off with “Show Breadcrumbs” in the page editor."></span></div>
301
+ <div class="stx-grid">
302
+ <div class="stx-wide stx-setting"><label class="form-label" for="stx-bc-on">Show breadcrumbs on every page</label>
303
+ <input id="stx-bc-on" type="checkbox" class="form-switch" data-model="bcEnabled.value"></div>
304
+ <div><label class="form-label" for="stx-bc-home">Home label</label>
305
+ <input id="stx-bc-home" type="text" class="form-input" placeholder="Home" data-model="bcHomeLabel.value"></div>
306
+ <div></div>
307
+ <div>
308
+ <span class="form-label">Corner</span>
309
+ <div class="stx-corners" role="group" aria-label="Corner">
310
+ <button type="button" data-on-click="corner('TL')" data-bind-class="bcPosition.value === 'TL' && 'is-on'">↖ Top left</button>
311
+ <button type="button" data-on-click="corner('TR')" data-bind-class="bcPosition.value === 'TR' && 'is-on'">Top right ↗</button>
312
+ <button type="button" data-on-click="corner('BL')" data-bind-class="bcPosition.value === 'BL' && 'is-on'">↙ Bottom left</button>
313
+ <button type="button" data-on-click="corner('BR')" data-bind-class="bcPosition.value === 'BR' && 'is-on'">Bottom right ↘</button>
314
+ </div>
315
+ </div>
316
+ <div class="stx-pair">
317
+ <div><label class="form-label" for="stx-bc-x">From the side (px)</label>
318
+ <input id="stx-bc-x" type="number" class="form-input" min="0" max="500" placeholder="8" data-model="bcOffsetX.value"></div>
319
+ <div><label class="form-label" for="stx-bc-y">From the top/bottom (px)</label>
320
+ <input id="stx-bc-y" type="number" class="form-input" min="0" max="500" placeholder="8" data-model="bcOffsetY.value"></div>
321
+ </div>
440
322
  </div>
441
- </div>
442
-
443
323
  </div>
444
- </div>
445
- </div>
446
- </div>
447
- <div class="row">
448
- <div class="col">
449
- <button id="save-breadcrumbs-btn" class="btn btn-primary"><span data-icon="save"></span> Save Breadcrumbs Settings</button>
450
324
  </div>
451
- </div>
452
- </div>
453
325
 
454
- <!-- Custom CSS -->
455
- <div class="tab-panel">
456
- <div class="row mb-3">
457
- <div class="col">
458
- <p class="text-muted" style="font-size:.875rem;margin-bottom:1rem;">
459
- CSS entered here is injected as an inline <code>&lt;style&gt;</code> block into the
460
- <code>&lt;head&gt;</code> of every public page. Changes take effect immediately on save.
461
- </p>
462
- <textarea id="field-custom-css" class="form-input" style="font-family:monospace;min-height:420px;resize:vertical;" spellcheck="false" placeholder="/* Add your custom CSS here */"></textarea>
463
- </div>
464
- </div>
465
- <div class="row">
466
- <div class="col">
467
- <button id="save-css-btn" class="btn btn-primary"><span data-icon="save"></span> Save CSS</button>
326
+ <!-- Custom CSS -->
327
+ <div class="tab-panel">
328
+ <div class="stx-card">
329
+ <div class="stx-card-title">Custom CSS
330
+ <span data-help="Added to the head of every public page, after the theme - so it has the last word. Saved with everything else (Save, or Ctrl+S). Up to 100 KB."></span></div>
331
+ <textarea class="form-input stx-css" spellcheck="false" placeholder="/* Add your custom CSS here */"></textarea>
332
+ <small class="stx-hint" data-bind-text="cssSize.value"></small>
333
+ </div>
468
334
  </div>
469
- </div>
470
- </div>
471
335
 
472
- <!-- Cache -->
473
- <div class="tab-panel">
474
- <div class="row mb-3">
475
- <div class="col">
476
- <p class="text-muted" style="font-size:.875rem;margin-bottom:1rem;">
477
- Status: <strong id="cache-status">checking…</strong>
478
- </p>
479
- <p class="text-muted" style="font-size:.875rem;margin-bottom:1rem;">
480
- Public page renders are cached by URL and role. Writes (page, collection, form, block, view, navigation, site settings) auto-invalidate the relevant tags. Use the button below to flush every cached entry - the next visitor to each page will trigger a fresh render.
481
- </p>
482
- <button id="btn-clear-cache" class="btn btn-secondary" type="button"><span data-icon="trash"></span> Clear cache</button>
483
- <button id="btn-refresh-cache-keys" class="btn btn-ghost" type="button" style="margin-left:8px;"><span data-icon="refresh"></span> Refresh list</button>
484
- </div>
485
- </div>
486
- <div class="row">
487
- <div class="col">
488
- <h4 style="margin-top:1.5rem;">Cached entries <span id="cache-key-count" class="text-muted" style="font-weight:normal;font-size:.875rem;"></span></h4>
489
- <p class="text-muted" style="font-size:.85rem;margin-bottom:.5rem;">
490
- Snapshot of what's currently keyed. Newest entries first. Values are not shown - they can be large HTML blobs.
491
- </p>
492
- <div id="cache-keys-table"></div>
336
+ <!-- Cache -->
337
+ <div class="tab-panel">
338
+ <div class="stx-card">
339
+ <div class="stx-card-title">Response cache
340
+ <span data-help-title="Response cache" data-help="Public pages are kept rendered, per address and per visitor role, so the next visitor gets them at once. Saving a page, collection, form, block, view, menu or these settings clears what it affects automatically; Clear empties it all. Turning it off makes every visit render afresh - useful while debugging, slower for visitors."></span>
341
+ <span class="stx-spacer"></span>
342
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="refreshCache"><span data-icon="refresh-cw" data-icon-size="14"></span> Refresh</button>
343
+ <button type="button" class="btn btn-sm btn-ghost" data-on-click="toggleCache" data-bind-text="cacheToggleText.value">Turn off</button>
344
+ <button type="button" class="btn btn-sm btn-outline" data-on-click="clearCache"><span data-icon="trash" data-icon-size="14"></span> Clear</button>
345
+ </div>
346
+ <div class="stx-chips">
347
+ <span class="stx-chip" data-bind-class="cacheOn.value ? 'is-ok' : 'is-warn'" data-bind-text="cacheState.value"></span>
348
+ <span class="stx-chip" data-bind-hidden="!cacheSize.value" data-bind-text="cacheSize.value"></span>
349
+ <span class="stx-chip" data-bind-hidden="!cacheHits.value" data-bind-text="cacheHits.value"></span>
350
+ </div>
351
+ </div>
352
+ <div class="stx-card">
353
+ <div class="stx-card-title">Cached pages <span class="stx-count" data-bind-text="cacheRows.value.length"></span>
354
+ <span data-help="What is in the cache now, newest first: the page, the visitor role it was rendered for, and when it expires. Right-click one (or ⋮) to open the page or copy its key."></span>
355
+ <span class="stx-spacer"></span>
356
+ <input type="search" class="form-input stx-search" placeholder="Filter…" aria-label="Filter cached pages" data-model="cacheQuery.value">
357
+ </div>
358
+ <p class="stx-note" data-bind-hidden="!cacheLoading.value">Loading…</p>
359
+ <p class="stx-note is-bad" data-bind-hidden="!cacheError.value" data-bind-text="cacheError.value"></p>
360
+ <p class="stx-note" data-bind-hidden="cacheLoading.value || cacheError.value || cacheRows.value.length" data-bind-text="cacheEmpty.value"></p>
361
+ <div class="stx-list" data-each="cacheRows.value key=id"></div>
362
+ </div>
493
363
  </div>
494
- </div>
495
364
  </div>
496
-
497
- </div>
498
365
  </div>
366
+
367
+ <style>
368
+ .stx { display: flex; flex-direction: column; gap: .75rem; }
369
+ .stx [hidden] { display: none !important; }
370
+ .stx-alert { margin: 0; display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; padding: .6rem .8rem; border-radius: var(--dm-radius-md); color: var(--dm-text); }
371
+ .stx-alert.is-dirty { background: color-mix(in srgb, var(--dm-primary) 12%, transparent); }
372
+ .stx-alert.is-warn { background: color-mix(in srgb, var(--dm-warning) 16%, transparent); }
373
+ .stx-alert.is-bad { background: color-mix(in srgb, var(--dm-danger) 14%, transparent); }
374
+ .stx-spacer { flex: 1; }
375
+ .stx-alert.is-dirty > span:nth-child(2) { flex: 1 1 18rem; min-width: 0; }
376
+ .stx-tabs .tab-item { display: inline-flex; align-items: center; gap: .35rem; }
377
+ .stx-dot { display: inline-block; width: .45rem; height: .45rem; border-radius: 50%; background: var(--dm-primary); }
378
+ .stx-dot.is-bad { background: var(--dm-danger); }
379
+ .stx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .65rem; margin-top: .75rem; }
380
+ .stx-card-title { display: flex; align-items: center; gap: .4rem; font-weight: 600; flex-wrap: wrap; }
381
+ .stx-card-title .btn { display: inline-flex; align-items: center; gap: .3rem; font-weight: 500; }
382
+ .stx-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem 1rem; }
383
+ .stx-wide { grid-column: 1 / -1; }
384
+ .stx-pair { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; }
385
+ .stx .form-label { display: flex; align-items: center; gap: .3rem; }
386
+ .stx-setting { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
387
+ .stx-setting .form-label { margin: 0; }
388
+ .stx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .25rem; }
389
+ .stx-muted { color: var(--dm-text-muted); font-weight: 400; }
390
+ .stx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
391
+ .stx-note.is-bad { color: var(--dm-danger); }
392
+ .stx-moved { flex-direction: row; align-items: center; gap: .75rem; background: var(--dm-surface-raised); }
393
+ .stx-moved-text { flex: 1; display: flex; flex-direction: column; }
394
+ .stx-moved-text small { color: var(--dm-text-muted); }
395
+ .stx-problems { display: flex; flex-direction: column; gap: .35rem; }
396
+ .stx-problem { display: flex; align-items: center; gap: .6rem; font-size: var(--dm-text-sm); }
397
+ .stx-problem button { margin-left: auto; }
398
+ .stx-inline { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
399
+ .stx-inline .btn { display: inline-flex; align-items: center; gap: .3rem; }
400
+ .stx-test-to { max-width: 18rem; }
401
+ .stx-result { font-size: var(--dm-text-sm); }
402
+ .stx-result.is-ok { color: var(--dm-success); }
403
+ .stx-result.is-bad { color: var(--dm-danger); }
404
+ .stx-corners { display: grid; grid-template-columns: 1fr 1fr; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; width: fit-content; }
405
+ .stx-corners button { border: 0; background: none; color: var(--dm-text); padding: .45rem .8rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
406
+ .stx-corners button:nth-child(odd) { border-right: 1px solid var(--dm-border); }
407
+ .stx-corners button:nth-child(-n+2) { border-bottom: 1px solid var(--dm-border); }
408
+ .stx-corners button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
409
+ .stx-corners button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
410
+ .stx-css { width: 100%; min-height: 420px; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm); }
411
+ .stx-chips { display: flex; gap: .35rem; flex-wrap: wrap; }
412
+ .stx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
413
+ background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
414
+ .stx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
415
+ .stx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
416
+ .stx-chip.is-bad { background: color-mix(in srgb, var(--dm-danger) 16%, transparent); color: var(--dm-danger); }
417
+ .stx-count { opacity: .65; font-size: var(--dm-text-xs); font-weight: 500; }
418
+ .stx-search { max-width: 14rem; font-weight: 400; }
419
+ .stx-list { display: flex; flex-direction: column; }
420
+ .stx-row { display: flex; align-items: center; gap: .75rem; padding: .45rem .3rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
421
+ .stx-row:last-child { border-bottom: 0; }
422
+ .stx-row:hover, .stx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
423
+ .stx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
424
+ .stx-main strong, .stx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
425
+ .stx-main strong { font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm); font-weight: 600; }
426
+ .stx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
427
+ .stx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
428
+ .stx-kebab:hover { color: var(--dm-primary); }
429
+ @media (max-width: 720px) { .stx-grid, .stx-pair { grid-template-columns: 1fr; } }
430
+ </style>