@koehler8/cms 1.0.0-beta.5

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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/bin/cms-generate-public-assets.js +27 -0
  4. package/bin/cms-validate-extensions.js +18 -0
  5. package/bin/cms-validate-themes.js +7 -0
  6. package/extensions/manifest.schema.json +125 -0
  7. package/package.json +84 -0
  8. package/public/img/preloaders/preloader-black.svg +1 -0
  9. package/public/img/preloaders/preloader-white.svg +1 -0
  10. package/public/robots.txt +5 -0
  11. package/scripts/check-overflow.mjs +33 -0
  12. package/scripts/generate-public-assets.js +401 -0
  13. package/scripts/patch-lru-cache-tla.js +164 -0
  14. package/scripts/validate-extensions.mjs +392 -0
  15. package/scripts/validate-themes.mjs +64 -0
  16. package/src/App.vue +3 -0
  17. package/src/components/About.vue +481 -0
  18. package/src/components/AboutValue.vue +361 -0
  19. package/src/components/BackToTop.vue +42 -0
  20. package/src/components/ComingSoon.vue +411 -0
  21. package/src/components/ComingSoonModal.vue +230 -0
  22. package/src/components/Contact.vue +518 -0
  23. package/src/components/Footer.vue +65 -0
  24. package/src/components/FooterMinimal.vue +153 -0
  25. package/src/components/Header.vue +583 -0
  26. package/src/components/Hero.vue +327 -0
  27. package/src/components/Home.vue +144 -0
  28. package/src/components/Intro.vue +130 -0
  29. package/src/components/IntroGate.vue +444 -0
  30. package/src/components/Plan.vue +116 -0
  31. package/src/components/Portfolio.vue +459 -0
  32. package/src/components/Preloader.vue +20 -0
  33. package/src/components/Principles.vue +67 -0
  34. package/src/components/Spacer15.vue +9 -0
  35. package/src/components/Spacer30.vue +9 -0
  36. package/src/components/Spacer40.vue +9 -0
  37. package/src/components/Spacer60.vue +9 -0
  38. package/src/components/StickyCTA.vue +263 -0
  39. package/src/components/Team.vue +432 -0
  40. package/src/components/icons/IconLinkedIn.vue +22 -0
  41. package/src/components/icons/IconX.vue +22 -0
  42. package/src/components/ui/SbCard.vue +52 -0
  43. package/src/components/ui/SkeletonPulse.vue +117 -0
  44. package/src/components/ui/UnitChip.vue +69 -0
  45. package/src/composables/useComingSoonConfig.js +120 -0
  46. package/src/composables/useComingSoonInterstitial.js +27 -0
  47. package/src/composables/useComponentResolver.js +196 -0
  48. package/src/composables/useEngagementTracking.js +187 -0
  49. package/src/composables/useIntroGate.js +46 -0
  50. package/src/composables/useLazyImage.js +77 -0
  51. package/src/composables/usePageConfig.js +184 -0
  52. package/src/composables/usePageMeta.js +76 -0
  53. package/src/composables/usePromoBackgroundStyles.js +67 -0
  54. package/src/constants/locales.js +20 -0
  55. package/src/extensions/extensionLoader.js +512 -0
  56. package/src/main.js +175 -0
  57. package/src/router/index.js +112 -0
  58. package/src/styles/base.css +896 -0
  59. package/src/styles/layout.css +342 -0
  60. package/src/styles/theme-base.css +84 -0
  61. package/src/themes/themeLoader.js +124 -0
  62. package/src/themes/themeManager.js +257 -0
  63. package/src/themes/themeValidator.js +380 -0
  64. package/src/utils/analytics.js +100 -0
  65. package/src/utils/appInfo.js +9 -0
  66. package/src/utils/assetResolver.js +162 -0
  67. package/src/utils/componentRegistry.js +46 -0
  68. package/src/utils/contentRequirements.js +67 -0
  69. package/src/utils/cookieConsent.js +281 -0
  70. package/src/utils/ctaCopy.js +58 -0
  71. package/src/utils/formatNumber.js +115 -0
  72. package/src/utils/imageSources.js +179 -0
  73. package/src/utils/inflateFlatConfig.js +30 -0
  74. package/src/utils/loadConfig.js +271 -0
  75. package/src/utils/semver.js +49 -0
  76. package/src/utils/siteStyles.js +40 -0
  77. package/src/utils/themeColors.js +65 -0
  78. package/src/utils/trackingContext.js +142 -0
  79. package/src/utils/unwrapDefault.js +14 -0
  80. package/src/utils/useScrollReveal.js +48 -0
  81. package/templates/index.html +36 -0
  82. package/themes/base/README.md +23 -0
  83. package/themes/base/theme.config.js +214 -0
  84. package/vite-plugin.js +637 -0
