@koehler8/cms 1.0.0-beta.10

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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -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 +88 -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 +412 -0
  13. package/scripts/migrate-to-content-dirs.mjs +223 -0
  14. package/scripts/patch-lru-cache-tla.js +164 -0
  15. package/scripts/validate-extensions.mjs +392 -0
  16. package/scripts/validate-themes.mjs +64 -0
  17. package/src/App.vue +3 -0
  18. package/src/components/About.vue +481 -0
  19. package/src/components/AboutValue.vue +361 -0
  20. package/src/components/BackToTop.vue +42 -0
  21. package/src/components/ComingSoon.vue +411 -0
  22. package/src/components/ComingSoonModal.vue +230 -0
  23. package/src/components/Contact.vue +518 -0
  24. package/src/components/Footer.vue +65 -0
  25. package/src/components/FooterMinimal.vue +153 -0
  26. package/src/components/Header.vue +583 -0
  27. package/src/components/Hero.vue +327 -0
  28. package/src/components/Home.vue +161 -0
  29. package/src/components/Intro.vue +130 -0
  30. package/src/components/IntroGate.vue +444 -0
  31. package/src/components/Plan.vue +116 -0
  32. package/src/components/Portfolio.vue +459 -0
  33. package/src/components/Preloader.vue +20 -0
  34. package/src/components/Principles.vue +67 -0
  35. package/src/components/Spacer15.vue +9 -0
  36. package/src/components/Spacer30.vue +9 -0
  37. package/src/components/Spacer40.vue +9 -0
  38. package/src/components/Spacer60.vue +9 -0
  39. package/src/components/StickyCTA.vue +263 -0
  40. package/src/components/Team.vue +432 -0
  41. package/src/components/icons/IconLinkedIn.vue +22 -0
  42. package/src/components/icons/IconX.vue +22 -0
  43. package/src/components/ui/SbCard.vue +52 -0
  44. package/src/components/ui/SkeletonPulse.vue +117 -0
  45. package/src/components/ui/UnitChip.vue +69 -0
  46. package/src/composables/useComingSoonConfig.js +120 -0
  47. package/src/composables/useComingSoonInterstitial.js +27 -0
  48. package/src/composables/useComponentResolver.js +235 -0
  49. package/src/composables/useEngagementTracking.js +187 -0
  50. package/src/composables/useIntroGate.js +46 -0
  51. package/src/composables/useLazyImage.js +77 -0
  52. package/src/composables/usePageConfig.js +184 -0
  53. package/src/composables/usePageMeta.js +76 -0
  54. package/src/composables/usePromoBackgroundStyles.js +67 -0
  55. package/src/constants/locales.js +20 -0
  56. package/src/extensions/extensionLoader.js +512 -0
  57. package/src/main.js +187 -0
  58. package/src/router/index.js +112 -0
  59. package/src/styles/base.css +896 -0
  60. package/src/styles/layout.css +342 -0
  61. package/src/styles/theme-base.css +84 -0
  62. package/src/themes/buildCssVarMap.js +246 -0
  63. package/src/themes/themeLoader.js +156 -0
  64. package/src/themes/themeManager.js +34 -0
  65. package/src/themes/themeValidator.js +380 -0
  66. package/src/utils/analytics.js +100 -0
  67. package/src/utils/appInfo.js +9 -0
  68. package/src/utils/assetResolver.js +162 -0
  69. package/src/utils/componentRegistry.js +146 -0
  70. package/src/utils/contentRequirements.js +67 -0
  71. package/src/utils/cookieConsent.js +281 -0
  72. package/src/utils/ctaCopy.js +58 -0
  73. package/src/utils/formatNumber.js +115 -0
  74. package/src/utils/imageSources.js +179 -0
  75. package/src/utils/inflateFlatConfig.js +30 -0
  76. package/src/utils/loadConfig.js +319 -0
  77. package/src/utils/semver.js +49 -0
  78. package/src/utils/siteStyles.js +40 -0
  79. package/src/utils/themeColors.js +65 -0
  80. package/src/utils/trackingContext.js +142 -0
  81. package/src/utils/unwrapDefault.js +14 -0
  82. package/src/utils/useScrollReveal.js +48 -0
  83. package/templates/index.html +36 -0
  84. package/themes/base/README.md +23 -0
  85. package/themes/base/theme.config.js +214 -0
  86. package/vite-plugin.js +726 -0
