domma-cms 0.54.2 → 0.66.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 (221) hide show
  1. package/CLAUDE.md +151 -77
  2. package/README.md +4 -4
  3. package/admin/css/admin.css +1 -1
  4. package/admin/dist/domma/domma-tools.css +3 -3
  5. package/admin/dist/domma/domma-tools.min.js +3 -3
  6. package/admin/js/api.js +1 -1
  7. package/admin/js/app.js +3 -3
  8. package/admin/js/lib/page-picker.js +1 -0
  9. package/admin/js/lib/plugin-accent.js +1 -0
  10. package/admin/js/lib/plugin-chrome.js +1 -0
  11. package/admin/js/lib/shortcode-context-menu.js +2 -2
  12. package/admin/js/lib/sidebar-grouping.js +1 -1
  13. package/admin/js/lib/sidebar-grouping.test.js +1 -1
  14. package/admin/js/lib/sidebar-renderer.js +4 -4
  15. package/admin/js/lib/slideover-resizable.js +1 -0
  16. package/admin/js/templates/context-menu-editor.html +212 -0
  17. package/admin/js/templates/context-menus.html +16 -0
  18. package/admin/js/templates/menu-editor.html +19 -25
  19. package/admin/js/templates/plugin-code.html +1 -1
  20. package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
  21. package/admin/js/templates/settings.html +27 -97
  22. package/admin/js/templates/theme.html +173 -0
  23. package/admin/js/views/context-menu-editor.js +55 -0
  24. package/admin/js/views/context-menus.js +5 -0
  25. package/admin/js/views/form-editor.js +7 -7
  26. package/admin/js/views/index.js +1 -1
  27. package/admin/js/views/menu-editor.js +13 -13
  28. package/admin/js/views/plugin-marketplace.js +1 -1
  29. package/admin/js/views/plugins.js +25 -23
  30. package/admin/js/views/search.js +1 -0
  31. package/admin/js/views/settings.js +3 -3
  32. package/admin/js/views/theme.js +1 -0
  33. package/bin/cli.js +11 -2
  34. package/bin/lib/smtp-defaults.js +53 -0
  35. package/bin/update.js +13 -2
  36. package/config/menus/admin-sidebar.json +129 -23
  37. package/config/plugins.json +5 -5
  38. package/config/search.json +13 -0
  39. package/config/theme.json +18 -0
  40. package/package.json +12 -4
  41. package/plugins/_lib/admin/mail/compose-window.js +914 -0
  42. package/plugins/_lib/admin/mail/contacts.js +301 -0
  43. package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
  44. package/plugins/_lib/admin/mail/folder-tree.js +254 -0
  45. package/plugins/_lib/admin/mail/identity.js +480 -0
  46. package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
  47. package/plugins/_lib/admin/mail/keyboard.js +136 -0
  48. package/plugins/_lib/admin/mail/mail.css +1 -0
  49. package/plugins/_lib/admin/mail/mail.html +71 -0
  50. package/plugins/_lib/admin/mail/panes.js +253 -0
  51. package/plugins/_lib/admin/mail/reader-view.js +4453 -0
  52. package/plugins/_lib/admin/mail/resizable.js +26 -0
  53. package/plugins/_lib/admin/mail/rules.js +343 -0
  54. package/plugins/_lib/admin/mail/scheduling.js +203 -0
  55. package/plugins/_lib/admin/mail/section-kit.js +277 -0
  56. package/plugins/_lib/admin/mail/templates.js +238 -0
  57. package/plugins/_lib/admin/mail/threads.js +200 -0
  58. package/plugins/_lib/admin/mail/vacation.js +269 -0
  59. package/plugins/_lib/admin/ui/help.css +1 -0
  60. package/plugins/_lib/admin/ui/help.js +174 -0
  61. package/plugins/_lib/admin/ui/resizable.js +151 -0
  62. package/plugins/_lib/dataStore.js +117 -0
  63. package/plugins/_lib/mail/accounts.js +919 -0
  64. package/plugins/_lib/mail/bodyTokens.js +101 -0
  65. package/plugins/_lib/mail/compose.js +256 -0
  66. package/plugins/_lib/mail/defaults.js +57 -0
  67. package/plugins/_lib/mail/diagnostics.js +154 -0
  68. package/plugins/_lib/mail/envelope.js +161 -0
  69. package/plugins/_lib/mail/folders.js +192 -0
  70. package/plugins/_lib/mail/handoff.js +274 -0
  71. package/plugins/_lib/mail/imapPool.js +293 -0
  72. package/plugins/_lib/mail/mbox.js +74 -0
  73. package/plugins/_lib/mail/pollSchedule.js +79 -0
  74. package/plugins/_lib/mail/poller.js +135 -0
  75. package/plugins/_lib/mail/priority.js +138 -0
  76. package/plugins/_lib/mail/readRoutes.js +680 -0
  77. package/plugins/_lib/mail/render.js +291 -0
  78. package/plugins/_lib/mail/ruleRunner.js +152 -0
  79. package/plugins/_lib/mail/scheduler.js +254 -0
  80. package/plugins/_lib/mail/secretbox.js +229 -0
  81. package/plugins/_lib/mail/send.js +396 -0
  82. package/plugins/_lib/mail/store.js +1002 -0
  83. package/plugins/_lib/mail/sync.js +292 -0
  84. package/plugins/_lib/mail/syncPlan.js +126 -0
  85. package/plugins/_lib/mail/syncSelection.js +82 -0
  86. package/plugins/_lib/mail/unsubscribe.js +183 -0
  87. package/plugins/_lib/mail/vacationRunner.js +114 -0
  88. package/plugins/_lib/mail/write.js +473 -0
  89. package/plugins/_lib/schemaSync.js +83 -0
  90. package/plugins/_template/admin/css/index.css +0 -0
  91. package/plugins/_template/admin/templates/index.html +4 -4
  92. package/plugins/_template/admin/views/index.js +7 -0
  93. package/plugins/analytics/admin/css/index.css +1 -0
  94. package/plugins/analytics/admin/templates/analytics.html +22 -13
  95. package/plugins/analytics/plugin.json +3 -0
  96. package/plugins/blog/admin/css/index.css +1 -0
  97. package/plugins/blog/admin/templates/blog.html +30 -18
  98. package/plugins/blog/admin/templates/categories.html +2 -2
  99. package/plugins/blog/admin/templates/comments.html +2 -2
  100. package/plugins/blog/admin/templates/post-editor.html +34 -34
  101. package/plugins/blog/admin/templates/settings.html +6 -3
  102. package/plugins/blog/admin/views/blog.js +8 -5
  103. package/plugins/blog/admin/views/categories.js +5 -10
  104. package/plugins/blog/admin/views/comments.js +5 -5
  105. package/plugins/blog/admin/views/post-editor.js +39 -20
  106. package/plugins/blog/admin/views/settings.js +52 -50
  107. package/plugins/blog/collections/categories/schema.json +7 -6
  108. package/plugins/blog/collections/comments/schema.json +11 -10
  109. package/plugins/blog/collections/posts/schema.json +14 -13
  110. package/plugins/blog/plugin.js +36 -13
  111. package/plugins/blog/plugin.json +13 -5
  112. package/plugins/blog/plugin.public.js +312 -0
  113. package/plugins/contacts/admin/css/index.css +1 -0
  114. package/plugins/contacts/admin/templates/contacts.html +128 -0
  115. package/plugins/contacts/admin/views/contacts.js +237 -4
  116. package/plugins/contacts/collections/user-contacts/schema.json +108 -0
  117. package/plugins/contacts/plugin.js +214 -27
  118. package/plugins/contacts/plugin.json +4 -1
  119. package/plugins/invoice/admin/css/index.css +1 -0
  120. package/plugins/invoice/admin/templates/editor.html +140 -49
  121. package/plugins/invoice/admin/templates/index.html +153 -23
  122. package/plugins/invoice/admin/templates/issuers.html +2 -5
  123. package/plugins/invoice/admin/templates/receivers.html +2 -5
  124. package/plugins/invoice/admin/views/contacts-source.js +266 -0
  125. package/plugins/invoice/admin/views/editor.js +366 -199
  126. package/plugins/invoice/admin/views/export.js +199 -0
  127. package/plugins/invoice/admin/views/help-content.js +61 -0
  128. package/plugins/invoice/admin/views/index.js +582 -94
  129. package/plugins/invoice/admin/views/issuers.js +24 -17
  130. package/plugins/invoice/admin/views/media.js +172 -0
  131. package/plugins/invoice/admin/views/party-view.js +305 -67
  132. package/plugins/invoice/admin/views/payments.js +127 -0
  133. package/plugins/invoice/admin/views/print.js +130 -0
  134. package/plugins/invoice/admin/views/receivers.js +49 -16
  135. package/plugins/invoice/admin/views/send.js +212 -0
  136. package/plugins/invoice/admin/views/settings.js +594 -0
  137. package/plugins/invoice/admin/views/view-lifecycle.js +33 -0
  138. package/plugins/invoice/collections/invoice-issuers/schema.json +77 -11
  139. package/plugins/invoice/collections/invoice-receivers/schema.json +10 -9
  140. package/plugins/invoice/collections/invoices/schema.json +19 -13
  141. package/plugins/invoice/config.js +27 -6
  142. package/plugins/invoice/pdf.js +164 -0
  143. package/plugins/invoice/plugin.js +1217 -44
  144. package/plugins/invoice/plugin.json +10 -9
  145. package/plugins/invoice/templates/_base.css +1 -0
  146. package/plugins/invoice/templates/classic-nologo.html +100 -0
  147. package/plugins/invoice/templates/classic.html +91 -0
  148. package/plugins/invoice/templates/invoice-print.html +24 -0
  149. package/plugins/invoice/templates/minimal.html +99 -0
  150. package/plugins/invoice/templates/modern-nologo.html +114 -0
  151. package/plugins/invoice/templates/modern.html +113 -0
  152. package/plugins/invoice/templates/templates.json +11 -0
  153. package/plugins/mail-reader/admin/views/mail.js +19 -0
  154. package/plugins/mail-reader/config.js +7 -0
  155. package/plugins/mail-reader/plugin.js +48 -0
  156. package/plugins/mail-reader/plugin.json +33 -0
  157. package/plugins/notes/admin/views/notes.js +1 -1
  158. package/plugins/notes/plugin.json +2 -2
  159. package/plugins/surveys/lib/audience.js +37 -0
  160. package/plugins/surveys/lib/campaigns.js +43 -0
  161. package/plugins/surveys/lib/ledger.js +110 -0
  162. package/plugins/surveys/lib/sending.js +106 -0
  163. package/plugins/surveys/lib/stats.js +62 -0
  164. package/plugins/surveys/lib/submit.js +95 -0
  165. package/plugins/surveys/lib/tokens.js +28 -0
  166. package/plugins/surveys/plugin.public.js +149 -0
  167. package/plugins/theme-switcher/admin/templates/theme-switcher.html +1 -1
  168. package/public/css/forms.css +1 -1
  169. package/public/css/menu-highlight.css +1 -1
  170. package/public/css/search.css +1 -0
  171. package/public/css/site.css +1 -1
  172. package/public/js/collection-context.js +2 -2
  173. package/public/js/context-menus.js +1 -0
  174. package/public/js/form-logic-engine.js +1 -1
  175. package/public/js/forms.js +2 -2
  176. package/public/js/menu-decor.mjs +1 -1
  177. package/public/js/search.js +1 -0
  178. package/public/js/site.js +1 -1
  179. package/scripts/build.js +37 -3
  180. package/scripts/copy-domma.js +48 -0
  181. package/scripts/seed.js +1996 -0
  182. package/scripts/setup.js +8 -0
  183. package/server/routes/api/collections.js +34 -0
  184. package/server/routes/api/context-menus.js +104 -0
  185. package/server/routes/api/forms.js +42 -3
  186. package/server/routes/api/notifications.js +69 -19
  187. package/server/routes/api/plugins.js +50 -6
  188. package/server/routes/api/search.js +43 -0
  189. package/server/routes/api/theme.js +69 -0
  190. package/server/routes/public.js +42 -7
  191. package/server/server.js +74 -0
  192. package/server/services/adapters/FileAdapter.js +6 -1
  193. package/server/services/content.js +26 -0
  194. package/server/services/contextMenus.js +477 -0
  195. package/server/services/email.js +29 -3
  196. package/server/services/health.js +23 -2
  197. package/server/services/markdown.js +70 -9
  198. package/server/services/menuRender.js +28 -4
  199. package/server/services/menus.js +25 -1
  200. package/server/services/permissionRegistry.js +24 -0
  201. package/server/services/pluginFiles.js +52 -11
  202. package/server/services/plugins.js +229 -6
  203. package/server/services/renderer.js +148 -22
  204. package/server/services/roles.js +1 -1
  205. package/server/services/search-migration.js +82 -0
  206. package/server/services/search.js +413 -0
  207. package/server/services/sidebar-migration.js +1 -0
  208. package/server/services/themeSettings.js +541 -0
  209. package/server/services/users.js +8 -0
  210. package/server/templates/page.html +4 -2
  211. package/plugins/contacts/data/contacts.json +0 -20
  212. package/plugins/notes/data/notes.json +0 -1
  213. package/plugins/site-search/admin/views/site-search.js +0 -116
  214. package/plugins/site-search/config.js +0 -15
  215. package/plugins/site-search/plugin.js +0 -188
  216. package/plugins/site-search/plugin.json +0 -40
  217. package/plugins/site-search/public/inject-body.html +0 -17
  218. package/plugins/site-search/public/inject-head.html +0 -1
  219. package/plugins/site-search/public/search.css +0 -1
  220. package/plugins/site-search/public/search.js +0 -1
  221. package/plugins/todo/data/todos.json +0 -1