@@ -0,0 +1,342 @@
1
+ :root {
2
+ --layout-container-max-width: min(1180px, 92vw);
3
+ --layout-container-padding: clamp(16px, 4vw, 48px);
4
+ --layout-section-padding: clamp(48px, 12vw, 120px);
5
+ --layout-section-padding-tight: clamp(32px, 10vw, 80px);
6
+ --layout-grid-gutter: clamp(16px, 4vw, 32px);
7
+ --layout-stack-gap-tight: var(--ui-space-12, 12px);
8
+ --layout-stack-gap: var(--ui-space-16, 16px);
9
+ --layout-stack-gap-lg: var(--ui-space-24, 24px);
10
+ --layout-stack-gap-xl: var(--ui-space-40, 40px);
11
+ --layout-breakpoint-sm: 600px;
12
+ --layout-breakpoint-md: 768px;
13
+ --layout-breakpoint-lg: 1024px;
14
+ --layout-breakpoint-xl: 1280px;
15
+ }
16
+
17
+ /* Containers */
18
+ .container {
19
+ width: min(var(--layout-container-max-width), 100%);
20
+ margin-left: auto;
21
+ margin-right: auto;
22
+ padding-left: var(--layout-container-padding);
23
+ padding-right: var(--layout-container-padding);
24
+ }
25
+
26
+ .container-fluid {
27
+ width: 100%;
28
+ margin: 0;
29
+ padding-left: var(--layout-container-padding);
30
+ padding-right: var(--layout-container-padding);
31
+ }
32
+
33
+ .section-shell {
34
+ padding-block: var(--layout-section-padding);
35
+ }
36
+
37
+ .section-shell--tight {
38
+ padding-block: var(--layout-section-padding-tight);
39
+ }
40
+
41
+ /* Spacing helpers */
42
+ .mt-0 {
43
+ margin-top: 0 !important;
44
+ }
45
+
46
+ .mb-0 {
47
+ margin-bottom: 0 !important;
48
+ }
49
+
50
+ .mb-1 {
51
+ margin-bottom: var(--ui-space-8, 8px) !important;
52
+ }
53
+
54
+ .mb-2 {
55
+ margin-bottom: var(--ui-space-12, 12px) !important;
56
+ }
57
+
58
+ .mb-3 {
59
+ margin-bottom: var(--ui-space-16, 16px) !important;
60
+ }
61
+
62
+ .mb-4 {
63
+ margin-bottom: var(--ui-space-24, 24px) !important;
64
+ }
65
+
66
+ .mt-1 {
67
+ margin-top: var(--ui-space-8, 8px) !important;
68
+ }
69
+
70
+ .mt-2 {
71
+ margin-top: var(--ui-space-12, 12px) !important;
72
+ }
73
+
74
+ .mt-3 {
75
+ margin-top: var(--ui-space-16, 16px) !important;
76
+ }
77
+
78
+ .mt-4 {
79
+ margin-top: var(--ui-space-24, 24px) !important;
80
+ }
81
+
82
+ /* Grid */
83
+ .row {
84
+ display: flex;
85
+ flex-wrap: wrap;
86
+ margin-left: calc(var(--layout-grid-gutter) * -0.5);
87
+ margin-right: calc(var(--layout-grid-gutter) * -0.5);
88
+ row-gap: var(--layout-grid-gutter);
89
+ }
90
+
91
+ .row--tight {
92
+ column-gap: var(--ui-gap-md, 16px);
93
+ row-gap: var(--ui-gap-md, 16px);
94
+ }
95
+
96
+ .row > [class*='col-'] {
97
+ padding-left: calc(var(--layout-grid-gutter) * 0.5);
98
+ padding-right: calc(var(--layout-grid-gutter) * 0.5);
99
+ flex: 0 0 auto;
100
+ width: 100%;
101
+ max-width: 100%;
102
+ --col-span: 12;
103
+ }
104
+
105
+ .row .col-auto {
106
+ flex: 0 0 auto;
107
+ width: auto;
108
+ max-width: none;
109
+ }
110
+
111
+ .row .col {
112
+ flex: 1 1 0;
113
+ width: auto;
114
+ max-width: none;
115
+ }
116
+
117
+ .row .col-1 { --col-span: 1; }
118
+ .row .col-2 { --col-span: 2; }
119
+ .row .col-3 { --col-span: 3; }
120
+ .row .col-4 { --col-span: 4; }
121
+ .row .col-5 { --col-span: 5; }
122
+ .row .col-6 { --col-span: 6; }
123
+ .row .col-7 { --col-span: 7; }
124
+ .row .col-8 { --col-span: 8; }
125
+ .row .col-9 { --col-span: 9; }
126
+ .row .col-10 { --col-span: 10; }
127
+ .row .col-11 { --col-span: 11; }
128
+ .row .col-12 { --col-span: 12; }
129
+
130
+ .row > [class*='col-']:not(.col-auto):not(.col) {
131
+ flex-basis: calc((var(--col-span) / 12) * 100%);
132
+ max-width: calc((var(--col-span) / 12) * 100%);
133
+ }
134
+
135
+ @media (min-width: 600px) {
136
+ .row .col-sm-1 { --col-span: 1; }
137
+ .row .col-sm-2 { --col-span: 2; }
138
+ .row .col-sm-3 { --col-span: 3; }
139
+ .row .col-sm-4 { --col-span: 4; }
140
+ .row .col-sm-5 { --col-span: 5; }
141
+ .row .col-sm-6 { --col-span: 6; }
142
+ .row .col-sm-7 { --col-span: 7; }
143
+ .row .col-sm-8 { --col-span: 8; }
144
+ .row .col-sm-9 { --col-span: 9; }
145
+ .row .col-sm-10 { --col-span: 10; }
146
+ .row .col-sm-11 { --col-span: 11; }
147
+ .row .col-sm-12 { --col-span: 12; }
148
+ }
149
+
150
+ @media (min-width: 768px) {
151
+ .row .col-md-1 { --col-span: 1; }
152
+ .row .col-md-2 { --col-span: 2; }
153
+ .row .col-md-3 { --col-span: 3; }
154
+ .row .col-md-4 { --col-span: 4; }
155
+ .row .col-md-5 { --col-span: 5; }
156
+ .row .col-md-6 { --col-span: 6; }
157
+ .row .col-md-7 { --col-span: 7; }
158
+ .row .col-md-8 { --col-span: 8; }
159
+ .row .col-md-9 { --col-span: 9; }
160
+ .row .col-md-10 { --col-span: 10; }
161
+ .row .col-md-11 { --col-span: 11; }
162
+ .row .col-md-12 { --col-span: 12; }
163
+ }
164
+
165
+ @media (min-width: 1024px) {
166
+ .row .col-lg-1 { --col-span: 1; }
167
+ .row .col-lg-2 { --col-span: 2; }
168
+ .row .col-lg-3 { --col-span: 3; }
169
+ .row .col-lg-4 { --col-span: 4; }
170
+ .row .col-lg-5 { --col-span: 5; }
171
+ .row .col-lg-6 { --col-span: 6; }
172
+ .row .col-lg-7 { --col-span: 7; }
173
+ .row .col-lg-8 { --col-span: 8; }
174
+ .row .col-lg-9 { --col-span: 9; }
175
+ .row .col-lg-10 { --col-span: 10; }
176
+ .row .col-lg-11 { --col-span: 11; }
177
+ .row .col-lg-12 { --col-span: 12; }
178
+ }
179
+
180
+ @media (min-width: 1280px) {
181
+ .row .col-xl-1 { --col-span: 1; }
182
+ .row .col-xl-2 { --col-span: 2; }
183
+ .row .col-xl-3 { --col-span: 3; }
184
+ .row .col-xl-4 { --col-span: 4; }
185
+ .row .col-xl-5 { --col-span: 5; }
186
+ .row .col-xl-6 { --col-span: 6; }
187
+ .row .col-xl-7 { --col-span: 7; }
188
+ .row .col-xl-8 { --col-span: 8; }
189
+ .row .col-xl-9 { --col-span: 9; }
190
+ .row .col-xl-10 { --col-span: 10; }
191
+ .row .col-xl-11 { --col-span: 11; }
192
+ .row .col-xl-12 { --col-span: 12; }
193
+ }
194
+
195
+ /* Stack helpers */
196
+ .stack-tight {
197
+ display: flex;
198
+ flex-direction: column;
199
+ gap: var(--layout-stack-gap-tight);
200
+ }
201
+
202
+ .stack {
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: var(--layout-stack-gap);
206
+ }
207
+
208
+ .stack-lg {
209
+ display: flex;
210
+ flex-direction: column;
211
+ gap: var(--layout-stack-gap-lg);
212
+ }
213
+
214
+ .stack-xl {
215
+ display: flex;
216
+ flex-direction: column;
217
+ gap: var(--layout-stack-gap-xl);
218
+ }
219
+
220
+ .gap-sm {
221
+ gap: var(--layout-stack-gap-tight);
222
+ }
223
+
224
+ .gap-md {
225
+ gap: var(--layout-stack-gap);
226
+ }
227
+
228
+ .gap-lg {
229
+ gap: var(--layout-stack-gap-lg);
230
+ }
231
+
232
+ .gap-xl {
233
+ gap: var(--layout-stack-gap-xl);
234
+ }
235
+
236
+ /* Typography helpers */
237
+ .text-center {
238
+ text-align: center !important;
239
+ }
240
+
241
+ .text-left {
242
+ text-align: left !important;
243
+ }
244
+
245
+ .text-right {
246
+ text-align: right !important;
247
+ }
248
+
249
+ .text-muted {
250
+ color: var(--ui-text-muted, var(--brand-fg-300, #54627b));
251
+ }
252
+
253
+ .text-primary {
254
+ color: var(--ui-text-primary, var(--brand-fg-100, #1f2a44));
255
+ }
256
+
257
+ .text-uppercase {
258
+ text-transform: uppercase;
259
+ letter-spacing: 0.12em;
260
+ }
261
+
262
+ .eyebrow-label {
263
+ font-size: 0.85rem;
264
+ font-weight: 600;
265
+ text-transform: uppercase;
266
+ letter-spacing: 0.16em;
267
+ color: var(--ui-text-muted, var(--brand-fg-300, #54627b));
268
+ }
269
+
270
+ /* Flexbox + alignment helpers */
271
+ .d-flex {
272
+ display: flex !important;
273
+ }
274
+
275
+ .flex-column {
276
+ flex-direction: column !important;
277
+ }
278
+
279
+ .flex-wrap {
280
+ flex-wrap: wrap !important;
281
+ }
282
+
283
+ .justify-between {
284
+ justify-content: space-between !important;
285
+ }
286
+
287
+ .justify-center {
288
+ justify-content: center !important;
289
+ }
290
+
291
+ .align-center {
292
+ align-items: center !important;
293
+ }
294
+
295
+ .align-start {
296
+ align-items: flex-start !important;
297
+ }
298
+
299
+ .align-end {
300
+ align-items: flex-end !important;
301
+ }
302
+
303
+ .mx-auto {
304
+ margin-left: auto !important;
305
+ margin-right: auto !important;
306
+ }
307
+
308
+ .w-100 {
309
+ width: 100% !important;
310
+ }
311
+
312
+ .rounded {
313
+ border-radius: 12px;
314
+ }
315
+
316
+ .rounded-lg {
317
+ border-radius: 24px;
318
+ }
319
+
320
+ .img-fluid {
321
+ display: block;
322
+ max-width: 100%;
323
+ height: auto;
324
+ }
325
+
326
+ .text-break {
327
+ overflow-wrap: anywhere;
328
+ }
329
+
330
+ .visually-hidden {
331
+ position: absolute;
332
+ width: 1px;
333
+ height: 1px;
334
+ padding: 0;
335
+ margin: -1px;
336
+ overflow: hidden;
337
+ clip: rect(0, 0, 0, 0);
338
+ white-space: nowrap;
339
+ border: 0;
340
+ }
341
+
342
+ /* Legal pages */
@@ -0,0 +1,84 @@
1
+ :root {
2
+ --about-image-surface: rgba(10, 10, 13, 0.94);
3
+ --about-image-placeholder: linear-gradient(135deg, rgba(20, 18, 22, 0.55), rgba(10, 10, 13, 0.75));
4
+ --responsive-picture-surface: var(--about-image-surface);
5
+ --responsive-picture-placeholder: var(--about-image-placeholder);
6
+ --brand-neon-pink: #e44b8d;
7
+ --brand-crimson-red: #d04f4f;
8
+ --brand-sultry-purple: #5c6ac4;
9
+ --brand-electric-blue: #4f6cf0;
10
+ --brand-amber-heat: #f18f3b;
11
+ --brand-deep-violet: #2f3a68;
12
+
13
+ --brand-bg-900: #f5f7ff;
14
+ --brand-bg-800: #edf1ff;
15
+ --brand-bg-700: #dce4ff;
16
+ --brand-fg-050: #243a80;
17
+ --brand-fg-100: #1f2a44;
18
+ --brand-fg-200: #334261;
19
+ --brand-fg-300: #54627b;
20
+
21
+ --brand-radius-sm: 8px;
22
+ --brand-radius-md: 14px;
23
+ --brand-radius-lg: 24px;
24
+
25
+ --brand-gradient-ignition: linear-gradient(135deg, #4f6cf0 0%, #243a80 100%);
26
+ --brand-shadow-glow: 0 16px 32px rgba(28, 42, 96, 0.28);
27
+ --brand-shadow-glow-strong: 0 22px 44px rgba(23, 35, 80, 0.32);
28
+ --brand-chart-track: rgba(236, 241, 255, 0.85);
29
+ --brand-chart-center-text: var(--brand-fg-100);
30
+ --brand-cta-text: #ffffff;
31
+
32
+ --brand-accent-electric: var(--brand-electric-blue);
33
+ --brand-accent-electric-soft: rgba(79, 108, 240, 0.18);
34
+ --brand-accent-warm: var(--brand-amber-heat);
35
+
36
+ --brand-card-soft: #ffffff;
37
+ --brand-card-border: rgba(79, 108, 240, 0.16);
38
+ --brand-card-shadow: 0 18px 40px rgba(15, 23, 42, 0.12);
39
+
40
+ --brand-primary-cta-gradient: var(--brand-gradient-ignition);
41
+ --brand-primary-cta-text: var(--brand-cta-text);
42
+ --brand-primary-cta-shadow: var(--brand-shadow-glow);
43
+ --brand-primary-cta-hover-shadow: var(--brand-shadow-glow-strong);
44
+ --brand-primary-cta-hover-translate: translateY(-2px);
45
+
46
+ --brand-surface-card-bg: var(--brand-card-soft);
47
+ --brand-surface-card-border: var(--brand-card-border);
48
+ --brand-surface-card-shadow: var(--brand-card-shadow);
49
+ --brand-surface-helper-bg: rgba(79, 108, 240, 0.08);
50
+ --brand-surface-helper-hover-bg: rgba(79, 108, 240, 0.14);
51
+ --brand-card-radius: var(--brand-radius-lg);
52
+ --brand-button-radius: var(--brand-radius-md);
53
+
54
+ --brand-icon-badge-bg: rgba(79, 108, 240, 0.12);
55
+ --brand-icon-badge-color: var(--brand-deep-violet);
56
+
57
+ --brand-pill-gradient: linear-gradient(135deg, var(--brand-electric-blue) 0%, #2b3d7b 100%);
58
+ --brand-pill-alt-gradient: linear-gradient(135deg, var(--brand-sultry-purple) 0%, var(--brand-electric-blue) 100%);
59
+ --brand-pill-contrast: #ffffff;
60
+
61
+ --brand-border-highlight: rgba(79, 108, 240, 0.28);
62
+ --brand-border-glow: rgba(79, 108, 240, 0.32);
63
+
64
+ --brand-status-success: #239c65;
65
+ --brand-status-error: var(--brand-crimson-red);
66
+ --brand-status-error-soft: rgba(208, 79, 79, 0.18);
67
+
68
+ --brand-input-bg: #ffffff;
69
+ --brand-input-border: rgba(79, 108, 240, 0.25);
70
+ --brand-input-border-active: rgba(79, 108, 240, 0.45);
71
+ --brand-input-text: var(--brand-fg-100);
72
+ --brand-input-placeholder: rgba(84, 98, 123, 0.64);
73
+ --brand-focus-ring: 0 0 0 2px rgba(79, 108, 240, 0.3);
74
+ --brand-focus-glow: 0 0 22px rgba(79, 108, 240, 0.22);
75
+
76
+ --brand-modal-backdrop: rgba(15, 23, 42, 0.55);
77
+ --brand-modal-surface: rgba(255, 255, 255, 0.97);
78
+ --brand-modal-border: rgba(79, 108, 240, 0.18);
79
+ --brand-modal-shadow: 0 24px 48px rgba(15, 23, 42, 0.18);
80
+ --brand-modal-radius: var(--brand-radius-lg);
81
+
82
+ --brand-countdown-digit: var(--brand-accent-electric);
83
+ --brand-countdown-label: rgba(84, 98, 123, 0.75);
84
+ }
@@ -0,0 +1,124 @@
1
+ import baseManifest from '../../themes/base/theme.config.js';
2
+ const cssModules = import.meta.glob('../../themes/**/theme.css', { eager: true });
3
+ import { validateThemeManifest } from './themeValidator.js';
4
+
5
+ const manifestModules = import.meta.glob(
6
+ [
7
+ '../../themes/**/theme.config.js',
8
+ '../../themes/**/theme.config.mjs',
9
+ '../../themes/**/theme.config.json',
10
+ ],
11
+ { eager: true }
12
+ );
13
+
14
+ const catalog = {};
15
+
16
+ const normalizeSlug = (slug) =>
17
+ typeof slug === 'string' && slug.trim().length > 0 ? slug.trim().toLowerCase() : null;
18
+
19
+ const cssMap = Object.keys(cssModules).reduce((acc, key) => {
20
+ const slugMatch = key.match(/\/themes\/([^/]+)\/theme\.css$/);
21
+ if (slugMatch) {
22
+ acc[slugMatch[1]] = key;
23
+ }
24
+ return acc;
25
+ }, {});
26
+
27
+ const addToCatalog = (manifest, sourceId, cssPath) => {
28
+ if (!manifest || typeof manifest !== 'object') return;
29
+ const normalized = normalizeSlug(manifest.slug);
30
+ try {
31
+ validateThemeManifest(manifest);
32
+ } catch (error) {
33
+ const message = error?.message || 'Unknown theme validation error';
34
+ throw new Error(`[theme-loader] ${message} (source: ${sourceId})`);
35
+ }
36
+ if (!normalized) {
37
+ throw new Error(`[theme-loader] Theme manifest missing slug (source: ${sourceId})`);
38
+ }
39
+ if (catalog[normalized]) {
40
+ console.warn(
41
+ `[theme-loader] Duplicate theme slug "${normalized}" detected. Source ${sourceId} skipped.`
42
+ );
43
+ return;
44
+ }
45
+
46
+ catalog[normalized] = {
47
+ ...manifest,
48
+ slug: normalized,
49
+ assets: {
50
+ ...manifest.assets,
51
+ css: manifest.assets?.css || cssPath || undefined,
52
+ },
53
+ };
54
+ };
55
+
56
+ // Auto-discover bundled themes (i.e. base)
57
+ Object.entries(manifestModules).forEach(([path, module]) => {
58
+ const manifest = module?.default ?? module;
59
+ const slug = normalizeSlug(manifest?.slug);
60
+ const bundledCssPath = slug ? cssMap[slug] : undefined;
61
+ addToCatalog(manifest, path, bundledCssPath ? bundledCssPath.replace('../../', '') : undefined);
62
+ });
63
+
64
+ if (!catalog.base) {
65
+ addToCatalog(baseManifest, 'themes/base/theme.config.js', undefined);
66
+ }
67
+
68
+ /**
69
+ * Register an external theme package.
70
+ *
71
+ * Called by the Vite plugin for each theme passed via the `themes` option.
72
+ * Must be called before the first render (during app initialisation).
73
+ *
74
+ * @param {object} themeModule - Default export from a @koehler8/cms-theme-* package
75
+ * Expected shape: { manifest, css? } or just the manifest object directly.
76
+ */
77
+ export function registerTheme(themeModule) {
78
+ if (!themeModule || typeof themeModule !== 'object') {
79
+ throw new Error('[theme-loader] registerTheme() requires a theme module object');
80
+ }
81
+
82
+ const manifest = themeModule.manifest || themeModule;
83
+ const cssContent = themeModule.css;
84
+ const sourceId = `external:${manifest?.slug || 'unknown'}`;
85
+
86
+ addToCatalog(manifest, sourceId, undefined);
87
+
88
+ // If the theme package shipped inline CSS, inject it into the document head.
89
+ if (cssContent && typeof cssContent === 'string' && typeof document !== 'undefined') {
90
+ const slug = normalizeSlug(manifest.slug);
91
+ const existingStyle = document.getElementById(`theme-css-${slug}`);
92
+ if (!existingStyle) {
93
+ const style = document.createElement('style');
94
+ style.id = `theme-css-${slug}`;
95
+ style.textContent = cssContent;
96
+ document.head.appendChild(style);
97
+ }
98
+ }
99
+ }
100
+
101
+ export function getThemeCatalog() {
102
+ return catalog;
103
+ }
104
+
105
+ export function getThemeManifest(themeKey) {
106
+ if (typeof themeKey !== 'string') return null;
107
+ return catalog[themeKey.trim().toLowerCase()] || null;
108
+ }
109
+
110
+ export function resolveThemeManifest(themeKey) {
111
+ return (
112
+ getThemeManifest(themeKey) ||
113
+ catalog.base ||
114
+ Object.values(catalog)[0] ||
115
+ null
116
+ );
117
+ }
118
+
119
+ export function listThemeMetadata() {
120
+ return Object.values(catalog).map((manifest) => ({
121
+ slug: manifest.slug,
122
+ meta: manifest.meta,
123
+ }));
124
+ }