@koehler8/cms 1.0.0-beta.7 → 1.0.0-beta.9
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/package.json +5 -1
- package/src/components/Home.vue +19 -2
- package/src/main.js +21 -9
- package/src/themes/buildCssVarMap.js +246 -0
- package/src/themes/themeLoader.js +33 -1
- package/src/themes/themeManager.js +4 -227
- package/vite-plugin.js +66 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koehler8/cms",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.9",
|
|
4
4
|
"description": "Vertex CMS — lightweight, config-driven Vue 3 framework with theming, extensions, and SSG support",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -80,5 +80,9 @@
|
|
|
80
80
|
"@vue/test-utils": "^2.4.6",
|
|
81
81
|
"happy-dom": "^20.8.9",
|
|
82
82
|
"vitest": "^4.1.4"
|
|
83
|
+
},
|
|
84
|
+
"engines": {
|
|
85
|
+
"node": ">=20.19.0",
|
|
86
|
+
"npm": "^10.8.0"
|
|
83
87
|
}
|
|
84
88
|
}
|
package/src/components/Home.vue
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
</template>
|
|
36
36
|
|
|
37
37
|
<script setup>
|
|
38
|
-
import { computed, nextTick, provide, watch } from 'vue';
|
|
38
|
+
import { computed, nextTick, onMounted, provide, ref, watch } from 'vue';
|
|
39
39
|
|
|
40
40
|
import ComingSoonModal from '../components/ComingSoonModal.vue';
|
|
41
41
|
import IntroGate from '../components/IntroGate.vue';
|
|
@@ -106,7 +106,24 @@ provide('pageContent', pageContent);
|
|
|
106
106
|
provide('pageConfig', currentPage);
|
|
107
107
|
provide('currentPageId', computed(() => currentPage.value.id));
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
// Suppress the loading placeholder during the initial hydration tick
|
|
110
|
+
// so SSG-rendered pages don't flash "Loading…" while the client-side
|
|
111
|
+
// syncPage briefly resets componentKeys to []. After the first mount
|
|
112
|
+
// completes, genuine slow loads (real navigation) can still surface
|
|
113
|
+
// the placeholder.
|
|
114
|
+
const isHydrating = ref(true);
|
|
115
|
+
onMounted(() => {
|
|
116
|
+
nextTick().then(() => {
|
|
117
|
+
isHydrating.value = false;
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const showLoadingIndicator = computed(() =>
|
|
122
|
+
!isHydrating.value &&
|
|
123
|
+
isLoading.value &&
|
|
124
|
+
componentKeys.value.length === 0 &&
|
|
125
|
+
!loadError.value
|
|
126
|
+
);
|
|
110
127
|
const loadErrorMessage = computed(() => loadError.value ? (loadError.value.message || 'Unable to load page content.') : '');
|
|
111
128
|
|
|
112
129
|
watch(componentKeys, async () => {
|
package/src/main.js
CHANGED
|
@@ -39,12 +39,26 @@ const extractThemeKey = (siteData) => {
|
|
|
39
39
|
return '';
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
const applySiteTheme = (themeKey) => {
|
|
43
|
-
if (typeof document === 'undefined') return;
|
|
42
|
+
const applySiteTheme = (themeKey, head) => {
|
|
44
43
|
const normalized = normalizeThemeKey(themeKey);
|
|
45
44
|
const resolved = normalized || 'base';
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
|
|
46
|
+
// Push the html attribute via @unhead so `data-site-theme="X"` lands
|
|
47
|
+
// in the SSR-rendered HTML *before* Vue hydrates. Theme CSS selectors
|
|
48
|
+
// (`:root[data-site-theme="X"]`) then apply during the very first
|
|
49
|
+
// paint, eliminating the need for site-side critical-CSS overrides.
|
|
50
|
+
if (head && typeof head.push === 'function') {
|
|
51
|
+
head.push({ htmlAttrs: { 'data-site-theme': resolved } });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Client only: also set the attribute imperatively (so runtime theme
|
|
55
|
+
// switching is reflected immediately) and inline the CSS variable map
|
|
56
|
+
// for themes that ship JS-only design tokens (e.g. the bundled `base`
|
|
57
|
+
// theme has no theme.css).
|
|
58
|
+
if (typeof document !== 'undefined') {
|
|
59
|
+
document.documentElement.dataset.siteTheme = resolved;
|
|
60
|
+
applyThemeVariables(resolved);
|
|
61
|
+
}
|
|
48
62
|
};
|
|
49
63
|
|
|
50
64
|
export function createCmsApp() {
|
|
@@ -74,9 +88,7 @@ export function createCmsApp() {
|
|
|
74
88
|
|
|
75
89
|
if (initialState.siteTheme) {
|
|
76
90
|
setActiveThemeKey(initialState.siteTheme);
|
|
77
|
-
|
|
78
|
-
applySiteTheme(initialState.siteTheme);
|
|
79
|
-
}
|
|
91
|
+
applySiteTheme(initialState.siteTheme, ctx.head);
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
applyRouterGuards(router);
|
|
@@ -130,7 +142,7 @@ export function createCmsApp() {
|
|
|
130
142
|
initialState.siteTheme ?? normalizeThemeKey(extractThemeKey(initialState.siteConfig));
|
|
131
143
|
initialState.siteTheme = existingTheme;
|
|
132
144
|
setActiveThemeKey(existingTheme);
|
|
133
|
-
applySiteTheme(existingTheme);
|
|
145
|
+
applySiteTheme(existingTheme, ctx.head);
|
|
134
146
|
return initialState.siteConfig;
|
|
135
147
|
}
|
|
136
148
|
|
|
@@ -140,7 +152,7 @@ export function createCmsApp() {
|
|
|
140
152
|
initialState.siteConfigLocale = localeKey;
|
|
141
153
|
initialState.siteTheme = themeKey;
|
|
142
154
|
setActiveThemeKey(themeKey);
|
|
143
|
-
applySiteTheme(themeKey);
|
|
155
|
+
applySiteTheme(themeKey, ctx.head);
|
|
144
156
|
return siteData;
|
|
145
157
|
};
|
|
146
158
|
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Pure CSS-variable derivation from a theme manifest. Lives in its own
|
|
2
|
+
// file (with no Vite-specific imports) so it can be called from both
|
|
3
|
+
// the runtime themeManager AND the build-time vite plugin, which uses
|
|
4
|
+
// it to emit `virtual:cms-theme-vars.css` — a synchronously-linked
|
|
5
|
+
// stylesheet containing every registered theme's tokens. That, combined
|
|
6
|
+
// with the SSR-rendered `data-site-theme` attribute (see main.js
|
|
7
|
+
// applySiteTheme), eliminates the FOUC dark-themed sites used to suffer.
|
|
8
|
+
|
|
9
|
+
const toKebab = (value = '') =>
|
|
10
|
+
value
|
|
11
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
12
|
+
.replace(/[\s_]+/g, '-')
|
|
13
|
+
.toLowerCase();
|
|
14
|
+
|
|
15
|
+
const withPx = (value) => {
|
|
16
|
+
if (value === undefined || value === null) return undefined;
|
|
17
|
+
if (typeof value === 'number') return `${value}px`;
|
|
18
|
+
return value;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const setVar = (vars, name, value) => {
|
|
22
|
+
if (value === undefined || value === null || value === '') return;
|
|
23
|
+
vars[name] = String(value);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const assignPrefixed = (vars, entries = {}, prefix = '') => {
|
|
27
|
+
Object.entries(entries).forEach(([key, value]) => {
|
|
28
|
+
setVar(vars, `${prefix}${key}`, value);
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function setSurfaceVars(vars, prefix, surface = {}) {
|
|
33
|
+
if (!surface) return;
|
|
34
|
+
setVar(vars, `${prefix}-bg`, surface.background);
|
|
35
|
+
setVar(vars, `${prefix}-before`, surface.before);
|
|
36
|
+
setVar(vars, `${prefix}-after`, surface.after);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function coerceSurface(value, defaults = {}) {
|
|
40
|
+
if (value && typeof value === 'object') return value;
|
|
41
|
+
if (!value) return defaults;
|
|
42
|
+
return { ...defaults, background: value };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function coerceArray(value) {
|
|
46
|
+
if (!value) return undefined;
|
|
47
|
+
return Array.isArray(value) ? value.filter(Boolean).join(', ') : value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildCssVarMap(manifest) {
|
|
51
|
+
if (!manifest) return {};
|
|
52
|
+
const vars = {};
|
|
53
|
+
const tokens = manifest.tokens || {};
|
|
54
|
+
const palette = tokens.palette || {};
|
|
55
|
+
const text = tokens.text || {};
|
|
56
|
+
const surfaces = tokens.surfaces || {};
|
|
57
|
+
const helper = surfaces.helper || {};
|
|
58
|
+
const tabs = surfaces.tabs || {};
|
|
59
|
+
const field = surfaces.field || {};
|
|
60
|
+
const strip = surfaces.strip || {};
|
|
61
|
+
const chrome = surfaces.chrome || {};
|
|
62
|
+
const heroSurface = surfaces.backdropPrimary || {};
|
|
63
|
+
const promoSurface = surfaces.backdropSecondary || {};
|
|
64
|
+
const typography = tokens.typography || {};
|
|
65
|
+
const ctas = tokens.ctas || {};
|
|
66
|
+
const chips = tokens.chips || {};
|
|
67
|
+
const focus = tokens.focus || {};
|
|
68
|
+
const radii = tokens.radii || {};
|
|
69
|
+
const elevation = tokens.elevation || {};
|
|
70
|
+
const utility = tokens.utility || {};
|
|
71
|
+
|
|
72
|
+
assignPrefixed(
|
|
73
|
+
vars,
|
|
74
|
+
Object.fromEntries(
|
|
75
|
+
Object.entries(palette).map(([key, value]) => [`--brand-${toKebab(key)}`, value])
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
setVar(vars, '--brand-accent-electric', palette.primary || palette.accent);
|
|
80
|
+
setVar(vars, '--brand-accent-electric-soft', palette.accentSoft);
|
|
81
|
+
setVar(vars, '--brand-accent-warm', palette.accent || palette.warning);
|
|
82
|
+
|
|
83
|
+
setVar(vars, '--brand-fg-100', text.primary);
|
|
84
|
+
setVar(vars, '--brand-fg-200', text.muted);
|
|
85
|
+
setVar(vars, '--brand-fg-300', text.mutedStrong);
|
|
86
|
+
setVar(vars, '--brand-fg-050', text.inverse);
|
|
87
|
+
setVar(vars, '--brand-bg-900', surfaces.base);
|
|
88
|
+
setVar(vars, '--brand-bg-800', surfaces.baseAlt);
|
|
89
|
+
setVar(vars, '--brand-bg-700', surfaces.raised);
|
|
90
|
+
setVar(vars, '--brand-bg-600', surfaces.sunken);
|
|
91
|
+
|
|
92
|
+
Object.entries(radii).forEach(([key, value]) => {
|
|
93
|
+
setVar(vars, `--brand-radius-${toKebab(key)}`, withPx(value));
|
|
94
|
+
});
|
|
95
|
+
setVar(vars, '--brand-card-radius', withPx(radii.lg || radii.md));
|
|
96
|
+
setVar(vars, '--brand-button-radius', withPx(radii.md || radii.sm));
|
|
97
|
+
|
|
98
|
+
setVar(vars, '--brand-shadow-glow', elevation.raised);
|
|
99
|
+
setVar(vars, '--brand-shadow-glow-strong', elevation.overlay);
|
|
100
|
+
setVar(vars, '--brand-card-shadow', elevation.raised);
|
|
101
|
+
setVar(vars, '--brand-surface-card-shadow', elevation.raised);
|
|
102
|
+
|
|
103
|
+
const cardTextColor = text.card || text.primary;
|
|
104
|
+
setVar(vars, '--brand-card-soft', surfaces.card);
|
|
105
|
+
setVar(vars, '--brand-surface-card-bg', surfaces.card);
|
|
106
|
+
setVar(vars, '--brand-card-border', surfaces.border);
|
|
107
|
+
setVar(vars, '--brand-surface-card-border', surfaces.border);
|
|
108
|
+
setVar(vars, '--brand-card-text', cardTextColor);
|
|
109
|
+
setVar(vars, '--brand-surface-helper-bg', helper.background || surfaces.callout);
|
|
110
|
+
setVar(vars, '--brand-surface-helper-hover-bg', helper.hover || helper.background);
|
|
111
|
+
setVar(vars, '--brand-icon-badge-bg', surfaces.chipAccent || surfaces.chip);
|
|
112
|
+
setVar(vars, '--brand-icon-badge-color', palette.primary || text.primary);
|
|
113
|
+
|
|
114
|
+
setVar(vars, '--brand-border-highlight', surfaces.border);
|
|
115
|
+
setVar(vars, '--brand-border-glow', surfaces.border);
|
|
116
|
+
|
|
117
|
+
setVar(vars, '--brand-status-success', palette.success);
|
|
118
|
+
setVar(vars, '--brand-status-error', palette.critical);
|
|
119
|
+
setVar(vars, '--brand-status-error-soft', palette.criticalSoft);
|
|
120
|
+
|
|
121
|
+
setVar(vars, '--brand-input-bg', field.background || surfaces.input);
|
|
122
|
+
setVar(vars, '--brand-input-border', field.border || utility.inputBorder);
|
|
123
|
+
setVar(vars, '--brand-input-border-active', focus.ring);
|
|
124
|
+
setVar(vars, '--brand-input-text', field.inputColor || utility.inputText || text.primary);
|
|
125
|
+
setVar(
|
|
126
|
+
vars,
|
|
127
|
+
'--brand-input-placeholder',
|
|
128
|
+
field.inputPlaceholder || utility.inputPlaceholder || text.muted
|
|
129
|
+
);
|
|
130
|
+
setVar(vars, '--brand-focus-ring', focus.ring);
|
|
131
|
+
setVar(vars, '--brand-focus-glow', focus.shadowInset);
|
|
132
|
+
|
|
133
|
+
setVar(vars, '--brand-modal-backdrop', surfaces.backdrop);
|
|
134
|
+
setVar(vars, '--brand-modal-surface', utility.modalSurface);
|
|
135
|
+
setVar(vars, '--brand-modal-border', utility.modalBorder);
|
|
136
|
+
setVar(vars, '--brand-modal-shadow', utility.modalShadow);
|
|
137
|
+
setVar(vars, '--brand-modal-radius', withPx(utility.modalRadius));
|
|
138
|
+
|
|
139
|
+
setVar(vars, '--brand-chart-track', utility.chartTrack);
|
|
140
|
+
setVar(vars, '--brand-chart-center-text', utility.chartCenterText);
|
|
141
|
+
|
|
142
|
+
setVar(vars, '--brand-header-bg', chrome.background);
|
|
143
|
+
setVar(vars, '--brand-header-text', chrome.text);
|
|
144
|
+
setVar(vars, '--site-header-shadow', chrome.shadow);
|
|
145
|
+
setVar(vars, '--site-header-shadow-compact', chrome.compactShadow);
|
|
146
|
+
|
|
147
|
+
const primaryCta = ctas.primary || {};
|
|
148
|
+
setVar(vars, '--brand-primary-cta-gradient', primaryCta.bg);
|
|
149
|
+
setVar(vars, '--brand-primary-cta-text', primaryCta.text);
|
|
150
|
+
setVar(vars, '--brand-primary-cta-border', primaryCta.border);
|
|
151
|
+
setVar(vars, '--brand-primary-cta-shadow', primaryCta.shadow);
|
|
152
|
+
setVar(vars, '--brand-primary-cta-hover-shadow', primaryCta.shadow);
|
|
153
|
+
setVar(vars, '--brand-primary-cta-hover-translate', 'translateY(-1px)');
|
|
154
|
+
setVar(vars, '--brand-cta-text', primaryCta.text);
|
|
155
|
+
|
|
156
|
+
setVar(vars, '--helper-strip-bg', helper.background || surfaces.callout);
|
|
157
|
+
setVar(vars, '--helper-strip-border', helper.border || surfaces.border);
|
|
158
|
+
setVar(vars, '--helper-strip-color', helper.text || text.primary);
|
|
159
|
+
setVar(vars, '--helper-strip-hover-bg', helper.hover || helper.background);
|
|
160
|
+
setVar(vars, '--helper-strip-hover-color', helper.hoverColor || helper.text || text.primary);
|
|
161
|
+
setVar(vars, '--helper-strip-link-hover', helper.linkHover || palette.primary);
|
|
162
|
+
setVar(vars, '--helper-strip-heading-color', helper.heading || text.primary);
|
|
163
|
+
setVar(vars, '--helper-strip-body-color', helper.body || text.mutedStrong || text.muted);
|
|
164
|
+
|
|
165
|
+
setVar(vars, '--tabs-bg', tabs.background);
|
|
166
|
+
setVar(vars, '--tabs-border', tabs.border || surfaces.border);
|
|
167
|
+
setVar(vars, '--tabs-shadow', tabs.shadow || elevation.flat);
|
|
168
|
+
setVar(vars, '--tab-color', tabs.tabColor || text.muted);
|
|
169
|
+
setVar(vars, '--tab-active-bg', tabs.activeBackground || palette.primary);
|
|
170
|
+
setVar(vars, '--tab-active-color', tabs.activeColor || palette.inverse || '#fff');
|
|
171
|
+
setVar(vars, '--tab-active-shadow', tabs.activeShadow || elevation.raised);
|
|
172
|
+
setVar(vars, '--tab-step-bg', tabs.stepBackground || helper.background);
|
|
173
|
+
setVar(vars, '--tab-step-border', tabs.stepBorder || helper.border);
|
|
174
|
+
setVar(vars, '--tab-step-color', tabs.stepColor || palette.primary);
|
|
175
|
+
setVar(vars, '--tab-active-step-bg', tabs.activeStepBackground || palette.primary);
|
|
176
|
+
setVar(vars, '--tab-active-step-border', tabs.activeStepBorder || tabs.stepBorder);
|
|
177
|
+
setVar(vars, '--tab-active-step-color', tabs.activeStepColor || palette.inverse || '#fff');
|
|
178
|
+
|
|
179
|
+
setVar(vars, '--field-bg', field.background);
|
|
180
|
+
setVar(vars, '--field-border', field.border || utility.inputBorder);
|
|
181
|
+
setVar(vars, '--field-shadow', field.shadow || elevation.flat);
|
|
182
|
+
setVar(vars, '--field-addon-bg', field.addonBackground || helper.background);
|
|
183
|
+
setVar(vars, '--field-addon-border', field.addonBorder || helper.border);
|
|
184
|
+
setVar(vars, '--field-addon-color', field.addonColor || helper.text || text.primary);
|
|
185
|
+
setVar(vars, '--field-input-color', field.inputColor || text.primary);
|
|
186
|
+
setVar(vars, '--field-input-placeholder', field.inputPlaceholder || text.muted);
|
|
187
|
+
|
|
188
|
+
setVar(vars, '--community-strip-bg', strip.background || surfaces.card);
|
|
189
|
+
setVar(vars, '--community-strip-border', strip.border || surfaces.border);
|
|
190
|
+
setVar(vars, '--community-strip-color', strip.text || text.primary);
|
|
191
|
+
|
|
192
|
+
setVar(vars, '--ui-text-primary', text.primary);
|
|
193
|
+
setVar(vars, '--ui-text-muted', text.muted);
|
|
194
|
+
setVar(vars, '--ui-field-label', text.muted);
|
|
195
|
+
setVar(vars, '--ui-field-value', text.primary);
|
|
196
|
+
setVar(vars, '--ui-status-heading-color', text.accent || palette.primary);
|
|
197
|
+
|
|
198
|
+
setVar(vars, '--brand-countdown-digit', palette.primary);
|
|
199
|
+
setVar(vars, '--brand-countdown-label', text.muted);
|
|
200
|
+
|
|
201
|
+
const secondaryCta = ctas.secondary || {};
|
|
202
|
+
setVar(vars, '--brand-pill-gradient', primaryCta.bg);
|
|
203
|
+
setVar(vars, '--brand-pill-alt-gradient', secondaryCta.bg || primaryCta.bg);
|
|
204
|
+
setVar(vars, '--brand-pill-contrast', primaryCta.text);
|
|
205
|
+
|
|
206
|
+
setVar(vars, '--brand-chip-neutral-bg', chips.neutral?.bg);
|
|
207
|
+
setVar(vars, '--brand-chip-neutral-color', chips.neutral?.text);
|
|
208
|
+
setVar(vars, '--brand-chip-neutral-border', chips.neutral?.border);
|
|
209
|
+
|
|
210
|
+
const heroBg = coerceArray(utility.gradientHero || heroSurface.background);
|
|
211
|
+
const promoBg = coerceArray(utility.gradientPromo || promoSurface.background);
|
|
212
|
+
if (heroBg) setVar(vars, '--brand-gradient-hero', heroBg);
|
|
213
|
+
if (promoBg) setVar(vars, '--brand-gradient-promo', promoBg);
|
|
214
|
+
|
|
215
|
+
setSurfaceVars(vars, '--hero-surface', heroSurface);
|
|
216
|
+
setSurfaceVars(vars, '--promo-surface', promoSurface);
|
|
217
|
+
const bodyBackground = coerceArray(utility.bodyBackground);
|
|
218
|
+
if (bodyBackground) {
|
|
219
|
+
setVar(vars, '--theme-body-background', bodyBackground);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const statusHeadline = coerceSurface(utility.statusHeadline, {
|
|
223
|
+
color: palette.primary || text.primary,
|
|
224
|
+
shadow: elevation.raised,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
setVar(vars, '--status-headline-bg', statusHeadline.background);
|
|
228
|
+
setVar(vars, '--status-headline-color', statusHeadline.color);
|
|
229
|
+
setVar(vars, '--status-headline-shadow', statusHeadline.shadow);
|
|
230
|
+
|
|
231
|
+
setVar(vars, '--status-price-value-color', palette.accent || palette.primary || text.primary);
|
|
232
|
+
setVar(vars, '--status-price-value-shadow', elevation.raised);
|
|
233
|
+
|
|
234
|
+
return vars;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Render the var map as a CSS rule body.
|
|
239
|
+
* @param {object} vars Map of CSS custom property name → value
|
|
240
|
+
*/
|
|
241
|
+
export function renderCssVarRule(selector, vars) {
|
|
242
|
+
const declarations = Object.entries(vars)
|
|
243
|
+
.map(([name, value]) => ` ${name}: ${value};`)
|
|
244
|
+
.join('\n');
|
|
245
|
+
return `${selector} {\n${declarations}\n}`;
|
|
246
|
+
}
|
|
@@ -71,6 +71,36 @@ if (!catalog.base) {
|
|
|
71
71
|
* Called by the Vite plugin for each theme passed via the `themes` option.
|
|
72
72
|
* Must be called before the first render (during app initialisation).
|
|
73
73
|
*
|
|
74
|
+
* Two patterns for shipping theme CSS:
|
|
75
|
+
*
|
|
76
|
+
* 1. **Side-effect import (preferred).** The theme's `index.js` imports
|
|
77
|
+
* its `theme.css` for side effects:
|
|
78
|
+
*
|
|
79
|
+
* import manifest from './theme.config.js';
|
|
80
|
+
* import './theme.css';
|
|
81
|
+
* export default { manifest };
|
|
82
|
+
*
|
|
83
|
+
* Vite bundles `theme.css` into the main entry chunk and emits a
|
|
84
|
+
* synchronous `<link rel="stylesheet">` in `<head>`, so theme rules
|
|
85
|
+
* apply during the initial paint. Combined with the SSR-rendered
|
|
86
|
+
* `data-site-theme` attribute (set in `applySiteTheme`), this
|
|
87
|
+
* eliminates the FOUC that JS-injected theme CSS used to cause.
|
|
88
|
+
*
|
|
89
|
+
* 2. **Inline string (legacy).** The theme's `index.js` imports the CSS
|
|
90
|
+
* with `?inline` and exports it as a `css` string:
|
|
91
|
+
*
|
|
92
|
+
* import manifest from './theme.config.js';
|
|
93
|
+
* import css from './theme.css?inline';
|
|
94
|
+
* export default { manifest, css };
|
|
95
|
+
*
|
|
96
|
+
* The CSS is injected via a runtime `<style>` tag (handled below).
|
|
97
|
+
* This works but applies after Vue hydrates, so dark themes will
|
|
98
|
+
* flash a light first paint unless the site provides critical CSS.
|
|
99
|
+
*
|
|
100
|
+
* Existing themes (cms-theme-neon, -frog, -aurora, -southpark, -swamp)
|
|
101
|
+
* still use pattern 2 and are supported indefinitely; new themes should
|
|
102
|
+
* prefer pattern 1.
|
|
103
|
+
*
|
|
74
104
|
* @param {object} themeModule - Default export from a @koehler8/cms-theme-* package
|
|
75
105
|
* Expected shape: { manifest, css? } or just the manifest object directly.
|
|
76
106
|
*/
|
|
@@ -85,7 +115,9 @@ export function registerTheme(themeModule) {
|
|
|
85
115
|
|
|
86
116
|
addToCatalog(manifest, sourceId, undefined);
|
|
87
117
|
|
|
88
|
-
//
|
|
118
|
+
// Legacy pattern 2: theme shipped inline CSS, inject it into the document head.
|
|
119
|
+
// For pattern 1 themes, `cssContent` is undefined and this block is skipped —
|
|
120
|
+
// the CSS is already loaded via Vite's bundling.
|
|
89
121
|
if (cssContent && typeof cssContent === 'string' && typeof document !== 'undefined') {
|
|
90
122
|
const slug = normalizeSlug(manifest.slug);
|
|
91
123
|
const existingStyle = document.getElementById(`theme-css-${slug}`);
|
|
@@ -1,232 +1,9 @@
|
|
|
1
1
|
import { resolveThemeManifest } from './themeLoader.js';
|
|
2
|
+
import { buildCssVarMap } from './buildCssVarMap.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
.replace(/[\s_]+/g, '-')
|
|
7
|
-
.toLowerCase();
|
|
8
|
-
|
|
9
|
-
const withPx = (value) => {
|
|
10
|
-
if (value === undefined || value === null) return undefined;
|
|
11
|
-
if (typeof value === 'number') return `${value}px`;
|
|
12
|
-
return value;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const setVar = (vars, name, value) => {
|
|
16
|
-
if (value === undefined || value === null || value === '') return;
|
|
17
|
-
vars[name] = String(value);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const assignPrefixed = (vars, entries = {}, prefix) => {
|
|
21
|
-
Object.entries(entries).forEach(([key, value]) => {
|
|
22
|
-
setVar(vars, `${prefix}${key}`, value);
|
|
23
|
-
});
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function setSurfaceVars(vars, prefix, surface = {}) {
|
|
27
|
-
if (!surface) return;
|
|
28
|
-
setVar(vars, `${prefix}-bg`, surface.background);
|
|
29
|
-
setVar(vars, `${prefix}-before`, surface.before);
|
|
30
|
-
setVar(vars, `${prefix}-after`, surface.after);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function coerceSurface(value, defaults = {}) {
|
|
34
|
-
if (value && typeof value === 'object') return value;
|
|
35
|
-
if (!value) return defaults;
|
|
36
|
-
return { ...defaults, background: value };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function coerceArray(value) {
|
|
40
|
-
if (!value) return undefined;
|
|
41
|
-
return Array.isArray(value) ? value.filter(Boolean).join(', ') : value;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function buildCssVarMap(manifest) {
|
|
45
|
-
if (!manifest) return {};
|
|
46
|
-
const vars = {};
|
|
47
|
-
const tokens = manifest.tokens || {};
|
|
48
|
-
const palette = tokens.palette || {};
|
|
49
|
-
const text = tokens.text || {};
|
|
50
|
-
const surfaces = tokens.surfaces || {};
|
|
51
|
-
const helper = surfaces.helper || {};
|
|
52
|
-
const tabs = surfaces.tabs || {};
|
|
53
|
-
const field = surfaces.field || {};
|
|
54
|
-
const strip = surfaces.strip || {};
|
|
55
|
-
const chrome = surfaces.chrome || {};
|
|
56
|
-
const heroSurface = surfaces.backdropPrimary || {};
|
|
57
|
-
const promoSurface = surfaces.backdropSecondary || {};
|
|
58
|
-
const typography = tokens.typography || {};
|
|
59
|
-
const ctas = tokens.ctas || {};
|
|
60
|
-
const chips = tokens.chips || {};
|
|
61
|
-
const focus = tokens.focus || {};
|
|
62
|
-
const radii = tokens.radii || {};
|
|
63
|
-
const elevation = tokens.elevation || {};
|
|
64
|
-
const utility = tokens.utility || {};
|
|
65
|
-
|
|
66
|
-
assignPrefixed(
|
|
67
|
-
vars,
|
|
68
|
-
Object.fromEntries(
|
|
69
|
-
Object.entries(palette).map(([key, value]) => [`--brand-${toKebab(key)}`, value])
|
|
70
|
-
)
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
setVar(vars, '--brand-accent-electric', palette.primary || palette.accent);
|
|
74
|
-
setVar(vars, '--brand-accent-electric-soft', palette.accentSoft);
|
|
75
|
-
setVar(vars, '--brand-accent-warm', palette.accent || palette.warning);
|
|
76
|
-
|
|
77
|
-
setVar(vars, '--brand-fg-100', text.primary);
|
|
78
|
-
setVar(vars, '--brand-fg-200', text.muted);
|
|
79
|
-
setVar(vars, '--brand-fg-300', text.mutedStrong);
|
|
80
|
-
setVar(vars, '--brand-fg-050', text.inverse);
|
|
81
|
-
setVar(vars, '--brand-bg-900', surfaces.base);
|
|
82
|
-
setVar(vars, '--brand-bg-800', surfaces.baseAlt);
|
|
83
|
-
setVar(vars, '--brand-bg-700', surfaces.raised);
|
|
84
|
-
setVar(vars, '--brand-bg-600', surfaces.sunken);
|
|
85
|
-
|
|
86
|
-
Object.entries(radii).forEach(([key, value]) => {
|
|
87
|
-
setVar(vars, `--brand-radius-${toKebab(key)}`, withPx(value));
|
|
88
|
-
});
|
|
89
|
-
setVar(vars, '--brand-card-radius', withPx(radii.lg || radii.md));
|
|
90
|
-
setVar(vars, '--brand-button-radius', withPx(radii.md || radii.sm));
|
|
91
|
-
|
|
92
|
-
setVar(vars, '--brand-shadow-glow', elevation.raised);
|
|
93
|
-
setVar(vars, '--brand-shadow-glow-strong', elevation.overlay);
|
|
94
|
-
setVar(vars, '--brand-card-shadow', elevation.raised);
|
|
95
|
-
setVar(vars, '--brand-surface-card-shadow', elevation.raised);
|
|
96
|
-
|
|
97
|
-
const cardTextColor = text.card || text.primary;
|
|
98
|
-
setVar(vars, '--brand-card-soft', surfaces.card);
|
|
99
|
-
setVar(vars, '--brand-surface-card-bg', surfaces.card);
|
|
100
|
-
setVar(vars, '--brand-card-border', surfaces.border);
|
|
101
|
-
setVar(vars, '--brand-surface-card-border', surfaces.border);
|
|
102
|
-
setVar(vars, '--brand-card-text', cardTextColor);
|
|
103
|
-
setVar(vars, '--brand-surface-helper-bg', helper.background || surfaces.callout);
|
|
104
|
-
setVar(vars, '--brand-surface-helper-hover-bg', helper.hover || helper.background);
|
|
105
|
-
setVar(vars, '--brand-icon-badge-bg', surfaces.chipAccent || surfaces.chip);
|
|
106
|
-
setVar(vars, '--brand-icon-badge-color', palette.primary || text.primary);
|
|
107
|
-
|
|
108
|
-
setVar(vars, '--brand-border-highlight', surfaces.border);
|
|
109
|
-
setVar(vars, '--brand-border-glow', surfaces.border);
|
|
110
|
-
|
|
111
|
-
setVar(vars, '--brand-status-success', palette.success);
|
|
112
|
-
setVar(vars, '--brand-status-error', palette.critical);
|
|
113
|
-
setVar(vars, '--brand-status-error-soft', palette.criticalSoft);
|
|
114
|
-
|
|
115
|
-
setVar(vars, '--brand-input-bg', field.background || surfaces.input);
|
|
116
|
-
setVar(vars, '--brand-input-border', field.border || utility.inputBorder);
|
|
117
|
-
setVar(vars, '--brand-input-border-active', focus.ring);
|
|
118
|
-
setVar(vars, '--brand-input-text', field.inputColor || utility.inputText || text.primary);
|
|
119
|
-
setVar(
|
|
120
|
-
vars,
|
|
121
|
-
'--brand-input-placeholder',
|
|
122
|
-
field.inputPlaceholder || utility.inputPlaceholder || text.muted
|
|
123
|
-
);
|
|
124
|
-
setVar(vars, '--brand-focus-ring', focus.ring);
|
|
125
|
-
setVar(vars, '--brand-focus-glow', focus.shadowInset);
|
|
126
|
-
|
|
127
|
-
setVar(vars, '--brand-modal-backdrop', surfaces.backdrop);
|
|
128
|
-
setVar(vars, '--brand-modal-surface', utility.modalSurface);
|
|
129
|
-
setVar(vars, '--brand-modal-border', utility.modalBorder);
|
|
130
|
-
setVar(vars, '--brand-modal-shadow', utility.modalShadow);
|
|
131
|
-
setVar(vars, '--brand-modal-radius', withPx(utility.modalRadius));
|
|
132
|
-
|
|
133
|
-
setVar(vars, '--brand-chart-track', utility.chartTrack);
|
|
134
|
-
setVar(vars, '--brand-chart-center-text', utility.chartCenterText);
|
|
135
|
-
|
|
136
|
-
setVar(vars, '--brand-header-bg', chrome.background);
|
|
137
|
-
setVar(vars, '--brand-header-text', chrome.text);
|
|
138
|
-
setVar(vars, '--site-header-shadow', chrome.shadow);
|
|
139
|
-
setVar(vars, '--site-header-shadow-compact', chrome.compactShadow);
|
|
140
|
-
|
|
141
|
-
const primaryCta = ctas.primary || {};
|
|
142
|
-
setVar(vars, '--brand-primary-cta-gradient', primaryCta.bg);
|
|
143
|
-
setVar(vars, '--brand-primary-cta-text', primaryCta.text);
|
|
144
|
-
setVar(vars, '--brand-primary-cta-border', primaryCta.border);
|
|
145
|
-
setVar(vars, '--brand-primary-cta-shadow', primaryCta.shadow);
|
|
146
|
-
setVar(vars, '--brand-primary-cta-hover-shadow', primaryCta.shadow);
|
|
147
|
-
setVar(vars, '--brand-primary-cta-hover-translate', 'translateY(-1px)');
|
|
148
|
-
setVar(vars, '--brand-cta-text', primaryCta.text);
|
|
149
|
-
|
|
150
|
-
setVar(vars, '--helper-strip-bg', helper.background || surfaces.callout);
|
|
151
|
-
setVar(vars, '--helper-strip-border', helper.border || surfaces.border);
|
|
152
|
-
setVar(vars, '--helper-strip-color', helper.text || text.primary);
|
|
153
|
-
setVar(vars, '--helper-strip-hover-bg', helper.hover || helper.background);
|
|
154
|
-
setVar(vars, '--helper-strip-hover-color', helper.hoverColor || helper.text || text.primary);
|
|
155
|
-
setVar(vars, '--helper-strip-link-hover', helper.linkHover || palette.primary);
|
|
156
|
-
setVar(vars, '--helper-strip-heading-color', helper.heading || text.primary);
|
|
157
|
-
setVar(vars, '--helper-strip-body-color', helper.body || text.mutedStrong || text.muted);
|
|
158
|
-
|
|
159
|
-
setVar(vars, '--tabs-bg', tabs.background);
|
|
160
|
-
setVar(vars, '--tabs-border', tabs.border || surfaces.border);
|
|
161
|
-
setVar(vars, '--tabs-shadow', tabs.shadow || elevation.flat);
|
|
162
|
-
setVar(vars, '--tab-color', tabs.tabColor || text.muted);
|
|
163
|
-
setVar(vars, '--tab-active-bg', tabs.activeBackground || palette.primary);
|
|
164
|
-
setVar(vars, '--tab-active-color', tabs.activeColor || palette.inverse || '#fff');
|
|
165
|
-
setVar(vars, '--tab-active-shadow', tabs.activeShadow || elevation.raised);
|
|
166
|
-
setVar(vars, '--tab-step-bg', tabs.stepBackground || helper.background);
|
|
167
|
-
setVar(vars, '--tab-step-border', tabs.stepBorder || helper.border);
|
|
168
|
-
setVar(vars, '--tab-step-color', tabs.stepColor || palette.primary);
|
|
169
|
-
setVar(vars, '--tab-active-step-bg', tabs.activeStepBackground || palette.primary);
|
|
170
|
-
setVar(vars, '--tab-active-step-border', tabs.activeStepBorder || tabs.stepBorder);
|
|
171
|
-
setVar(vars, '--tab-active-step-color', tabs.activeStepColor || palette.inverse || '#fff');
|
|
172
|
-
|
|
173
|
-
setVar(vars, '--field-bg', field.background);
|
|
174
|
-
setVar(vars, '--field-border', field.border || utility.inputBorder);
|
|
175
|
-
setVar(vars, '--field-shadow', field.shadow || elevation.flat);
|
|
176
|
-
setVar(vars, '--field-addon-bg', field.addonBackground || helper.background);
|
|
177
|
-
setVar(vars, '--field-addon-border', field.addonBorder || helper.border);
|
|
178
|
-
setVar(vars, '--field-addon-color', field.addonColor || helper.text || text.primary);
|
|
179
|
-
setVar(vars, '--field-input-color', field.inputColor || text.primary);
|
|
180
|
-
setVar(vars, '--field-input-placeholder', field.inputPlaceholder || text.muted);
|
|
181
|
-
|
|
182
|
-
setVar(vars, '--community-strip-bg', strip.background || surfaces.card);
|
|
183
|
-
setVar(vars, '--community-strip-border', strip.border || surfaces.border);
|
|
184
|
-
setVar(vars, '--community-strip-color', strip.text || text.primary);
|
|
185
|
-
|
|
186
|
-
setVar(vars, '--ui-text-primary', text.primary);
|
|
187
|
-
setVar(vars, '--ui-text-muted', text.muted);
|
|
188
|
-
setVar(vars, '--ui-field-label', text.muted);
|
|
189
|
-
setVar(vars, '--ui-field-value', text.primary);
|
|
190
|
-
setVar(vars, '--ui-status-heading-color', text.accent || palette.primary);
|
|
191
|
-
|
|
192
|
-
setVar(vars, '--brand-countdown-digit', palette.primary);
|
|
193
|
-
setVar(vars, '--brand-countdown-label', text.muted);
|
|
194
|
-
|
|
195
|
-
const secondaryCta = ctas.secondary || {};
|
|
196
|
-
setVar(vars, '--brand-pill-gradient', primaryCta.bg);
|
|
197
|
-
setVar(vars, '--brand-pill-alt-gradient', secondaryCta.bg || primaryCta.bg);
|
|
198
|
-
setVar(vars, '--brand-pill-contrast', primaryCta.text);
|
|
199
|
-
|
|
200
|
-
setVar(vars, '--brand-chip-neutral-bg', chips.neutral?.bg);
|
|
201
|
-
setVar(vars, '--brand-chip-neutral-color', chips.neutral?.text);
|
|
202
|
-
setVar(vars, '--brand-chip-neutral-border', chips.neutral?.border);
|
|
203
|
-
|
|
204
|
-
const heroBg = coerceArray(utility.gradientHero || heroSurface.background);
|
|
205
|
-
const promoBg = coerceArray(utility.gradientPromo || promoSurface.background);
|
|
206
|
-
if (heroBg) setVar(vars, '--brand-gradient-hero', heroBg);
|
|
207
|
-
if (promoBg) setVar(vars, '--brand-gradient-promo', promoBg);
|
|
208
|
-
|
|
209
|
-
setSurfaceVars(vars, '--hero-surface', heroSurface);
|
|
210
|
-
setSurfaceVars(vars, '--promo-surface', promoSurface);
|
|
211
|
-
const bodyBackground = coerceArray(utility.bodyBackground);
|
|
212
|
-
if (bodyBackground) {
|
|
213
|
-
setVar(vars, '--theme-body-background', bodyBackground);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const statusHeadline = coerceSurface(utility.statusHeadline, {
|
|
217
|
-
color: palette.primary || text.primary,
|
|
218
|
-
shadow: elevation.raised,
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
setVar(vars, '--status-headline-bg', statusHeadline.background);
|
|
222
|
-
setVar(vars, '--status-headline-color', statusHeadline.color);
|
|
223
|
-
setVar(vars, '--status-headline-shadow', statusHeadline.shadow);
|
|
224
|
-
|
|
225
|
-
setVar(vars, '--status-price-value-color', palette.accent || palette.primary || text.primary);
|
|
226
|
-
setVar(vars, '--status-price-value-shadow', elevation.raised);
|
|
227
|
-
|
|
228
|
-
return vars;
|
|
229
|
-
}
|
|
4
|
+
// buildCssVarMap was extracted to its own pure module so the build-time
|
|
5
|
+
// vite plugin can call it (to emit `virtual:cms-theme-vars.css`) without
|
|
6
|
+
// pulling in this file's runtime-only imports.
|
|
230
7
|
|
|
231
8
|
export function applyThemeVariables(themeKey) {
|
|
232
9
|
const manifest = resolveThemeManifest(themeKey);
|
package/vite-plugin.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import {
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
12
13
|
import vue from '@vitejs/plugin-vue';
|
|
13
14
|
|
|
14
15
|
import { SUPPORTED_LOCALES } from './src/constants/locales.js';
|
|
@@ -99,8 +100,13 @@ const __dirname = path.dirname(__filename);
|
|
|
99
100
|
const VIRTUAL_CONFIG = 'virtual:cms-config-loader';
|
|
100
101
|
const VIRTUAL_STYLES = 'virtual:cms-site-styles';
|
|
101
102
|
const VIRTUAL_ASSETS = 'virtual:cms-asset-resolver';
|
|
103
|
+
// Synthetic stylesheet built from each registered theme's manifest at build
|
|
104
|
+
// time. Importing this from the generated entry causes Vite to emit a real
|
|
105
|
+
// <link rel="stylesheet"> in <head>, so theme CSS variables apply during the
|
|
106
|
+
// initial paint instead of after Vue hydrates.
|
|
107
|
+
const VIRTUAL_THEME_VARS = 'virtual:cms-theme-vars.css';
|
|
102
108
|
|
|
103
|
-
const VIRTUAL_IDS = new Set([VIRTUAL_CONFIG, VIRTUAL_STYLES, VIRTUAL_ASSETS]);
|
|
109
|
+
const VIRTUAL_IDS = new Set([VIRTUAL_CONFIG, VIRTUAL_STYLES, VIRTUAL_ASSETS, VIRTUAL_THEME_VARS]);
|
|
104
110
|
|
|
105
111
|
// Entry file name (written to site repo root, gitignored, cleaned up in buildEnd)
|
|
106
112
|
const ENTRY_FILENAME = '.cms-entry.js';
|
|
@@ -148,6 +154,7 @@ ${extImports}
|
|
|
148
154
|
import * as __cmsConfig from '${VIRTUAL_CONFIG}';
|
|
149
155
|
import * as __cmsStyles from '${VIRTUAL_STYLES}';
|
|
150
156
|
import * as __cmsAssets from '${VIRTUAL_ASSETS}';
|
|
157
|
+
import '${VIRTUAL_THEME_VARS}';
|
|
151
158
|
|
|
152
159
|
setConfigLoader(__cmsConfig);
|
|
153
160
|
setSiteStyleLoader(__cmsStyles);
|
|
@@ -479,10 +486,66 @@ export default function cmsPlugin(options = {}) {
|
|
|
479
486
|
return null;
|
|
480
487
|
},
|
|
481
488
|
|
|
482
|
-
load(id) {
|
|
489
|
+
async load(id) {
|
|
483
490
|
if (!isResolvedVirtual(id)) return null;
|
|
484
491
|
const virtualId = id.slice(1); // strip leading \0
|
|
485
492
|
|
|
493
|
+
if (virtualId === VIRTUAL_THEME_VARS) {
|
|
494
|
+
// Build a stylesheet by deriving CSS variables from each registered
|
|
495
|
+
// theme's manifest at build time. We resolve the theme package via
|
|
496
|
+
// the site's node_modules (createRequire from siteRoot) and import
|
|
497
|
+
// its pure manifest through the package's `./config` exports field
|
|
498
|
+
// (which points at theme.config.js — sidestepping any Vite-only
|
|
499
|
+
// imports the theme's index.js may have, e.g. `?inline` CSS or a
|
|
500
|
+
// side-effect `import './theme.css'`). The bundled `base` theme is
|
|
501
|
+
// always included so sites without a `themes` option still get a
|
|
502
|
+
// sync theme.
|
|
503
|
+
const { buildCssVarMap, renderCssVarRule } = await import(
|
|
504
|
+
'./src/themes/buildCssVarMap.js'
|
|
505
|
+
);
|
|
506
|
+
const baseManifestModule = await import('./themes/base/theme.config.js');
|
|
507
|
+
const baseManifest = baseManifestModule.default || baseManifestModule;
|
|
508
|
+
|
|
509
|
+
const requireFromSite = createRequire(path.join(siteRoot, 'package.json'));
|
|
510
|
+
|
|
511
|
+
const blocks = [];
|
|
512
|
+
const seen = new Set();
|
|
513
|
+
|
|
514
|
+
const addManifest = (manifest, sourceLabel) => {
|
|
515
|
+
if (!manifest || typeof manifest !== 'object') return;
|
|
516
|
+
const slug = typeof manifest.slug === 'string' ? manifest.slug.trim().toLowerCase() : '';
|
|
517
|
+
if (!slug || seen.has(slug)) return;
|
|
518
|
+
seen.add(slug);
|
|
519
|
+
const vars = buildCssVarMap(manifest);
|
|
520
|
+
if (Object.keys(vars).length === 0) return;
|
|
521
|
+
const selector = `:root[data-site-theme="${slug}"]`;
|
|
522
|
+
blocks.push(`/* ${sourceLabel} */\n${renderCssVarRule(selector, vars)}`);
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
addManifest(baseManifest, '@koehler8/cms (base)');
|
|
526
|
+
|
|
527
|
+
for (const pkg of themePackages) {
|
|
528
|
+
let manifestPath;
|
|
529
|
+
try {
|
|
530
|
+
manifestPath = requireFromSite.resolve(`${pkg}/config`);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
this.warn(`[cms-vite-plugin] Theme "${pkg}" does not expose a "./config" export pointing at theme.config.js; skipping. (${err.message})`);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
let manifest;
|
|
536
|
+
try {
|
|
537
|
+
const mod = await import(pathToFileURL(manifestPath).href);
|
|
538
|
+
manifest = mod.default || mod.manifest || mod;
|
|
539
|
+
} catch (err) {
|
|
540
|
+
this.warn(`[cms-vite-plugin] Failed to load theme manifest from "${pkg}": ${err.message}`);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
addManifest(manifest, pkg);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return blocks.join('\n\n') + '\n';
|
|
547
|
+
}
|
|
548
|
+
|
|
486
549
|
if (virtualId === VIRTUAL_CONFIG) {
|
|
487
550
|
return `
|
|
488
551
|
import { createConfigLoader } from '@koehler8/cms/utils/loadConfig';
|