@@ -1,116 +0,0 @@
1
- /**
2
- * Site Search Plugin - Admin Settings View
3
- */
4
- import {apiRequest} from '/admin/js/api.js';
5
-
6
- export const siteSearchView = {
7
- templateUrl: '/plugins/site-search/admin/templates/site-search.html',
8
-
9
- async onMount($container) {
10
- let settings = {};
11
- try {
12
- settings = await apiRequest('/plugins/site-search/settings');
13
- } catch {
14
- E.toast('Could not load settings.', {type: 'error'});
15
- }
16
-
17
- // ---- Basic settings ----
18
- $container.find('#field-placeholder').val(settings.placeholder || 'Search pages...');
19
- $container.find('#field-keyboard-shortcut').prop('checked', settings.keyboardShortcut !== false);
20
- $container.find('#field-max-results').val(settings.maxResults ?? 10);
21
- $container.find('#field-min-query-length').val(settings.minQueryLength ?? 2);
22
- $container.find('#field-debounce-ms').val(settings.debounceMs ?? 300);
23
-
24
- // ---- Position settings ----
25
- const displayMode = settings.displayMode || 'navbar';
26
- const fieldMode = document.getElementById('field-display-mode');
27
- const fieldPos = document.getElementById('field-position');
28
- const fieldOffX = document.getElementById('field-offset-x');
29
- const fieldOffY = document.getElementById('field-offset-y');
30
- const fieldIconOnly = document.getElementById('field-icon-only');
31
- const posSection = document.getElementById('ss-pos-section');
32
- const posGrid = document.getElementById('ss-pos-grid');
33
- const modeBar = document.getElementById('ss-display-mode');
34
-
35
- if (fieldMode) fieldMode.value = displayMode;
36
- if (fieldPos) fieldPos.value = settings.position ?? 'bottom-right';
37
- if (fieldOffX) fieldOffX.value = settings.offsetX ?? 32;
38
- if (fieldOffY) fieldOffY.value = settings.offsetY ?? 32;
39
- if (fieldIconOnly) fieldIconOnly.checked = settings.iconOnly !== false;
40
-
41
- function applyMode(mode) {
42
- if (fieldMode) fieldMode.value = mode;
43
- if (posSection) posSection.style.display = mode === 'floating' ? '' : 'none';
44
- if (modeBar) {
45
- modeBar.querySelectorAll('.ss-mode-btn').forEach(function (btn) {
46
- const active = btn.getAttribute('data-mode') === mode;
47
- btn.classList.toggle('btn-primary', active);
48
- btn.classList.toggle('btn-secondary', !active);
49
- });
50
- }
51
- }
52
-
53
- function updatePosBtns(pos) {
54
- document.querySelectorAll('.ss-pos-btn').forEach(function (btn) {
55
- const active = btn.getAttribute('data-pos') === pos;
56
- btn.style.background = active ? 'var(--dm-primary, #5b8cff)' : '';
57
- btn.style.color = active ? '#fff' : '';
58
- });
59
- }
60
-
61
- applyMode(displayMode);
62
- updatePosBtns(settings.position || 'bottom-right');
63
-
64
- // Mode toggle buttons
65
- if (modeBar) {
66
- modeBar.addEventListener('click', function (e) {
67
- const btn = e.target.closest('.ss-mode-btn');
68
- if (!btn) return;
69
- applyMode(btn.getAttribute('data-mode'));
70
- });
71
- }
72
-
73
- // Position grid
74
- if (posGrid) {
75
- posGrid.addEventListener('click', function (e) {
76
- const btn = e.target.closest('[data-pos]');
77
- if (!btn) return;
78
- const pos = btn.getAttribute('data-pos');
79
- if (fieldPos) fieldPos.value = pos;
80
- updatePosBtns(pos);
81
- });
82
- }
83
-
84
- // ---- Save ----
85
- $container.find('#save-settings-btn').off('click').on('click', async () => {
86
- const data = {
87
- placeholder: $container.find('#field-placeholder').val() || 'Search pages...',
88
- keyboardShortcut: $container.find('#field-keyboard-shortcut').prop('checked'),
89
- maxResults: parseInt($container.find('#field-max-results').val(), 10) || 10,
90
- minQueryLength: parseInt($container.find('#field-min-query-length').val(), 10) || 2,
91
- debounceMs: parseInt($container.find('#field-debounce-ms').val(), 10) || 300,
92
- displayMode: fieldMode ? fieldMode.value : 'navbar',
93
- position: fieldPos ? fieldPos.value : 'bottom-right',
94
- offsetX: fieldOffX ? (parseInt(fieldOffX.value, 10) || 32) : 32,
95
- offsetY: fieldOffY ? (parseInt(fieldOffY.value, 10) || 32) : 32,
96
- iconOnly: fieldIconOnly ? fieldIconOnly.checked : true
97
- };
98
-
99
- try {
100
- await apiRequest('/plugins/site-search/settings', {
101
- method: 'PUT',
102
- body: JSON.stringify(data)
103
- });
104
- E.toast('Settings saved.', {type: 'success'});
105
- } catch {
106
- E.toast('Failed to save settings.', {type: 'error'});
107
- }
108
- });
109
-
110
- // Initialise tabs
111
- const tabsEl = $container.find('#ss-settings-tabs').get(0);
112
- if (tabsEl) E.tabs(tabsEl);
113
-
114
- Domma.icons.scan();
115
- }
116
- };
@@ -1,15 +0,0 @@
1
- /**
2
- * Site Search Plugin - Default Configuration
3
- */
4
- export default {
5
- placeholder: 'Search pages...',
6
- keyboardShortcut: true,
7
- maxResults: 10,
8
- minQueryLength: 2,
9
- debounceMs: 300,
10
- displayMode: 'navbar',
11
- position: 'bottom-right',
12
- offsetX: 32,
13
- offsetY: 32,
14
- iconOnly: true
15
- };
@@ -1,188 +0,0 @@
1
- /**
2
- * Site Search Plugin - Server
3
- *
4
- * Endpoints (prefix: /api/plugins/site-search):
5
- * GET /search?q=term - public: full-text search across all published pages
6
- * GET /settings - public: return plugin settings (used by inject script)
7
- * PUT /settings - admin-auth: save user overrides
8
- */
9
- import {listPages} from '../../server/services/content.js';
10
- import {getPluginSettings, savePluginState} from '../../server/services/plugins.js';
11
-
12
- /**
13
- * Strip markdown syntax and shortcodes from content for plain-text searching.
14
- *
15
- * @param {string} content
16
- * @returns {string}
17
- */
18
- function stripMarkdown(content) {
19
- if (!content) return '';
20
- return content
21
- // Remove shortcodes (self-closing and wrapping)
22
- .replace(/\[[a-z][^\]]*\/\]/gi, '')
23
- .replace(/\[[a-z][^\]]*\][\s\S]*?\[\/[a-z]+\]/gi, '')
24
- .replace(/\[[a-z][^\]]*\]/gi, '')
25
- // Remove HTML tags
26
- .replace(/<[^>]+>/g, '')
27
- // Remove headings (keep text)
28
- .replace(/^#{1,6}\s+/gm, '')
29
- // Remove bold / italic
30
- .replace(/\*{1,3}([^*]+)\*{1,3}/g, '$1')
31
- .replace(/_{1,3}([^_]+)_{1,3}/g, '$1')
32
- // Remove images
33
- .replace(/!\[[^\]]*\]\([^)]*\)/g, '')
34
- // Remove links (keep text)
35
- .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
36
- // Remove code blocks
37
- .replace(/```[\s\S]*?```/g, '')
38
- .replace(/`[^`]+`/g, '')
39
- // Remove blockquotes marker
40
- .replace(/^>\s+/gm, '')
41
- // Collapse whitespace
42
- .replace(/\s+/g, ' ')
43
- .trim();
44
- }
45
-
46
- /**
47
- * Build a snippet of ~length chars around the first occurrence of term in content.
48
- *
49
- * @param {string} rawContent - raw markdown content
50
- * @param {string} term
51
- * @param {number} length
52
- * @returns {string}
53
- */
54
- function buildSnippet(rawContent, term, length = 120) {
55
- const text = stripMarkdown(rawContent);
56
- if (!text) return '';
57
-
58
- const lower = text.toLowerCase();
59
- const idx = lower.indexOf(term.toLowerCase());
60
-
61
- if (idx === -1) {
62
- return text.length > length ? text.slice(0, length) + '…' : text;
63
- }
64
-
65
- const half = Math.floor(length / 2);
66
- const start = Math.max(0, idx - half);
67
- const end = Math.min(text.length, start + length);
68
- const snippet = text.slice(start, end);
69
-
70
- return (start > 0 ? '…' : '') + snippet + (end < text.length ? '…' : '');
71
- }
72
-
73
- /**
74
- * Score a page against a search query using weighted field matching.
75
- *
76
- * @param {object} page
77
- * @param {string} query
78
- * @param {string[]} words - query split into words
79
- * @returns {number}
80
- */
81
- function scorePage(page, query, words) {
82
- let score = 0;
83
- const title = (page.title || '').toLowerCase();
84
- const description = (page.description || '').toLowerCase();
85
- const tagsRaw = Array.isArray(page.tags) ? page.tags : [];
86
- const tags = tagsRaw.map(t => t.toLowerCase());
87
- const stripped = stripMarkdown(page.content || '').toLowerCase();
88
- const q = query.toLowerCase();
89
-
90
- // Title scoring
91
- if (title === q) {
92
- score += 100;
93
- } else if (title.includes(q)) {
94
- score += 60;
95
- } else if (words.every(w => title.includes(w))) {
96
- score += 40;
97
- } else if (words.some(w => title.includes(w))) {
98
- score += 20;
99
- }
100
-
101
- // Tags scoring
102
- if (tags.includes(q)) {
103
- score += 30;
104
- } else if (tags.some(t => t.includes(q))) {
105
- score += 20;
106
- } else if (words.every(w => tags.some(t => t.includes(w)))) {
107
- score += 15;
108
- } else if (words.some(w => tags.some(t => t.includes(w)))) {
109
- score += 8;
110
- }
111
-
112
- // Description scoring
113
- if (description.includes(q)) {
114
- score += 25;
115
- } else if (words.some(w => description.includes(w))) {
116
- score += 10;
117
- }
118
-
119
- // Content scoring
120
- if (stripped.includes(q)) {
121
- score += 10;
122
- } else if (words.every(w => stripped.includes(w))) {
123
- score += 5;
124
- } else if (words.some(w => stripped.includes(w))) {
125
- score += 2;
126
- }
127
-
128
- return score;
129
- }
130
-
131
- export default async function siteSearchPlugin(fastify, options) {
132
- const {authenticate, requireAdmin} = options.auth;
133
-
134
- // -------------------------------------------------------------------------
135
- // Public search endpoint
136
- // -------------------------------------------------------------------------
137
-
138
- fastify.get('/search', async (request, reply) => {
139
- const settings = getPluginSettings('site-search');
140
- const minLen = settings.minQueryLength ?? 2;
141
- const maxResults = settings.maxResults ?? 10;
142
-
143
- const q = (request.query.q || '').trim();
144
- if (q.length < minLen) {
145
- return reply.status(400).send({error: `Query must be at least ${minLen} characters`});
146
- }
147
-
148
- const pages = await listPages();
149
- const words = q.toLowerCase().split(/\s+/).filter(Boolean);
150
-
151
- const results = [];
152
- for (const page of pages) {
153
- // Filter out drafts and private pages
154
- if (page.status === 'draft') continue;
155
- if (page.visibility === 'private') continue;
156
-
157
- const score = scorePage(page, q, words);
158
- if (score === 0) continue;
159
-
160
- results.push({
161
- title: page.title || 'Untitled',
162
- url: page.urlPath,
163
- description: page.description || '',
164
- tags: Array.isArray(page.tags) ? page.tags : [],
165
- snippet: buildSnippet(page.content || '', q),
166
- score
167
- });
168
- }
169
-
170
- results.sort((a, b) => b.score - a.score);
171
-
172
- return {results: results.slice(0, maxResults)};
173
- });
174
-
175
- // -------------------------------------------------------------------------
176
- // Settings routes
177
- // -------------------------------------------------------------------------
178
-
179
- fastify.get('/settings', async () => {
180
- return getPluginSettings('site-search');
181
- });
182
-
183
- fastify.put('/settings', {preHandler: [authenticate, requireAdmin]}, async (request) => {
184
- const body = request.body || {};
185
- savePluginState('site-search', {settings: body});
186
- return {ok: true};
187
- });
188
- }
@@ -1,40 +0,0 @@
1
- {
2
- "name": "site-search",
3
- "displayName": "Site Search",
4
- "version": "1.0.0",
5
- "description": "Full-text search for the public site. Search icon in navbar, Cmd+K shortcut, weighted results.",
6
- "author": "Darryl Waterhouse",
7
- "date": "2026-03-19",
8
- "icon": "search",
9
- "admin": {
10
- "sidebar": [
11
- {
12
- "id": "site-search",
13
- "text": "Site Search",
14
- "icon": "search",
15
- "url": "#/plugins/site-search",
16
- "section": "#/plugins/site-search",
17
- "permissions": [
18
- "settings"
19
- ]
20
- }
21
- ],
22
- "routes": [
23
- {
24
- "path": "/plugins/site-search",
25
- "view": "plugin-site-search",
26
- "title": "Site Search - Domma CMS"
27
- }
28
- ],
29
- "views": {
30
- "plugin-site-search": {
31
- "entry": "site-search/admin/views/site-search.js?v=0c7dd970",
32
- "exportName": "siteSearchView"
33
- }
34
- }
35
- },
36
- "inject": {
37
- "headLate": "public/inject-head.html",
38
- "bodyEnd": "public/inject-body.html"
39
- }
40
- }
@@ -1,17 +0,0 @@
1
- <!-- site-search: load settings then initialise search UI -->
2
- <script>
3
- (function () {
4
- fetch('/api/plugins/site-search/settings')
5
- .then(function (r) {
6
- return r.json();
7
- })
8
- .then(function (cfg) {
9
- window.__SITE_SEARCH__ = cfg;
10
- var s = document.createElement('script');
11
- s.src = '/plugins/site-search/public/search.js';
12
- document.body.appendChild(s);
13
- })
14
- .catch(function () {
15
- });
16
- }());
17
- </script>
@@ -1 +0,0 @@
1
- <link rel="stylesheet" href="/plugins/site-search/public/search.css?v=2">
@@ -1 +0,0 @@
1
- .site-search-trigger{display:inline-flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;padding:6px 8px;border-radius:var(--dm-radius, 6px);color:inherit;opacity:.75;transition:opacity .15s,background .15s;font-size:var(--dm-font-size-sm, 14px)}.site-search-trigger:hover{opacity:1;background:var(--dm-surface-hover, rgba(0, 0, 0, .06))}.site-search-trigger svg,.site-search-trigger span[data-icon]{width:18px;height:18px}.site-search-shortcut-hint{display:inline-block;font-size:11px;line-height:1;padding:2px 5px;border-radius:4px;border:1px solid var(--dm-border, rgba(0, 0, 0, .15));color:var(--dm-text-muted, #888);margin-left:4px;vertical-align:middle;font-family:var(--dm-font-mono, monospace)}.site-search-overlay{position:fixed;inset:0;z-index:10000;background:#00000073;backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);display:flex;align-items:flex-start;justify-content:center;padding-top:10vh;animation:ss-overlay-in .15s ease}@keyframes ss-overlay-in{0%{opacity:0}to{opacity:1}}.site-search-panel{width:100%;max-width:600px;margin:0 16px;border-radius:var(--dm-radius-lg, 10px);background:#1e2030;color:#e8eaf0;border:1px solid rgba(255,255,255,.1);box-shadow:0 24px 64px #0009;overflow:hidden;animation:ss-panel-in .15s ease}@keyframes ss-panel-in{0%{transform:translateY(-12px);opacity:0}to{transform:translateY(0);opacity:1}}.site-search-header{display:flex;align-items:center;padding:12px 16px;border-bottom:1px solid rgba(255,255,255,.08);gap:8px}.site-search-header span[data-icon],.site-search-header svg{width:18px;height:18px;flex-shrink:0;opacity:.45;color:#e8eaf0}.site-search-input{flex:1;border:none;outline:none;background:transparent;font-size:var(--dm-font-size-lg, 16px);color:#e8eaf0;font-family:var(--dm-font-sans, sans-serif);padding:0}.site-search-input::placeholder{color:#e8eaf066;opacity:1}.site-search-close-btn{display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid rgba(255,255,255,.15);cursor:pointer;color:#e8eaf080;border-radius:var(--dm-radius, 6px);padding:4px 8px;font-size:12px;font-family:var(--dm-font-mono, monospace);transition:color .15s,border-color .15s;flex-shrink:0}.site-search-close-btn:hover{color:#e8eaf0;border-color:#ffffff4d}.site-search-results{max-height:60vh;overflow-y:auto;padding:8px 0}.site-search-result{display:block;padding:10px 16px;text-decoration:none;color:inherit;border-left:3px solid transparent;transition:background .1s,border-color .1s;cursor:pointer}.site-search-result:hover,.site-search-result.active{background:#ffffff0f;border-left-color:var(--dm-primary, #5b8cff)}.site-search-result-title{font-weight:600;font-size:var(--dm-font-size-sm, 14px);margin-bottom:2px;color:#e8eaf0}.site-search-result-snippet{font-size:12px;color:#e8eaf08c;line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.site-search-result-snippet mark{background:#5b8cff40;color:#e8eaf0;border-radius:2px;padding:0 1px}.site-search-loading,.site-search-empty{padding:24px 16px;text-align:center;color:#e8eaf073;font-size:var(--dm-font-size-sm, 14px)}.site-search-loading-dots{display:inline-flex;gap:4px}.site-search-loading-dots span{width:6px;height:6px;border-radius:50%;background:#e8eaf066;animation:ss-dot-pulse 1.2s ease-in-out infinite}.site-search-loading-dots span:nth-child(2){animation-delay:.2s}.site-search-loading-dots span:nth-child(3){animation-delay:.4s}@keyframes ss-dot-pulse{0%,80%,to{transform:scale(.6);opacity:.4}40%{transform:scale(1);opacity:1}}.site-search-results:not(:empty){border-top:1px solid rgba(255,255,255,.06)}.site-search-footer{padding:6px 16px;border-top:1px solid rgba(255,255,255,.06);display:flex;gap:12px;font-size:11px;color:#e8eaf059}.site-search-footer kbd{display:inline-block;padding:1px 4px;border-radius:3px;border:1px solid rgba(255,255,255,.15);font-family:var(--dm-font-mono, monospace);font-size:10px}@media(max-width:640px){.site-search-overlay{padding-top:5vh;align-items:flex-start}.site-search-panel{margin:0 8px;border-radius:var(--dm-radius, 6px)}.site-search-results{max-height:70vh}.site-search-shortcut-hint{display:none}}.site-search-floating-trigger{position:fixed;z-index:9999;width:52px;height:52px;border-radius:50%;border:1px solid var(--dm-border, rgba(0, 0, 0, .12));cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;background:var(--dm-surface, #fff);color:var(--dm-text, #111);box-shadow:0 4px 16px #00000024;transition:background .15s,color .15s,border-color .15s,transform .15s,box-shadow .15s;outline:none;white-space:nowrap}.site-search-floating-trigger:hover{background:var(--dm-primary, #5b8cff);color:#fff;border-color:var(--dm-primary, #5b8cff);box-shadow:0 6px 20px #00000038;transform:scale(1.05)}.site-search-floating-trigger:active{transform:scale(.97)}.site-search-floating-trigger svg,.site-search-floating-trigger [data-icon]{width:20px;height:20px;flex-shrink:0;pointer-events:none}.site-search-floating-trigger--pill{width:auto;height:44px;border-radius:999px;padding:0 18px 0 14px}.site-search-floating-label{font-size:.875rem;font-weight:500;letter-spacing:.01em;pointer-events:none}
@@ -1 +0,0 @@
1
- (function(){"use strict";var l=window.__SITE_SEARCH__||{},N=l.placeholder||"Search pages...",g=l.keyboardShortcut!==!1,w=l.debounceMs||300,D=l.displayMode||"navbar",y=l.iconOnly!==!1,u=null,s=null,c=null,p=null,f=!1;function h(){var t=document.querySelector("#site-navbar, .navbar");if(t){var e=t.querySelector(".navbar-actions");if(!e){var n=t.querySelector(".navbar-container")||t;e=document.createElement("div"),e.className="navbar-actions",n.appendChild(e)}if(!e.querySelector(".site-search-trigger")){var r=/Mac|iPhone|iPad|iPod/.test(navigator.platform||navigator.userAgent),o=r?"\u2318K":"Ctrl+K",a=document.createElement("button");a.className="navbar-action-link site-search-trigger",a.setAttribute("aria-label","Search"),a.setAttribute("type","button");var i=document.createElement("span");if(i.setAttribute("data-icon","search"),a.appendChild(i),g){var d=document.createElement("span");d.className="site-search-shortcut-hint",d.textContent=o,a.appendChild(d)}e.insertBefore(a,e.firstChild),a.addEventListener("click",b),window.Domma&&Domma.icons&&Domma.icons.scan&&Domma.icons.scan(a)}}}function L(){if(document.querySelector("#site-navbar .navbar-actions, .navbar .navbar-actions")){h();return}var t=new MutationObserver(function(){var e=document.querySelector("#site-navbar, .navbar");e&&(t.disconnect(),h())});t.observe(document.body,{childList:!0,subtree:!0}),setTimeout(function(){t.disconnect(),h()},5e3)}function k(){var t=document.createElement("div");t.className="site-search-overlay",t.setAttribute("role","dialog"),t.setAttribute("aria-modal","true"),t.setAttribute("aria-label","Site search");var e=document.createElement("div");e.className="site-search-panel",e.setAttribute("role","search");var n=document.createElement("div");n.className="site-search-header";var r=document.createElement("span");r.setAttribute("data-icon","search"),n.appendChild(r),s=document.createElement("input"),s.className="site-search-input",s.type="search",s.setAttribute("autocomplete","off"),s.setAttribute("autocorrect","off"),s.setAttribute("spellcheck","false"),s.setAttribute("aria-label","Search"),s.placeholder=N,n.appendChild(s);var o=document.createElement("button");o.className="site-search-close-btn",o.setAttribute("type","button"),o.setAttribute("aria-label","Close search"),o.textContent="Esc",n.appendChild(o),e.appendChild(n),c=document.createElement("div"),c.className="site-search-results",c.setAttribute("role","listbox"),c.setAttribute("aria-live","polite"),e.appendChild(c);var a=document.createElement("div");return a.className="site-search-footer",a.innerHTML="<span><kbd>\u2191</kbd><kbd>\u2193</kbd> navigate</span><span><kbd>\u21B5</kbd> open</span><span><kbd>Esc</kbd> close</span>",e.appendChild(a),t.appendChild(e),t.addEventListener("click",function(i){i.target===t&&v()}),o.addEventListener("click",v),s.addEventListener("input",function(){clearTimeout(p);var i=s.value.trim();if(!i){E();return}p=setTimeout(function(){T(i)},w)}),s.addEventListener("keydown",q),window.Domma&&Domma.icons&&Domma.icons.scan&&Domma.icons.scan(n),t}function b(){f||(f=!0,u=k(),document.body.appendChild(u),document.body.style.overflow="hidden",setTimeout(function(){s&&s.focus()},30))}function v(){f&&(f=!1,clearTimeout(p),u&&u.parentNode&&u.parentNode.removeChild(u),u=null,s=null,c=null,document.body.style.overflow="")}function T(t){c&&(x(),fetch("/api/plugins/site-search/search?q="+encodeURIComponent(t)).then(function(e){return e.json()}).then(function(e){M(e.results||[],t)}).catch(function(){E();var e=document.createElement("div");e.className="site-search-empty",e.textContent="Search unavailable. Please try again.",c.appendChild(e)}))}function x(){c&&(c.innerHTML='<div class="site-search-loading"><div class="site-search-loading-dots"><span></span><span></span><span></span></div></div>')}function E(){c&&(c.innerHTML="")}function M(t,e){if(c){if(c.innerHTML="",!t.length){var n=document.createElement("div");n.className="site-search-empty",n.textContent='No results found for "'+e+'"',c.appendChild(n);return}for(var r=document.createDocumentFragment(),o=0;o<t.length;o++){var a=t[o],i=document.createElement("a");i.className="site-search-result",i.href=a.url||"/",i.setAttribute("role","option"),i.setAttribute("tabindex","-1"),i.setAttribute("data-index",String(o));var d=document.createElement("div");if(d.className="site-search-result-title",C(d,a.title||"Untitled",e),i.appendChild(d),a.snippet){var m=document.createElement("div");m.className="site-search-result-snippet",C(m,a.snippet,e),i.appendChild(m)}i.addEventListener("click",v),r.appendChild(i)}c.appendChild(r)}}function C(t,e,n){if(!n){t.textContent=e;return}var r=n.trim().split(/\s+/).filter(Boolean);if(!r.length){t.textContent=e;return}var o;try{o=new RegExp("("+r.map(function(m){return m.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}).join("|")+")","gi")}catch{t.textContent=e;return}for(var a=e.split(o),i=0;i<a.length;i++){if(o.test(a[i])){var d=document.createElement("mark");d.textContent=a[i],t.appendChild(d)}else t.appendChild(document.createTextNode(a[i]));o.lastIndex=0}}function q(t){if(t.key==="Escape"){t.preventDefault(),v();return}if(c){var e=c.querySelectorAll(".site-search-result");if(e.length){for(var n=c.querySelector(".site-search-result.active"),r=-1,o=0;o<e.length;o++)if(e[o]===n){r=o;break}if(t.key==="ArrowDown")t.preventDefault(),A(e,r<e.length-1?r+1:0);else if(t.key==="ArrowUp")t.preventDefault(),A(e,r>0?r-1:e.length-1);else if(t.key==="Enter"&&n){t.preventDefault();var a=n.getAttribute("href");v(),window.location.href=a}}}}function A(t,e){for(var n=0;n<t.length;n++)t[n].classList.toggle("active",n===e);t[e]&&t[e].scrollIntoView({block:"nearest"})}g&&document.addEventListener("keydown",function(t){if((t.metaKey||t.ctrlKey)&&t.key==="k"){var e=document.activeElement&&document.activeElement.tagName;if((e==="INPUT"||e==="TEXTAREA"||e==="SELECT")&&!f)return;t.preventDefault(),f?v():b()}});function O(t,e,n,r){var o=e==="left-center"||e==="bottom-left"||e==="top-left",a=e==="bottom-left"||e==="bottom-right",i=e==="top-left"||e==="top-right";Object.assign(t.style,{right:o?"auto":n+"px",left:o?n+"px":"auto",top:i?r+"px":a?"auto":"50vh",bottom:a?r+"px":"auto",transform:i||a?"none":"translateY(-50%)"})}function H(){if(!document.querySelector(".site-search-floating-trigger")){var t=l.position||"bottom-right",e=l.offsetX!=null?l.offsetX:32,n=l.offsetY!=null?l.offsetY:32,r=document.createElement("button");r.className="site-search-floating-trigger"+(y?"":" site-search-floating-trigger--pill"),r.setAttribute("type","button"),r.setAttribute("aria-label","Search");var o=document.createElement("span");if(o.setAttribute("data-icon","search"),r.appendChild(o),!y){var a=document.createElement("span");a.className="site-search-floating-label",a.textContent="Search",r.appendChild(a)}O(r,t,e,n),document.body.appendChild(r),r.addEventListener("click",b),window.Domma&&Domma.icons&&Domma.icons.scan&&Domma.icons.scan(r)}}function S(){D==="floating"?H():L()}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",S):S()})();
@@ -1 +0,0 @@
1
- []