@@ -0,0 +1,481 @@
1
+ <template>
2
+ <section
3
+ id="about"
4
+ class="about-section section-shell section-shell--tight ui-section--overlay"
5
+ data-analytics-section="about-overview"
6
+ >
7
+ <div
8
+ v-for="slice in aboutSlices"
9
+ :key="slice.key"
10
+ class="container about-slice"
11
+ :class="`about-slice--${slice.key}`"
12
+ >
13
+ <div class="about-slice__grid" :class="{ 'about-slice__grid--reversed': slice.reversed }">
14
+ <div class="about-slice__media" :class="slice.mediaClass">
15
+ <div
16
+ :ref="slice.image.assignRef"
17
+ :class="[
18
+ 'responsive-picture-shell',
19
+ 'rounded',
20
+ 'about-picture',
21
+ slice.image.wrapperVariant,
22
+ ]"
23
+ :style="{ aspectRatio: slice.image.aspectRatio }"
24
+ >
25
+ <picture v-if="slice.image.isVisible" class="responsive-picture">
26
+ <source
27
+ v-for="source in slice.image.sources"
28
+ :key="source.type"
29
+ :type="source.type"
30
+ :srcset="source.srcset"
31
+ :sizes="slice.image.sizes"
32
+ />
33
+ <img
34
+ :src="slice.image.fallback"
35
+ :srcset="slice.image.fallbackSet || undefined"
36
+ :sizes="slice.image.sizes"
37
+ :alt="siteName"
38
+ @error="slice.image.onError"
39
+ :width="slice.image.width"
40
+ :height="slice.image.height"
41
+ loading="lazy"
42
+ decoding="async"
43
+ class="responsive-picture__img about-picture__img"
44
+ />
45
+ </picture>
46
+ <div
47
+ v-else
48
+ class="responsive-picture-placeholder rounded about-picture__img"
49
+ :style="slice.image.placeholder ? { backgroundImage: `url(${slice.image.placeholder})` } : null"
50
+ ></div>
51
+ </div>
52
+ </div>
53
+ <div class="about-slice__content" :class="slice.contentClass">
54
+ <div
55
+ :class="['about-text-wrapper', 'about-text-wrapper--raised', slice.textWrapperClass]"
56
+ >
57
+ <div class="about-text-block brand-card brand-card--translucent">
58
+ <h2 class="about-heading" v-html="slice.content.title"></h2>
59
+ <p class="about-copy" v-html="slice.content.copy"></p>
60
+ </div>
61
+ <a
62
+ class="about-cta primary-button"
63
+ :href="slice.content.button.href || '#'"
64
+ @click="handleAboutButtonClick($event, slice.content.rawButton, slice.buttonOrigin)"
65
+ >
66
+ <span class="about-cta__label">{{ slice.content.button.text }}</span>
67
+ <span class="about-cta__icon" aria-hidden="true">
68
+ <svg viewBox="0 0 16 16" role="presentation" aria-hidden="true">
69
+ <path
70
+ d="M3 8h10m0 0-4-4m4 4-4 4"
71
+ fill="none"
72
+ stroke="currentColor"
73
+ stroke-width="1.5"
74
+ stroke-linecap="round"
75
+ stroke-linejoin="round"
76
+ />
77
+ </svg>
78
+ </span>
79
+ </a>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </section>
85
+ </template>
86
+
87
+ <script setup>
88
+ import { computed, inject, ref } from 'vue';
89
+ import { useComingSoonInterstitial } from '../composables/useComingSoonInterstitial.js';
90
+ import { trackEvent } from '../utils/analytics.js';
91
+ import { useComingSoonResolver } from '../composables/useComingSoonConfig.js';
92
+ import { useResponsiveImage } from '../utils/imageSources.js';
93
+ import { useLazyImage } from '../composables/useLazyImage.js';
94
+
95
+ const { openComingSoon } = useComingSoonInterstitial();
96
+
97
+ const injectedSiteData = inject('siteData', ref({}));
98
+ const pageContent = inject('pageContent', ref({}));
99
+ const { resolve: resolveComingSoon } = useComingSoonResolver(pageContent);
100
+
101
+ const aboutContent = computed(() => pageContent.value?.about || {});
102
+
103
+ const getAboutValue = (key, fallback) => {
104
+ const value = aboutContent.value?.[key];
105
+ return value === undefined || value === null ? fallback : value;
106
+ };
107
+
108
+ const siteName = computed(() => injectedSiteData.value?.site?.title || '');
109
+
110
+ const aboutTopTitle = computed(() => getAboutValue('topTitle', ''));
111
+ const aboutTopCopy = computed(() => getAboutValue('topCopy', ''));
112
+ const aboutBottomTitle = computed(() => getAboutValue('bottomTitle', ''));
113
+ const aboutBottomCopy = computed(() => getAboutValue('bottomCopy', ''));
114
+ const aboutTopButton = computed(() => getAboutValue('topButton', {}));
115
+ const aboutBottomButton = computed(() => getAboutValue('bottomButton', {}));
116
+
117
+ const topImage = useResponsiveImage('img/about-top', {
118
+ widths: [320, 900],
119
+ fallbackFormat: 'jpg',
120
+ });
121
+ const topImageHandleError = topImage.handleError;
122
+ const bottomImage = useResponsiveImage('img/about-bottom', {
123
+ widths: [320, 600],
124
+ fallbackFormat: 'jpg',
125
+ });
126
+ const bottomImageHandleError = bottomImage.handleError;
127
+
128
+ const topImageSizes = '(min-width: 1200px) 66vw, (min-width: 992px) 60vw, 100vw';
129
+ const bottomImageSizes = '(min-width: 1200px) 45vw, (min-width: 992px) 50vw, 100vw';
130
+
131
+ const { isVisible: isTopImageVisible, targetRef: topImageTarget } = useLazyImage({
132
+ rootMargin: '250px 0px',
133
+ });
134
+ const { isVisible: isBottomImageVisible, targetRef: bottomImageTarget } = useLazyImage({
135
+ rootMargin: '250px 0px',
136
+ });
137
+
138
+ const topImageSources = topImage.sources;
139
+ const topImageFallback = topImage.fallbackSrc;
140
+ const topImageFallbackSet = topImage.fallbackSrcSet;
141
+ const topPlaceholder = topImage.placeholderSrc;
142
+
143
+ const bottomImageSources = bottomImage.sources;
144
+ const bottomImageFallback = bottomImage.fallbackSrc;
145
+ const bottomImageFallbackSet = bottomImage.fallbackSrcSet;
146
+ const bottomPlaceholder = bottomImage.placeholderSrc;
147
+
148
+ const normalizeButton = (button = {}) => ({
149
+ text: button?.text ?? '',
150
+ href: button?.href ?? '#',
151
+ });
152
+
153
+ const bindTemplateRef = (target) => (el) => {
154
+ target.value = el;
155
+ };
156
+
157
+ const aboutSlices = computed(() => [
158
+ {
159
+ key: 'top',
160
+ variant: 'about-top',
161
+ buttonOrigin: 'about-top',
162
+ reversed: false,
163
+ mediaClass: 'about-slice__media--wide',
164
+ contentClass: 'about-slice__content--narrow',
165
+ textWrapperClass: 'about-text-wrapper--top',
166
+ content: {
167
+ title: aboutTopTitle.value,
168
+ copy: aboutTopCopy.value,
169
+ button: normalizeButton(aboutTopButton.value),
170
+ rawButton: aboutTopButton.value,
171
+ },
172
+ image: {
173
+ assignRef: bindTemplateRef(topImageTarget),
174
+ isVisible: isTopImageVisible.value,
175
+ sources: topImageSources.value,
176
+ fallback: topImageFallback.value,
177
+ fallbackSet: topImageFallbackSet.value,
178
+ placeholder: topPlaceholder.value,
179
+ sizes: topImageSizes,
180
+ width: 900,
181
+ height: 450,
182
+ aspectRatio: '2 / 1',
183
+ onError: topImageHandleError,
184
+ wrapperVariant: 'about-picture--primary',
185
+ },
186
+ },
187
+ {
188
+ key: 'bottom',
189
+ variant: 'about-bottom',
190
+ buttonOrigin: 'about-bottom',
191
+ reversed: true,
192
+ mediaClass: 'about-slice__media--half',
193
+ contentClass: 'about-slice__content--offset',
194
+ textWrapperClass: 'about-text-wrapper--bottom',
195
+ content: {
196
+ title: aboutBottomTitle.value,
197
+ copy: aboutBottomCopy.value,
198
+ button: normalizeButton(aboutBottomButton.value),
199
+ rawButton: aboutBottomButton.value,
200
+ },
201
+ image: {
202
+ assignRef: bindTemplateRef(bottomImageTarget),
203
+ isVisible: isBottomImageVisible.value,
204
+ sources: bottomImageSources.value,
205
+ fallback: bottomImageFallback.value,
206
+ fallbackSet: bottomImageFallbackSet.value,
207
+ placeholder: bottomPlaceholder.value,
208
+ sizes: bottomImageSizes,
209
+ width: 600,
210
+ height: 450,
211
+ aspectRatio: '4 / 3',
212
+ onError: bottomImageHandleError,
213
+ wrapperVariant: 'about-picture--outlined',
214
+ },
215
+ },
216
+ ]);
217
+
218
+ function handleAboutButtonClick(event, button, origin) {
219
+ if (!event) return;
220
+
221
+ const label = (button?.text ?? '').toString();
222
+ const trimmedLabel = label.trim();
223
+
224
+ const href = (button?.href ?? '').toString().trim();
225
+
226
+ const modalPayload = resolveComingSoon({
227
+ href,
228
+ });
229
+ if (!modalPayload) {
230
+ return;
231
+ }
232
+
233
+ event.preventDefault();
234
+ openComingSoon(modalPayload);
235
+ trackEvent('coming_soon_interstitial_shown', {
236
+ trigger: origin,
237
+ label: trimmedLabel ? trimmedLabel.toLowerCase() : 'coming-soon',
238
+ });
239
+ }
240
+ </script>
241
+
242
+ <style scoped>
243
+ .about-section {
244
+ background: var(--about-section-bg, transparent);
245
+ }
246
+
247
+ .about-section.section-shell--tight {
248
+ padding-top: 50px;
249
+ padding-bottom: 50px;
250
+ }
251
+
252
+ .about-slice {
253
+ padding-block: clamp(24px, 6vw, 56px);
254
+ }
255
+
256
+ .about-slice:last-child {
257
+ padding-bottom: 0;
258
+ }
259
+
260
+ .about-slice--bottom {
261
+ margin-top: clamp(-180px, -12vw, -80px);
262
+ position: relative;
263
+ z-index: 4;
264
+ pointer-events: none;
265
+ }
266
+
267
+ .about-slice--top {
268
+ position: relative;
269
+ }
270
+
271
+ .about-slice__grid {
272
+ display: flex;
273
+ flex-direction: column;
274
+ gap: clamp(20px, 5vw, 48px);
275
+ }
276
+
277
+ .about-slice__media,
278
+ .about-slice__content {
279
+ width: 100%;
280
+ position: relative;
281
+ }
282
+
283
+ .about-slice__media {
284
+ z-index: 1;
285
+ }
286
+
287
+ .about-slice__content {
288
+ z-index: 2;
289
+ }
290
+
291
+ @media (min-width: 1024px) {
292
+ .about-slice__grid {
293
+ flex-direction: row;
294
+ align-items: center;
295
+ gap: clamp(24px, 5vw, 64px);
296
+ }
297
+
298
+ .about-slice__grid--reversed {
299
+ flex-direction: row-reverse;
300
+ }
301
+
302
+ .about-slice__media,
303
+ .about-slice__content {
304
+ flex: 0 0 auto;
305
+ }
306
+
307
+ .about-slice__media--wide {
308
+ flex-basis: 60%;
309
+ }
310
+
311
+ .about-slice__content--narrow {
312
+ flex-basis: 40%;
313
+ }
314
+
315
+ .about-slice__media--half {
316
+ flex-basis: 50%;
317
+ }
318
+
319
+ .about-slice__content--offset {
320
+ flex-basis: 42%;
321
+ }
322
+ }
323
+
324
+ .about-picture {
325
+ width: 100%;
326
+ box-shadow: 0 25px 60px
327
+ color-mix(in srgb, var(--brand-surface-card-shadow, rgba(12, 9, 26, 0.45)) 100%, transparent);
328
+ pointer-events: none;
329
+ }
330
+
331
+ .about-picture--outlined {
332
+ border: 1px solid color-mix(in srgb, var(--brand-accent-electric, rgba(39, 243, 255, 0.35)) 35%, transparent);
333
+ }
334
+
335
+ .about-picture__img {
336
+ border-radius: inherit;
337
+ pointer-events: none;
338
+ }
339
+
340
+ .about-picture .responsive-picture-placeholder {
341
+ pointer-events: none;
342
+ }
343
+
344
+ .about-heading,
345
+ .about-heading.mb-4 {
346
+ color: var(--ui-text-primary, var(--brand-fg-100));
347
+ font-family: var(--ui-font-heading, 'Space Grotesk', 'Inter', sans-serif);
348
+ font-weight: 600;
349
+ letter-spacing: 0.04em;
350
+ line-height: 1.2;
351
+ font-size: clamp(1.5rem, 3vw, 2.375rem);
352
+ max-width: 100%;
353
+ overflow-wrap: anywhere;
354
+ margin: 0;
355
+ }
356
+
357
+ .about-copy {
358
+ color: var(--ui-text-muted, var(--brand-fg-300));
359
+ font-family: var(--ui-font-body, 'Inter', sans-serif);
360
+ font-size: 1rem;
361
+ line-height: 1.7;
362
+ margin-bottom: 0;
363
+ }
364
+
365
+ .about-text-wrapper {
366
+ position: relative;
367
+ max-width: clamp(340px, 36vw, 460px);
368
+ }
369
+
370
+ .about-text-wrapper--raised {
371
+ margin-top: -40px;
372
+ }
373
+
374
+ @media (min-width: 1024px) {
375
+ .about-text-wrapper--raised {
376
+ margin-top: -80px;
377
+ }
378
+ }
379
+
380
+ @media (max-width: 768px) {
381
+ .about-text-wrapper--raised {
382
+ margin-top: 0;
383
+ }
384
+ }
385
+
386
+ .about-text-wrapper--top,
387
+ .about-text-wrapper--bottom {
388
+ margin-left: 0;
389
+ margin-right: 0;
390
+ }
391
+
392
+ @media (min-width: 1024px) {
393
+ .about-text-wrapper--top {
394
+ margin-left: clamp(-140px, -9vw, -56px);
395
+ }
396
+
397
+ .about-slice__grid--reversed .about-text-wrapper--bottom {
398
+ margin-right: clamp(-120px, -8vw, -48px);
399
+ }
400
+ }
401
+
402
+ .about-text-block {
403
+ position: relative;
404
+ z-index: 1;
405
+ color: inherit;
406
+ margin-bottom: var(--ui-space-24, 24px);
407
+ }
408
+
409
+ .brand-card--translucent {
410
+ background: color-mix(in srgb, var(--brand-surface-card-bg, rgba(20, 18, 30, 0.4)) 65%, transparent);
411
+ backdrop-filter: blur(14px);
412
+ box-shadow:
413
+ 0 18px 42px color-mix(in srgb, var(--brand-surface-card-shadow, rgba(5, 5, 10, 0.45)) 65%, transparent),
414
+ inset 0 1px 0 color-mix(in srgb, rgba(255, 255, 255, 0.8) 40%, transparent);
415
+ }
416
+
417
+ .about-cta {
418
+ gap: var(--ui-gap-tight, 8px);
419
+ display: inline-flex;
420
+ align-items: center;
421
+ position: relative;
422
+ z-index: 6;
423
+ }
424
+
425
+ .about-slice--bottom :is(.about-cta, a, button, input, textarea, select) {
426
+ pointer-events: auto;
427
+ }
428
+
429
+ .about-cta__label {
430
+ letter-spacing: inherit;
431
+ text-transform: inherit;
432
+ }
433
+
434
+ .about-cta__icon {
435
+ display: inline-flex;
436
+ align-items: center;
437
+ justify-content: center;
438
+ width: 24px;
439
+ height: 24px;
440
+ border-radius: 999px;
441
+ background: var(
442
+ --about-cta-icon-bg,
443
+ var(--brand-icon-badge-bg, var(--brand-accent-electric, #243a80))
444
+ );
445
+ color: var(--about-cta-icon-color, var(--brand-cta-text, #ffffff));
446
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 35%, transparent);
447
+ font-size: 0.75rem;
448
+ }
449
+
450
+ .about-cta__icon svg {
451
+ width: 14px;
452
+ height: 14px;
453
+ }
454
+
455
+ .about-slice--top .about-cta {
456
+ margin-bottom: clamp(20px, 4vw, 80px);
457
+ }
458
+
459
+ .about-slice__content .about-text-wrapper--raised {
460
+ transition: margin-top 0.2s ease;
461
+ }
462
+
463
+ @media (max-width: 480px) {
464
+ .about-slice--top .about-cta {
465
+ margin-bottom: 44px;
466
+ }
467
+
468
+ .about-slice__content .about-text-wrapper--raised {
469
+ margin-top: -50px;
470
+ }
471
+
472
+ .about-slice--bottom .about-text-wrapper--raised {
473
+ margin-top: -60px;
474
+ }
475
+
476
+ .about-slice__media {
477
+ margin-bottom: -8px;
478
+ }
479
+ }
480
+
481
+ </style>