domma-cms 0.55.1 → 0.69.3
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.
- package/CLAUDE.md +194 -3
- package/README.md +18 -14
- package/admin/css/admin.css +1 -1
- package/admin/css/contacts.css +1 -0
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/analytics-shape.js +1 -0
- package/admin/js/lib/contacts-arrange.js +1 -0
- package/admin/js/lib/help-popover.js +1 -0
- package/admin/js/lib/notes-arrange.js +1 -0
- package/admin/js/lib/page-picker.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -0
- package/admin/js/lib/plugin-chrome.js +1 -0
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-grouping.test.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/lib/slideover-resizable.js +1 -0
- package/admin/js/lib/todo-arrange.js +1 -0
- package/admin/js/lib/tool-kit.js +1 -0
- package/admin/js/templates/analytics.html +138 -0
- package/admin/js/templates/contacts.html +156 -0
- package/admin/js/templates/context-menu-editor.html +212 -0
- package/admin/js/templates/context-menus.html +16 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-code.html +1 -1
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
- package/admin/js/templates/settings.html +16 -97
- package/admin/js/templates/theme.html +211 -0
- package/admin/js/templates/todo.html +113 -0
- package/admin/js/views/analytics.js +62 -0
- package/admin/js/views/contacts.js +174 -0
- package/admin/js/views/context-menu-editor.js +55 -0
- package/admin/js/views/context-menus.js +5 -0
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/notes.js +104 -0
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +28 -24
- package/admin/js/views/search.js +1 -0
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +1 -0
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +6 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +27 -2
- package/config/menus/admin-sidebar.json +129 -23
- package/config/plugins.json +5 -5
- package/config/search.json +13 -0
- package/config/server.json +3 -1
- package/package.json +14 -10
- package/plugins/_lib/admin/mail/compose-window.js +914 -0
- package/plugins/_lib/admin/mail/contacts.js +301 -0
- package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
- package/plugins/_lib/admin/mail/folder-tree.js +254 -0
- package/plugins/_lib/admin/mail/identity.js +480 -0
- package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
- package/plugins/_lib/admin/mail/keyboard.js +136 -0
- package/plugins/_lib/admin/mail/mail.css +1 -0
- package/plugins/_lib/admin/mail/mail.html +71 -0
- package/plugins/_lib/admin/mail/panes.js +253 -0
- package/plugins/_lib/admin/mail/reader-view.js +4473 -0
- package/plugins/_lib/admin/mail/resizable.js +26 -0
- package/plugins/_lib/admin/mail/rules.js +343 -0
- package/plugins/_lib/admin/mail/scheduling.js +203 -0
- package/plugins/_lib/admin/mail/section-kit.js +277 -0
- package/plugins/_lib/admin/mail/templates.js +238 -0
- package/plugins/_lib/admin/mail/threads.js +200 -0
- package/plugins/_lib/admin/mail/vacation.js +269 -0
- package/plugins/_lib/admin/ui/help.css +1 -0
- package/plugins/_lib/admin/ui/help.js +174 -0
- package/plugins/_lib/admin/ui/resizable.js +151 -0
- package/plugins/_lib/dataStore.js +117 -0
- package/plugins/_lib/mail/accounts.js +919 -0
- package/plugins/_lib/mail/bodyTokens.js +101 -0
- package/plugins/_lib/mail/compose.js +256 -0
- package/plugins/_lib/mail/defaults.js +57 -0
- package/plugins/_lib/mail/diagnostics.js +154 -0
- package/plugins/_lib/mail/envelope.js +161 -0
- package/plugins/_lib/mail/folders.js +192 -0
- package/plugins/_lib/mail/handoff.js +274 -0
- package/plugins/_lib/mail/imapPool.js +293 -0
- package/plugins/_lib/mail/mbox.js +74 -0
- package/plugins/_lib/mail/pollSchedule.js +79 -0
- package/plugins/_lib/mail/poller.js +135 -0
- package/plugins/_lib/mail/priority.js +138 -0
- package/plugins/_lib/mail/readRoutes.js +680 -0
- package/plugins/_lib/mail/render.js +291 -0
- package/plugins/_lib/mail/ruleRunner.js +152 -0
- package/plugins/_lib/mail/scheduler.js +254 -0
- package/plugins/_lib/mail/secretbox.js +229 -0
- package/plugins/_lib/mail/send.js +396 -0
- package/plugins/_lib/mail/store.js +1022 -0
- package/plugins/_lib/mail/sync.js +292 -0
- package/plugins/_lib/mail/syncPlan.js +126 -0
- package/plugins/_lib/mail/syncSelection.js +82 -0
- package/plugins/_lib/mail/unsubscribe.js +183 -0
- package/plugins/_lib/mail/vacationRunner.js +114 -0
- package/plugins/_lib/mail/write.js +473 -0
- package/plugins/_lib/schemaSync.js +83 -0
- package/plugins/_template/admin/css/index.css +0 -0
- package/plugins/_template/admin/templates/index.html +4 -4
- package/plugins/_template/admin/views/index.js +7 -0
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -0
- package/plugins/blog/admin/templates/blog.html +35 -41
- package/plugins/blog/admin/templates/categories.html +19 -6
- package/plugins/blog/admin/templates/comments.html +26 -9
- package/plugins/blog/admin/templates/post-editor.html +63 -40
- package/plugins/blog/admin/templates/settings.html +3 -9
- package/plugins/blog/admin/views/blog.js +288 -159
- package/plugins/blog/admin/views/categories.js +212 -206
- package/plugins/blog/admin/views/comments.js +207 -158
- package/plugins/blog/admin/views/kit.js +372 -0
- package/plugins/blog/admin/views/post-editor.js +306 -39
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -84
- package/plugins/blog/blocks/blog-card-compact.css +17 -0
- package/plugins/blog/blocks/blog-card-compact.html +8 -0
- package/plugins/blog/blocks/blog-card-row.css +22 -0
- package/plugins/blog/blocks/blog-card-row.html +11 -0
- package/plugins/blog/blocks/blog-card.css +35 -0
- package/plugins/blog/blocks/blog-card.html +16 -0
- package/plugins/blog/blocks/blog-post-classic.css +15 -0
- package/plugins/blog/blocks/blog-post-classic.html +28 -0
- package/plugins/blog/blocks/blog-post-feature.css +20 -0
- package/plugins/blog/blocks/blog-post-feature.html +30 -0
- package/plugins/blog/blocks/blog-post-minimal.css +10 -0
- package/plugins/blog/blocks/blog-post-minimal.html +23 -0
- package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
- package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
- package/plugins/blog/collections/categories/schema.json +7 -6
- package/plugins/blog/collections/comments/schema.json +11 -10
- package/plugins/blog/collections/posts/schema.json +16 -13
- package/plugins/blog/config.js +6 -0
- package/plugins/blog/lib/layouts.js +304 -0
- package/plugins/blog/lib/page.js +158 -0
- package/plugins/blog/lib/render.js +119 -0
- package/plugins/blog/lib/samples.js +234 -0
- package/plugins/blog/plugin.js +259 -26
- package/plugins/blog/plugin.json +21 -73
- package/plugins/blog/plugin.public.js +217 -0
- package/plugins/blog/public/blog.css +177 -0
- package/plugins/blog/public/blog.js +586 -0
- package/plugins/blog/public/samples/aurora.svg +7 -0
- package/plugins/blog/public/samples/dusk.svg +7 -0
- package/plugins/blog/public/samples/ember.svg +7 -0
- package/plugins/blog/public/samples/harbour.svg +7 -0
- package/plugins/blog/public/samples/meadow.svg +7 -0
- package/plugins/blog/public/samples/rosewood.svg +7 -0
- package/plugins/blog/templates/index.html +9 -8
- package/plugins/blog/templates/post.html +4 -17
- package/plugins/blog/tests/layouts.test.js +97 -0
- package/plugins/blog/tests/public.test.js +71 -0
- package/plugins/free-tier.lock.json +74 -0
- package/plugins/mail-reader/CLAUDE.md +78 -0
- package/plugins/mail-reader/admin/views/mail.js +19 -0
- package/plugins/mail-reader/config.js +7 -0
- package/plugins/mail-reader/plugin.js +48 -0
- package/plugins/mail-reader/plugin.json +34 -0
- package/public/css/forms.css +1 -1
- package/public/css/search.css +1 -0
- package/public/css/site.css +1 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/collection-context.js +2 -2
- package/public/js/context-menus.js +1 -0
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -2
- package/public/js/search.js +1 -0
- package/public/js/site.js +1 -1
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +77 -4
- package/scripts/copy-domma.js +48 -0
- package/scripts/run-tests.mjs +92 -0
- package/scripts/seed.js +1996 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/server/routes/api/collections.js +34 -0
- package/server/routes/api/contacts.js +506 -0
- package/server/routes/api/context-menus.js +104 -0
- package/server/routes/api/dashboard.js +8 -2
- package/server/routes/api/forms.js +42 -3
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/notifications.js +69 -19
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +198 -9
- package/server/routes/api/search.js +43 -0
- package/server/routes/api/theme.js +69 -0
- package/server/routes/api/todo.js +178 -0
- package/server/routes/public.js +42 -7
- package/server/server.js +135 -1
- package/server/services/adapters/FileAdapter.js +6 -1
- package/server/services/collections.js +13 -3
- package/server/services/content.js +26 -0
- package/server/services/contextMenus.js +477 -0
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +124 -10
- package/server/services/permissionRegistry.js +74 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginFiles.js +52 -11
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +340 -7
- package/server/services/presetCollections.js +344 -0
- package/server/services/promoted-plugins-migration.js +254 -0
- package/server/services/publicCors.js +46 -0
- package/server/services/renderer.js +224 -22
- package/server/services/roles.js +1 -1
- package/server/services/search-migration.js +82 -0
- package/server/services/search.js +413 -0
- package/server/services/sidebar-migration.js +1 -0
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +307 -0
- package/server/services/themeSettings.js +580 -0
- package/server/services/users.js +8 -0
- package/server/templates/page.html +4 -2
- package/plugins/analytics/admin/templates/analytics.html +0 -61
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -57
- package/plugins/analytics/public/inject-body.html +0 -60
- package/plugins/analytics/public/inject-head.html +0 -1
- package/plugins/blog/templates/author.html +0 -9
- package/plugins/blog/templates/category.html +0 -9
- package/plugins/blog/templates/tag.html +0 -9
- package/plugins/contacts/admin/templates/contacts.html +0 -126
- package/plugins/contacts/admin/views/contacts.js +0 -710
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -71
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/data/contacts.json +0 -20
- package/plugins/contacts/plugin.js +0 -291
- package/plugins/contacts/plugin.json +0 -33
- package/plugins/demo-viewer/config.js +0 -4
- package/plugins/demo-viewer/plugin.js +0 -36
- package/plugins/demo-viewer/plugin.json +0 -9
- package/plugins/invoice/admin/templates/editor.html +0 -129
- package/plugins/invoice/admin/templates/index.html +0 -43
- package/plugins/invoice/admin/templates/issuers.html +0 -5
- package/plugins/invoice/admin/templates/receivers.html +0 -5
- package/plugins/invoice/admin/views/editor.js +0 -267
- package/plugins/invoice/admin/views/index.js +0 -155
- package/plugins/invoice/admin/views/issuers.js +0 -23
- package/plugins/invoice/admin/views/party-view.js +0 -148
- package/plugins/invoice/admin/views/receivers.js +0 -22
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -16
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -15
- package/plugins/invoice/collections/invoices/schema.json +0 -27
- package/plugins/invoice/config.js +0 -16
- package/plugins/invoice/plugin.js +0 -283
- package/plugins/invoice/plugin.json +0 -85
- package/plugins/invoice/templates/invoice-print.html +0 -213
- package/plugins/notes/admin/templates/notes.html +0 -83
- package/plugins/notes/admin/views/notes.js +0 -281
- package/plugins/notes/collections/user-notes/schema.json +0 -54
- package/plugins/notes/config.js +0 -6
- package/plugins/notes/data/notes.json +0 -1
- package/plugins/notes/plugin.json +0 -33
- package/plugins/site-search/admin/views/site-search.js +0 -116
- package/plugins/site-search/config.js +0 -15
- package/plugins/site-search/plugin.js +0 -188
- package/plugins/site-search/plugin.json +0 -40
- package/plugins/site-search/public/inject-body.html +0 -17
- package/plugins/site-search/public/inject-head.html +0 -1
- package/plugins/site-search/public/search.css +0 -1
- package/plugins/site-search/public/search.js +0 -1
- package/plugins/surveys/admin/templates/audience.html +0 -47
- package/plugins/surveys/admin/templates/results.html +0 -58
- package/plugins/surveys/admin/templates/survey-editor.html +0 -141
- package/plugins/surveys/admin/templates/surveys.html +0 -25
- package/plugins/surveys/admin/views/audience.js +0 -301
- package/plugins/surveys/admin/views/results.js +0 -172
- package/plugins/surveys/admin/views/survey-editor.js +0 -211
- package/plugins/surveys/admin/views/surveys.js +0 -161
- package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
- package/plugins/surveys/collections/survey-groups/schema.json +0 -11
- package/plugins/surveys/collections/survey-invites/schema.json +0 -16
- package/plugins/surveys/collections/surveys/schema.json +0 -23
- package/plugins/surveys/config.js +0 -8
- package/plugins/surveys/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- package/plugins/surveys/public/dist-shared.mjs +0 -1
- package/plugins/surveys/public/survey.css +0 -1
- package/plugins/surveys/public/survey.mjs +0 -1
- package/plugins/surveys/templates/survey-page.html +0 -63
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
- package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
- package/plugins/theme-switcher/config.js +0 -10
- package/plugins/theme-switcher/plugin.js +0 -26
- package/plugins/theme-switcher/plugin.json +0 -74
- package/plugins/theme-switcher/public/inject-body.html +0 -153
- package/plugins/theme-switcher/public/inject-head.html +0 -260
- package/plugins/todo/admin/templates/todo.html +0 -158
- package/plugins/todo/admin/views/todo.js +0 -343
- package/plugins/todo/collections/todos/schema.json +0 -60
- package/plugins/todo/config.js +0 -7
- package/plugins/todo/data/todos.json +0 -1
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
|
@@ -11,6 +11,9 @@ import {applyTransforms} from './hooks.js';
|
|
|
11
11
|
import {resolveLocation, resolveMenuDecorations, resolveOverlaysForPage} from './menus.js';
|
|
12
12
|
import {buildMenuNav} from './menuRender.js';
|
|
13
13
|
import {getProjectForPage} from './projects.js';
|
|
14
|
+
import {resolveContextMenusForPage} from './contextMenus.js';
|
|
15
|
+
import {buildOverrideStyleTag, getThemeTokenMap, listThemeIds, loadThemeConfig} from './themeSettings.js';
|
|
16
|
+
import {getSearchSettings} from './search.js';
|
|
14
17
|
|
|
15
18
|
const VALID_LAYOUT_WIDTHS = new Set(['narrow', 'normal', 'wide', 'full']);
|
|
16
19
|
const CUSTOM_CSS_PATH = new URL('../../content/custom.css', import.meta.url).pathname;
|
|
@@ -34,6 +37,117 @@ async function getTemplate() {
|
|
|
34
37
|
}
|
|
35
38
|
let _templateCache = null;
|
|
36
39
|
|
|
40
|
+
/*
|
|
41
|
+
* Asset version for the public search UI. Bumped by hand when search.js or
|
|
42
|
+
* search.css changes - the updater replaces public/ wholesale, and a browser
|
|
43
|
+
* holding the previous file otherwise keeps it.
|
|
44
|
+
*/
|
|
45
|
+
const SEARCH_ASSET_V = '20260920-search';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The public search UI: stylesheet, settings, script.
|
|
49
|
+
*
|
|
50
|
+
* Settings travel INSIDE the page rather than being fetched, so the trigger is
|
|
51
|
+
* there on first paint instead of a round trip later. Every key is public by
|
|
52
|
+
* design (see SEARCH_DEFAULTS); nothing sensitive belongs in that object.
|
|
53
|
+
*
|
|
54
|
+
* Both render paths need this, and wiring one and not the other is the standing
|
|
55
|
+
* trap in this file - the same one buildThemeView() carries.
|
|
56
|
+
*
|
|
57
|
+
* @returns {{headTag: string, bodyTag: string}} Empty strings when search is off
|
|
58
|
+
*/
|
|
59
|
+
function buildSearchAssets() {
|
|
60
|
+
let settings;
|
|
61
|
+
try {
|
|
62
|
+
settings = getSearchSettings();
|
|
63
|
+
} catch {
|
|
64
|
+
// A malformed config/search.json is not worth a blank page.
|
|
65
|
+
return {headTag: '', bodyTag: ''};
|
|
66
|
+
}
|
|
67
|
+
if (settings.enabled === false) return {headTag: '', bodyTag: ''};
|
|
68
|
+
|
|
69
|
+
const json = JSON.stringify(settings).replace(/<\/script>/gi, '<\\/script>');
|
|
70
|
+
return {
|
|
71
|
+
headTag: `<link rel="stylesheet" href="/public/css/search.css?v=${SEARCH_ASSET_V}">`,
|
|
72
|
+
bodyTag: `<script>window.__CMS_SEARCH__ = ${json};</script>\n`
|
|
73
|
+
+ `<script src="/public/js/search.js?v=${SEARCH_ASSET_V}"></script>`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const THEME_SWITCHER_ASSET_V = '20260922-ts';
|
|
78
|
+
const ANALYTICS_ASSET_V = '20260922-analytics';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The page-view beacon.
|
|
82
|
+
*
|
|
83
|
+
* Always emitted: analytics was a CORE_PLUGINS member, meaning it could not be
|
|
84
|
+
* switched off, and promotion should not quietly change that. What it counts is
|
|
85
|
+
* decided in the browser - Do-Not-Track, per-session dedup and preview renders
|
|
86
|
+
* all opt out there, where the facts are.
|
|
87
|
+
*
|
|
88
|
+
* Both render paths, the standing trap in this file.
|
|
89
|
+
*
|
|
90
|
+
* @returns {string} the body tag
|
|
91
|
+
*/
|
|
92
|
+
function buildAnalyticsTag() {
|
|
93
|
+
return `<script src="/public/js/analytics.js?v=${ANALYTICS_ASSET_V}"></script>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The public theme switcher: stylesheet, theme list, script.
|
|
98
|
+
*
|
|
99
|
+
* The theme LIST is built here from listThemeIds() rather than being written
|
|
100
|
+
* out anywhere. The plugin this replaced shipped 26 hand-written dots against a
|
|
101
|
+
* stylesheet carrying 32 themes, and nothing made the gap visible - deriving it
|
|
102
|
+
* means a theme added to domma-themes.css simply appears.
|
|
103
|
+
*
|
|
104
|
+
* Each dot's colours are that theme's own --dm-primary and --dm-secondary, for
|
|
105
|
+
* the same reason: no second list of colours to drift.
|
|
106
|
+
*
|
|
107
|
+
* Both render paths need this, the same standing trap buildThemeView() and
|
|
108
|
+
* buildSearchAssets() carry.
|
|
109
|
+
*
|
|
110
|
+
* @returns {{headTag: string, bodyTag: string}} Empty strings when it is off
|
|
111
|
+
*/
|
|
112
|
+
function buildThemeSwitcherAssets(themeCfg) {
|
|
113
|
+
const sw = themeCfg?.switcher;
|
|
114
|
+
if (!sw || sw.enabled !== true) return {headTag: '', bodyTag: ''};
|
|
115
|
+
|
|
116
|
+
let ids;
|
|
117
|
+
try {
|
|
118
|
+
ids = listThemeIds();
|
|
119
|
+
} catch {
|
|
120
|
+
return {headTag: '', bodyTag: ''};
|
|
121
|
+
}
|
|
122
|
+
const wanted = Array.isArray(sw.themes) && sw.themes.length
|
|
123
|
+
? ids.filter(id => sw.themes.includes(id))
|
|
124
|
+
: ids;
|
|
125
|
+
if (!wanted.length) return {headTag: '', bodyTag: ''};
|
|
126
|
+
|
|
127
|
+
const themes = wanted.map(id => {
|
|
128
|
+
const tokens = getThemeTokenMap(id) || {};
|
|
129
|
+
return {
|
|
130
|
+
id,
|
|
131
|
+
label: id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
132
|
+
from: tokens['--dm-primary'] || tokens['--dm-surface'] || '#888',
|
|
133
|
+
to: tokens['--dm-secondary'] || tokens['--dm-background'] || '#888'
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const payload = {
|
|
138
|
+
position: sw.position || 'right-center',
|
|
139
|
+
offsetX: sw.offsetX ?? 32,
|
|
140
|
+
offsetY: sw.offsetY ?? 32,
|
|
141
|
+
themes
|
|
142
|
+
};
|
|
143
|
+
const json = JSON.stringify(payload).replace(/<\/script>/gi, '<\\/script>');
|
|
144
|
+
return {
|
|
145
|
+
headTag: `<link rel="stylesheet" href="/public/css/theme-switcher.css?v=${THEME_SWITCHER_ASSET_V}">`,
|
|
146
|
+
bodyTag: `<script>window.__CMS_THEME_SWITCHER__ = ${json};</script>\n`
|
|
147
|
+
+ `<script src="/public/js/theme-switcher.js?v=${THEME_SWITCHER_ASSET_V}"></script>`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
37
151
|
/**
|
|
38
152
|
* Strip hidden items from the navigation config before public injection.
|
|
39
153
|
* Returns a shallow clone - never mutates the cached config object.
|
|
@@ -191,10 +305,10 @@ export async function renderPage(page, opts = {}) {
|
|
|
191
305
|
|
|
192
306
|
const breadcrumbsHtml = buildBreadcrumbsHtml(page, site);
|
|
193
307
|
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
308
|
+
const themeView = await buildThemeView(page);
|
|
309
|
+
const searchAssets = buildSearchAssets();
|
|
310
|
+
const switcherAssets = buildThemeSwitcherAssets(themeView.cfg);
|
|
311
|
+
const {fontLink, fontStyleTag, autoTheme, activeTheme, dommaTheme, customThemeClass} = themeView;
|
|
198
312
|
|
|
199
313
|
const {navbarFontLink, navbarStyleTag} = buildNavbarStyleTag(navigation);
|
|
200
314
|
|
|
@@ -202,16 +316,6 @@ export async function renderPage(page, opts = {}) {
|
|
|
202
316
|
? `<style>${customCss.replace(/<\/style>/gi, '<\\/style>')}</style>`
|
|
203
317
|
: '';
|
|
204
318
|
|
|
205
|
-
const autoTheme = (!page.theme && site.autoTheme?.enabled) ? site.autoTheme : null;
|
|
206
|
-
const activeTheme = page.theme
|
|
207
|
-
|| (autoTheme ? autoTheme.dayTheme : site.theme)
|
|
208
|
-
|| 'charcoal-dark';
|
|
209
|
-
// When a custom theme is active, Domma's theme engine must receive the base
|
|
210
|
-
// built-in theme (which it recognises). The custom override class is added
|
|
211
|
-
// separately after Domma's init so _applyTheme() cannot strip it.
|
|
212
|
-
const dommaTheme = site.baseTheme || activeTheme;
|
|
213
|
-
const customThemeClass = site.baseTheme ? `dm-theme-${activeTheme}` : '';
|
|
214
|
-
|
|
215
319
|
// Compose window.__CMS_FOOTER__ from the menus mapped to footer-primary / footer-legal.
|
|
216
320
|
const footer = {
|
|
217
321
|
primary: footerPrimaryDecorated,
|
|
@@ -229,6 +333,26 @@ export async function renderPage(page, opts = {}) {
|
|
|
229
333
|
// counts for something nobody has published.
|
|
230
334
|
const noTrack = !!opts.preview || opts.noTrack === true;
|
|
231
335
|
|
|
336
|
+
/*
|
|
337
|
+
* Authored context menus. Resolved server-side so a visitor only receives
|
|
338
|
+
* the menus that apply to this page - a menu bound to /shop/* is not in the
|
|
339
|
+
* source of /about. Domma does the rest in the browser: which menu answers
|
|
340
|
+
* a given right-click is decided by walking outward from the click, not here.
|
|
341
|
+
*/
|
|
342
|
+
let ctxMenus = [];
|
|
343
|
+
try {
|
|
344
|
+
ctxMenus = await resolveContextMenusForPage(menuCtx);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
// A malformed menu file must not take the page down with it.
|
|
347
|
+
console.warn(`[context-menus] Resolution skipped: ${err.message}`);
|
|
348
|
+
}
|
|
349
|
+
const ctxMenusScript = ctxMenus.length
|
|
350
|
+
? `window.__CMS_CTXMENUS__ = ${JSON.stringify(ctxMenus).replace(/<\/script>/gi, '<\\/script>')};`
|
|
351
|
+
: '';
|
|
352
|
+
const ctxMenusModule = ctxMenus.length
|
|
353
|
+
? '<script src="/public/js/context-menus.js?v=20260918-ctx" type="module"></script>'
|
|
354
|
+
: '';
|
|
355
|
+
|
|
232
356
|
const vars = {
|
|
233
357
|
previewFlagScript: noTrack ? '<script>window.__CMS_PREVIEW__ = true;</script>' : '',
|
|
234
358
|
seoTitle,
|
|
@@ -252,6 +376,8 @@ export async function renderPage(page, opts = {}) {
|
|
|
252
376
|
showSidebar: preset.sidebar === true || page.sidebar === true,
|
|
253
377
|
navJson: JSON.stringify(filterHiddenNavItems(navigation)).replace(/<\/script>/gi, '<\\/script>'),
|
|
254
378
|
footerScript: footerJson ? `window.__CMS_FOOTER__ = ${footerJson};` : '',
|
|
379
|
+
ctxMenusScript: ctxMenusScript,
|
|
380
|
+
ctxMenusModule: ctxMenusModule,
|
|
255
381
|
siteJson: JSON.stringify(Object.assign(
|
|
256
382
|
{
|
|
257
383
|
title: site.title,
|
|
@@ -268,7 +394,11 @@ export async function renderPage(page, opts = {}) {
|
|
|
268
394
|
autoTheme ? {autoTheme} : {}
|
|
269
395
|
)).replace(/<\/script>/gi, '<\\/script>'),
|
|
270
396
|
headInject: [injection.head, navbarFontLink].filter(Boolean).join('\n'),
|
|
271
|
-
|
|
397
|
+
// Theme overrides land BEFORE content/custom.css: the overrides are
|
|
398
|
+
// generated, custom.css is hand-written, and the hand-written one stays
|
|
399
|
+
// the last word.
|
|
400
|
+
headInjectLate: [injection.headLate, searchAssets.headTag, switcherAssets.headTag, themeView.overrideStyleTag, customCssTag, navbarStyleTag]
|
|
401
|
+
.filter(Boolean).join('\n'),
|
|
272
402
|
bodyEndInject: [
|
|
273
403
|
// Draft banner - first, so it is the topmost fixed element and a
|
|
274
404
|
// floating overlay menu cannot bury the only affordance telling
|
|
@@ -279,6 +409,9 @@ export async function renderPage(page, opts = {}) {
|
|
|
279
409
|
// content flow.
|
|
280
410
|
overlayMenus,
|
|
281
411
|
injection.bodyEnd,
|
|
412
|
+
searchAssets.bodyTag,
|
|
413
|
+
switcherAssets.bodyTag,
|
|
414
|
+
buildAnalyticsTag(),
|
|
282
415
|
site.backToTop?.enabled ? '<script src="/public/js/btt.js"></script>' : '',
|
|
283
416
|
site.cookieConsent?.enabled ? '<script src="/public/js/cookie-consent.js"></script>' : '',
|
|
284
417
|
(page.usedComponents || [])
|
|
@@ -641,6 +774,46 @@ function fontFamilyValue(family) {
|
|
|
641
774
|
return `'${safe}', ${generic}`;
|
|
642
775
|
}
|
|
643
776
|
|
|
777
|
+
/**
|
|
778
|
+
* The theme facts one render needs.
|
|
779
|
+
*
|
|
780
|
+
* config/theme.json is authoritative. The matching site.json keys still exist
|
|
781
|
+
* as a one-way mirror for plugins and the fleet manager, so reading them here
|
|
782
|
+
* would make the mirror load-bearing and give the CMS two sources of truth.
|
|
783
|
+
*
|
|
784
|
+
* @param {object} page - Page record; `page.theme` still wins over everything.
|
|
785
|
+
* @returns {Promise<object>} Template vars plus the compiled override stylesheet.
|
|
786
|
+
*/
|
|
787
|
+
async function buildThemeView(page = {}) {
|
|
788
|
+
const cfg = await loadThemeConfig();
|
|
789
|
+
|
|
790
|
+
// A page pinned to a theme opts out of the automatic pair - otherwise the
|
|
791
|
+
// pin would silently expire at the day/night boundary.
|
|
792
|
+
const autoTheme = (!page.theme && cfg.autoTheme?.enabled) ? cfg.autoTheme : null;
|
|
793
|
+
const activeTheme = page.theme
|
|
794
|
+
|| (autoTheme ? autoTheme.dayTheme : cfg.theme)
|
|
795
|
+
|| 'charcoal-dark';
|
|
796
|
+
|
|
797
|
+
// A custom (Theme Roller) theme is not a theme Domma's engine knows, so it
|
|
798
|
+
// receives the built-in base and the custom class rides along separately,
|
|
799
|
+
// added after init so _applyTheme() cannot strip it.
|
|
800
|
+
const dommaTheme = cfg.baseTheme || activeTheme;
|
|
801
|
+
const customThemeClass = cfg.baseTheme ? `dm-theme-${activeTheme}` : '';
|
|
802
|
+
|
|
803
|
+
const {fontLink, fontOverride} = buildFontVars(cfg.font?.family, cfg.font?.size);
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
cfg,
|
|
807
|
+
autoTheme,
|
|
808
|
+
activeTheme,
|
|
809
|
+
dommaTheme,
|
|
810
|
+
customThemeClass,
|
|
811
|
+
fontLink,
|
|
812
|
+
fontStyleTag: fontOverride ? `<style>${fontOverride}</style>` : '',
|
|
813
|
+
overrideStyleTag: buildOverrideStyleTag(cfg)
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
644
817
|
function buildFontVars(fontFamily, fontSize) {
|
|
645
818
|
const PRECONNECT = '<link rel="preconnect" href="https://fonts.googleapis.com">\n <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>';
|
|
646
819
|
const family = fontFamily || 'Roboto';
|
|
@@ -753,18 +926,18 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
|
|
|
753
926
|
};
|
|
754
927
|
const seoTags = buildSeoTags({page: syntheticPage, site, baseUrl, seoTitle, seoDescription, ogImage});
|
|
755
928
|
|
|
756
|
-
|
|
757
|
-
|
|
929
|
+
// Both render paths resolve the theme the same way. Wiring one and not the
|
|
930
|
+
// other is the standing trap in this file.
|
|
931
|
+
const themeView = await buildThemeView({});
|
|
932
|
+
const searchAssets = buildSearchAssets();
|
|
933
|
+
const switcherAssets = buildThemeSwitcherAssets(themeView.cfg);
|
|
934
|
+
const {fontLink, fontStyleTag, activeTheme, dommaTheme, customThemeClass} = themeView;
|
|
758
935
|
const {navbarFontLink, navbarStyleTag} = buildNavbarStyleTag(navigation);
|
|
759
936
|
|
|
760
937
|
const customCssTag = customCss.trim()
|
|
761
938
|
? `<style>${customCss.replace(/<\/style>/gi, '<\\/style>')}</style>`
|
|
762
939
|
: '';
|
|
763
940
|
|
|
764
|
-
const activeTheme = site.theme || 'charcoal-dark';
|
|
765
|
-
const dommaTheme = site.baseTheme || activeTheme;
|
|
766
|
-
const customThemeClass = site.baseTheme ? `dm-theme-${activeTheme}` : '';
|
|
767
|
-
|
|
768
941
|
// Compose window.__CMS_FOOTER__ from the menus mapped to footer-primary / footer-legal.
|
|
769
942
|
const footer = {
|
|
770
943
|
primary: footerPrimaryDecorated,
|
|
@@ -776,6 +949,26 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
|
|
|
776
949
|
};
|
|
777
950
|
const footerJson = JSON.stringify(footer).replace(/<\/script>/gi, '<\\/script>');
|
|
778
951
|
|
|
952
|
+
/*
|
|
953
|
+
* Authored context menus. Resolved server-side so a visitor only receives
|
|
954
|
+
* the menus that apply to this page - a menu bound to /shop/* is not in the
|
|
955
|
+
* source of /about. Domma does the rest in the browser: which menu answers
|
|
956
|
+
* a given right-click is decided by walking outward from the click, not here.
|
|
957
|
+
*/
|
|
958
|
+
let ctxMenus = [];
|
|
959
|
+
try {
|
|
960
|
+
ctxMenus = await resolveContextMenusForPage({urlPath: seoMeta.urlPath || '/'});
|
|
961
|
+
} catch (err) {
|
|
962
|
+
// A malformed menu file must not take the page down with it.
|
|
963
|
+
console.warn(`[context-menus] Resolution skipped: ${err.message}`);
|
|
964
|
+
}
|
|
965
|
+
const ctxMenusScript = ctxMenus.length
|
|
966
|
+
? `window.__CMS_CTXMENUS__ = ${JSON.stringify(ctxMenus).replace(/<\/script>/gi, '<\\/script>')};`
|
|
967
|
+
: '';
|
|
968
|
+
const ctxMenusModule = ctxMenus.length
|
|
969
|
+
? '<script src="/public/js/context-menus.js?v=20260918-ctx" type="module"></script>'
|
|
970
|
+
: '';
|
|
971
|
+
|
|
779
972
|
const vars = {
|
|
780
973
|
seoTitle,
|
|
781
974
|
seoDescription,
|
|
@@ -799,6 +992,8 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
|
|
|
799
992
|
showSidebar: false,
|
|
800
993
|
navJson: JSON.stringify(filterHiddenNavItems(navigation)).replace(/<\/script>/gi, '<\\/script>'),
|
|
801
994
|
footerScript: footerJson ? `window.__CMS_FOOTER__ = ${footerJson};` : '',
|
|
995
|
+
ctxMenusScript: ctxMenusScript,
|
|
996
|
+
ctxMenusModule: ctxMenusModule,
|
|
802
997
|
siteJson: JSON.stringify({
|
|
803
998
|
title: site.title,
|
|
804
999
|
footer: site.footer
|
|
@@ -810,10 +1005,17 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
|
|
|
810
1005
|
social: site.social || null
|
|
811
1006
|
}).replace(/<\/script>/gi, '<\\/script>'),
|
|
812
1007
|
headInject: [injection.head, navbarFontLink].filter(Boolean).join('\n'),
|
|
813
|
-
|
|
1008
|
+
// Theme overrides land BEFORE content/custom.css: the overrides are
|
|
1009
|
+
// generated, custom.css is hand-written, and the hand-written one stays
|
|
1010
|
+
// the last word.
|
|
1011
|
+
headInjectLate: [injection.headLate, searchAssets.headTag, switcherAssets.headTag, themeView.overrideStyleTag, customCssTag, navbarStyleTag]
|
|
1012
|
+
.filter(Boolean).join('\n'),
|
|
814
1013
|
bodyEndInject: [
|
|
815
1014
|
overlayMenus,
|
|
816
1015
|
injection.bodyEnd,
|
|
1016
|
+
searchAssets.bodyTag,
|
|
1017
|
+
switcherAssets.bodyTag,
|
|
1018
|
+
buildAnalyticsTag(),
|
|
817
1019
|
(seoMeta.usedComponents || [])
|
|
818
1020
|
.map(n => `<script type="module" src="/api/components/${n}.js"></script>`)
|
|
819
1021
|
.join('\n')
|
package/server/services/roles.js
CHANGED
|
@@ -74,7 +74,7 @@ const SEED_ENTRIES = [
|
|
|
74
74
|
'pages', 'media', 'blocks', 'components', 'navigation', 'menus',
|
|
75
75
|
'projects', 'layouts', 'collections', 'views', 'actions',
|
|
76
76
|
'users', 'settings', 'notifications', 'plugins',
|
|
77
|
-
'api-tokens', 'api-endpoints'
|
|
77
|
+
'api-tokens', 'api-endpoints', 'context-menus', 'theme'
|
|
78
78
|
],
|
|
79
79
|
badgeClass: 'badge-warning'
|
|
80
80
|
},
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search migration - one-shot lift of the retired `site-search` plugin.
|
|
3
|
+
*
|
|
4
|
+
* Search used to be a bundled plugin that the loader force-enabled as a core
|
|
5
|
+
* feature. It is now part of the CMS, which leaves three traces on an existing
|
|
6
|
+
* install:
|
|
7
|
+
*
|
|
8
|
+
* 1. its settings, stored inside config/plugins.json,
|
|
9
|
+
* 2. its plugins.json entry, which would otherwise offer a toggle for a
|
|
10
|
+
* feature that is no longer a plugin,
|
|
11
|
+
* 3. plugins/site-search/ itself - the updater replaces the plugins it ships
|
|
12
|
+
* and adds new ones, but never removes one that has gone away, so the old
|
|
13
|
+
* directory sits there and a click in the admin would run a SECOND copy of
|
|
14
|
+
* the search UI on every public page.
|
|
15
|
+
*
|
|
16
|
+
* Idempotent: with no plugins.json entry and no plugin directory there is
|
|
17
|
+
* nothing to do, which is the state of every fresh install.
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import {normaliseSearchSettings} from './search.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run the migration.
|
|
25
|
+
*
|
|
26
|
+
* Paths are resolved per call rather than at import: the same process can run
|
|
27
|
+
* this against more than one install (the tests do), and a path frozen at load
|
|
28
|
+
* time would point at the first one forever.
|
|
29
|
+
*
|
|
30
|
+
* @returns {string[]} What it changed, for the startup log (empty = nothing to do)
|
|
31
|
+
*/
|
|
32
|
+
export function migrateSiteSearchPlugin() {
|
|
33
|
+
const CONFIG_DIR = path.resolve('config');
|
|
34
|
+
const SEARCH_CONFIG = path.join(CONFIG_DIR, 'search.json');
|
|
35
|
+
const PLUGINS_CONFIG = path.join(CONFIG_DIR, 'plugins.json');
|
|
36
|
+
const RETIRED_PLUGIN_DIR = path.resolve('plugins', 'site-search');
|
|
37
|
+
|
|
38
|
+
const changes = [];
|
|
39
|
+
let plugins = null;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
plugins = JSON.parse(fs.readFileSync(PLUGINS_CONFIG, 'utf8'));
|
|
43
|
+
} catch {
|
|
44
|
+
// No plugins.json, or a malformed one. Neither is this migration's
|
|
45
|
+
// business - a broken file is reported loudly elsewhere.
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stored = plugins?.['site-search'];
|
|
49
|
+
|
|
50
|
+
// 1. Settings. Only ever written when the file is absent: a site that has
|
|
51
|
+
// already saved search settings has the newer answer.
|
|
52
|
+
if (!fs.existsSync(SEARCH_CONFIG)) {
|
|
53
|
+
const settings = normaliseSearchSettings(stored?.settings || {});
|
|
54
|
+
fs.writeFileSync(SEARCH_CONFIG, JSON.stringify(settings, null, 2) + '\n');
|
|
55
|
+
changes.push(stored ? 'lifted settings out of plugins.json' : 'wrote default settings');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. The plugins.json entry.
|
|
59
|
+
if (plugins && stored) {
|
|
60
|
+
delete plugins['site-search'];
|
|
61
|
+
fs.writeFileSync(PLUGINS_CONFIG, JSON.stringify(plugins, null, 2) + '\n');
|
|
62
|
+
changes.push('removed the plugins.json entry');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 3. The plugin directory - but only if it is the bundled one. A directory
|
|
66
|
+
// of that name carrying anything else is somebody's own work, and this
|
|
67
|
+
// is no place to decide it should go.
|
|
68
|
+
if (fs.existsSync(RETIRED_PLUGIN_DIR)) {
|
|
69
|
+
let isBundled = false;
|
|
70
|
+
try {
|
|
71
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(RETIRED_PLUGIN_DIR, 'plugin.json'), 'utf8'));
|
|
72
|
+
isBundled = manifest.name === 'site-search' && manifest.author === 'Darryl Waterhouse';
|
|
73
|
+
} catch { /* no manifest - leave it alone */ }
|
|
74
|
+
|
|
75
|
+
if (isBundled) {
|
|
76
|
+
fs.rmSync(RETIRED_PLUGIN_DIR, {recursive: true, force: true});
|
|
77
|
+
changes.push('removed plugins/site-search/');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return changes;
|
|
82
|
+
}
|