@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.
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/bin/cms-generate-public-assets.js +27 -0
- package/bin/cms-validate-extensions.js +18 -0
- package/bin/cms-validate-themes.js +7 -0
- package/extensions/manifest.schema.json +125 -0
- package/package.json +88 -0
- package/public/img/preloaders/preloader-black.svg +1 -0
- package/public/img/preloaders/preloader-white.svg +1 -0
- package/public/robots.txt +5 -0
- package/scripts/check-overflow.mjs +33 -0
- package/scripts/generate-public-assets.js +412 -0
- package/scripts/migrate-to-content-dirs.mjs +223 -0
- package/scripts/patch-lru-cache-tla.js +164 -0
- package/scripts/validate-extensions.mjs +392 -0
- package/scripts/validate-themes.mjs +64 -0
- package/src/App.vue +3 -0
- package/src/components/About.vue +481 -0
- package/src/components/AboutValue.vue +361 -0
- package/src/components/BackToTop.vue +42 -0
- package/src/components/ComingSoon.vue +411 -0
- package/src/components/ComingSoonModal.vue +230 -0
- package/src/components/Contact.vue +518 -0
- package/src/components/Footer.vue +65 -0
- package/src/components/FooterMinimal.vue +153 -0
- package/src/components/Header.vue +583 -0
- package/src/components/Hero.vue +327 -0
- package/src/components/Home.vue +161 -0
- package/src/components/Intro.vue +130 -0
- package/src/components/IntroGate.vue +444 -0
- package/src/components/Plan.vue +116 -0
- package/src/components/Portfolio.vue +459 -0
- package/src/components/Preloader.vue +20 -0
- package/src/components/Principles.vue +67 -0
- package/src/components/Spacer15.vue +9 -0
- package/src/components/Spacer30.vue +9 -0
- package/src/components/Spacer40.vue +9 -0
- package/src/components/Spacer60.vue +9 -0
- package/src/components/StickyCTA.vue +263 -0
- package/src/components/Team.vue +432 -0
- package/src/components/icons/IconLinkedIn.vue +22 -0
- package/src/components/icons/IconX.vue +22 -0
- package/src/components/ui/SbCard.vue +52 -0
- package/src/components/ui/SkeletonPulse.vue +117 -0
- package/src/components/ui/UnitChip.vue +69 -0
- package/src/composables/useComingSoonConfig.js +120 -0
- package/src/composables/useComingSoonInterstitial.js +27 -0
- package/src/composables/useComponentResolver.js +235 -0
- package/src/composables/useEngagementTracking.js +187 -0
- package/src/composables/useIntroGate.js +46 -0
- package/src/composables/useLazyImage.js +77 -0
- package/src/composables/usePageConfig.js +184 -0
- package/src/composables/usePageMeta.js +76 -0
- package/src/composables/usePromoBackgroundStyles.js +67 -0
- package/src/constants/locales.js +20 -0
- package/src/extensions/extensionLoader.js +512 -0
- package/src/main.js +187 -0
- package/src/router/index.js +112 -0
- package/src/styles/base.css +896 -0
- package/src/styles/layout.css +342 -0
- package/src/styles/theme-base.css +84 -0
- package/src/themes/buildCssVarMap.js +246 -0
- package/src/themes/themeLoader.js +156 -0
- package/src/themes/themeManager.js +34 -0
- package/src/themes/themeValidator.js +380 -0
- package/src/utils/analytics.js +100 -0
- package/src/utils/appInfo.js +9 -0
- package/src/utils/assetResolver.js +162 -0
- package/src/utils/componentRegistry.js +146 -0
- package/src/utils/contentRequirements.js +67 -0
- package/src/utils/cookieConsent.js +281 -0
- package/src/utils/ctaCopy.js +58 -0
- package/src/utils/formatNumber.js +115 -0
- package/src/utils/imageSources.js +179 -0
- package/src/utils/inflateFlatConfig.js +30 -0
- package/src/utils/loadConfig.js +319 -0
- package/src/utils/semver.js +49 -0
- package/src/utils/siteStyles.js +40 -0
- package/src/utils/themeColors.js +65 -0
- package/src/utils/trackingContext.js +142 -0
- package/src/utils/unwrapDefault.js +14 -0
- package/src/utils/useScrollReveal.js +48 -0
- package/templates/index.html +36 -0
- package/themes/base/README.md +23 -0
- package/themes/base/theme.config.js +214 -0
- package/vite-plugin.js +726 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section id="about" class="about-value-section section-shell" data-analytics-section="about-value">
|
|
3
|
+
<div v-if="hasValueProps" class="container">
|
|
4
|
+
<div class="about-value__grid">
|
|
5
|
+
<div class="about-value__media">
|
|
6
|
+
<div
|
|
7
|
+
ref="topImageTarget"
|
|
8
|
+
class="responsive-picture-shell rounded value-picture"
|
|
9
|
+
style="aspect-ratio: 2 / 1;"
|
|
10
|
+
>
|
|
11
|
+
<picture v-if="isTopImageVisible" class="responsive-picture">
|
|
12
|
+
<source
|
|
13
|
+
v-for="source in topImageSources"
|
|
14
|
+
:key="source.type"
|
|
15
|
+
:type="source.type"
|
|
16
|
+
:srcset="source.srcset"
|
|
17
|
+
:sizes="topImageSizes"
|
|
18
|
+
/>
|
|
19
|
+
<img
|
|
20
|
+
:src="topImageFallback"
|
|
21
|
+
:srcset="topImageFallbackSet || undefined"
|
|
22
|
+
:sizes="topImageSizes"
|
|
23
|
+
:alt="valueImageAlt"
|
|
24
|
+
@error="topImageHandleError"
|
|
25
|
+
width="900"
|
|
26
|
+
height="450"
|
|
27
|
+
loading="lazy"
|
|
28
|
+
decoding="async"
|
|
29
|
+
class="responsive-picture__img rounded"
|
|
30
|
+
/>
|
|
31
|
+
</picture>
|
|
32
|
+
<div
|
|
33
|
+
v-else
|
|
34
|
+
class="responsive-picture-placeholder rounded"
|
|
35
|
+
:style="topPlaceholder ? { backgroundImage: `url(${topPlaceholder})` } : null"
|
|
36
|
+
></div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="about-value__content">
|
|
40
|
+
<div class="value-copy brand-card">
|
|
41
|
+
<span v-if="valueBadge" class="value-badge text-uppercase">{{ valueBadge }}</span>
|
|
42
|
+
<h2 v-if="valueHeading" class="value-heading">
|
|
43
|
+
{{ valueHeading }}
|
|
44
|
+
</h2>
|
|
45
|
+
<p v-if="valueIntro" class="value-intro">
|
|
46
|
+
{{ valueIntro }}
|
|
47
|
+
</p>
|
|
48
|
+
<p v-if="valueSecondary" class="value-secondary">
|
|
49
|
+
{{ valueSecondary }}
|
|
50
|
+
</p>
|
|
51
|
+
<ul v-if="valueBenefits.length" class="value-benefits">
|
|
52
|
+
<li
|
|
53
|
+
v-for="(benefit, idx) in valueBenefits"
|
|
54
|
+
:key="idx"
|
|
55
|
+
class="value-benefit brand-card"
|
|
56
|
+
>
|
|
57
|
+
<span
|
|
58
|
+
v-if="benefit.icon"
|
|
59
|
+
class="value-benefit-icon"
|
|
60
|
+
aria-hidden="true"
|
|
61
|
+
>
|
|
62
|
+
{{ benefit.icon }}
|
|
63
|
+
</span>
|
|
64
|
+
<span class="value-benefit-text">
|
|
65
|
+
{{ benefit.text }}
|
|
66
|
+
</span>
|
|
67
|
+
</li>
|
|
68
|
+
</ul>
|
|
69
|
+
<div class="value-actions" v-if="hasValueActions">
|
|
70
|
+
<a
|
|
71
|
+
v-if="hasValueCta"
|
|
72
|
+
class="primary-button value-cta"
|
|
73
|
+
:href="valueCta.href"
|
|
74
|
+
>
|
|
75
|
+
<span class="value-cta__label">{{ valueCta.text }}</span>
|
|
76
|
+
</a>
|
|
77
|
+
<a
|
|
78
|
+
v-if="valueDisclaimer.text && valueDisclaimer.href"
|
|
79
|
+
class="value-disclaimer-link"
|
|
80
|
+
:href="valueDisclaimer.href"
|
|
81
|
+
>
|
|
82
|
+
{{ valueDisclaimer.text }}
|
|
83
|
+
</a>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<script setup>
|
|
93
|
+
import { computed, inject, ref } from 'vue';
|
|
94
|
+
import { useResponsiveImage } from '../utils/imageSources.js';
|
|
95
|
+
import { useLazyImage } from '../composables/useLazyImage.js';
|
|
96
|
+
|
|
97
|
+
const injectedSiteData = inject('siteData', ref({}));
|
|
98
|
+
const pageContent = inject('pageContent', ref({}));
|
|
99
|
+
|
|
100
|
+
const aboutContent = computed(() => pageContent.value?.aboutValue || {});
|
|
101
|
+
|
|
102
|
+
const siteName = computed(() => injectedSiteData.value?.site?.title || '');
|
|
103
|
+
|
|
104
|
+
const getValueContent = (key, fallback) => {
|
|
105
|
+
const value = aboutContent.value?.[key];
|
|
106
|
+
return value === undefined || value === null ? fallback : value;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const valueBadge = computed(() => getValueContent('valueBadge', ''));
|
|
110
|
+
const valueHeading = computed(() => getValueContent('valueHeading', ''));
|
|
111
|
+
const valueIntro = computed(() => getValueContent('valueIntro', ''));
|
|
112
|
+
const valueSecondary = computed(() => getValueContent('valueSecondary', ''));
|
|
113
|
+
const valueBenefits = computed(() => {
|
|
114
|
+
const benefits = getValueContent('valueBullets', []);
|
|
115
|
+
if (!Array.isArray(benefits)) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
return benefits
|
|
119
|
+
.map((benefit) => {
|
|
120
|
+
if (!benefit || typeof benefit !== 'object') return null;
|
|
121
|
+
const text = typeof benefit.text === 'string' ? benefit.text.trim() : '';
|
|
122
|
+
const icon = typeof benefit.icon === 'string' ? benefit.icon.trim() : '';
|
|
123
|
+
if (!text) return null;
|
|
124
|
+
return { text, icon };
|
|
125
|
+
})
|
|
126
|
+
.filter(Boolean);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const valueDisclaimer = computed(() => {
|
|
130
|
+
const disclaimer = getValueContent('disclaimer', null);
|
|
131
|
+
const text = disclaimer && typeof disclaimer.text === 'string' ? disclaimer.text.trim() : '';
|
|
132
|
+
const href = disclaimer && typeof disclaimer.href === 'string' ? disclaimer.href.trim() : '';
|
|
133
|
+
return { text, href };
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const valueCta = computed(() => {
|
|
137
|
+
const cta = getValueContent('valueCta', null);
|
|
138
|
+
const text = cta && typeof cta.text === 'string' ? cta.text.trim() : '';
|
|
139
|
+
const href = cta && typeof cta.href === 'string' ? cta.href.trim() : '';
|
|
140
|
+
return { text, href };
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const valueImageAlt = computed(() => {
|
|
144
|
+
const alt = getValueContent('valueImageAlt', '');
|
|
145
|
+
if (typeof alt === 'string' && alt.trim()) {
|
|
146
|
+
return alt.trim();
|
|
147
|
+
}
|
|
148
|
+
return siteName.value || '';
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const hasValueCta = computed(() => Boolean(valueCta.value.text && valueCta.value.href));
|
|
152
|
+
const hasValueDisclaimer = computed(() =>
|
|
153
|
+
Boolean(valueDisclaimer.value.text && valueDisclaimer.value.href)
|
|
154
|
+
);
|
|
155
|
+
const hasValueActions = computed(() => hasValueCta.value || hasValueDisclaimer.value);
|
|
156
|
+
|
|
157
|
+
const hasValueProps = computed(() => {
|
|
158
|
+
return Boolean(valueIntro.value && valueBenefits.value.length > 0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const DEFAULT_VALUE_IMAGE_PATH = 'img/about-top';
|
|
162
|
+
const DEFAULT_VALUE_IMAGE_WIDTHS = [320, 900];
|
|
163
|
+
const DEFAULT_VALUE_IMAGE_FALLBACK = 'jpg';
|
|
164
|
+
|
|
165
|
+
const valueImagePath = computed(() => {
|
|
166
|
+
const raw = getValueContent('valueImage', null);
|
|
167
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
168
|
+
return raw.trim();
|
|
169
|
+
}
|
|
170
|
+
if (raw && typeof raw === 'object' && typeof raw.path === 'string' && raw.path.trim()) {
|
|
171
|
+
return raw.path.trim();
|
|
172
|
+
}
|
|
173
|
+
return DEFAULT_VALUE_IMAGE_PATH;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const valueImageWidths = computed(() => {
|
|
177
|
+
const raw = getValueContent('valueImage', null);
|
|
178
|
+
const widths = raw && typeof raw === 'object' ? raw.widths : null;
|
|
179
|
+
if (Array.isArray(widths) && widths.length) {
|
|
180
|
+
return widths;
|
|
181
|
+
}
|
|
182
|
+
return DEFAULT_VALUE_IMAGE_WIDTHS;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const valueImageFallbackFormat = computed(() => {
|
|
186
|
+
const raw = getValueContent('valueImage', null);
|
|
187
|
+
const format = raw && typeof raw === 'object' ? raw.fallbackFormat : null;
|
|
188
|
+
if (typeof format === 'string' && format.trim()) {
|
|
189
|
+
return format.trim();
|
|
190
|
+
}
|
|
191
|
+
return DEFAULT_VALUE_IMAGE_FALLBACK;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const topImage = useResponsiveImage(valueImagePath, {
|
|
195
|
+
widths: valueImageWidths,
|
|
196
|
+
fallbackFormat: valueImageFallbackFormat,
|
|
197
|
+
});
|
|
198
|
+
const topImageHandleError = topImage.handleError;
|
|
199
|
+
|
|
200
|
+
const topImageSizes = '(min-width: 1200px) 66vw, (min-width: 992px) 60vw, 100vw';
|
|
201
|
+
|
|
202
|
+
const { isVisible: isTopImageVisible, targetRef: topImageTarget } = useLazyImage({
|
|
203
|
+
rootMargin: '250px 0px',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const topImageSources = topImage.sources;
|
|
207
|
+
const topImageFallback = topImage.fallbackSrc;
|
|
208
|
+
const topImageFallbackSet = topImage.fallbackSrcSet;
|
|
209
|
+
const topPlaceholder = topImage.placeholderSrc;
|
|
210
|
+
</script>
|
|
211
|
+
|
|
212
|
+
<style scoped>
|
|
213
|
+
.about-value__grid {
|
|
214
|
+
display: flex;
|
|
215
|
+
flex-direction: column;
|
|
216
|
+
gap: var(--ui-space-32, 32px);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@media (min-width: 1024px) {
|
|
220
|
+
.about-value__grid {
|
|
221
|
+
flex-direction: row;
|
|
222
|
+
align-items: center;
|
|
223
|
+
gap: clamp(32px, 6vw, 80px);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.about-value__media,
|
|
228
|
+
.about-value__content {
|
|
229
|
+
flex: 1 1 0;
|
|
230
|
+
min-width: 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.about-value__content {
|
|
234
|
+
display: flex;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.value-picture {
|
|
238
|
+
--responsive-picture-surface: color-mix(
|
|
239
|
+
in srgb,
|
|
240
|
+
var(--brand-surface-card-bg, rgba(10, 10, 13, 0.94)) 90%,
|
|
241
|
+
transparent
|
|
242
|
+
);
|
|
243
|
+
--responsive-picture-placeholder: linear-gradient(
|
|
244
|
+
135deg,
|
|
245
|
+
color-mix(in srgb, var(--brand-surface-card-bg, rgba(20, 18, 22, 0.55)) 80%, transparent),
|
|
246
|
+
color-mix(in srgb, var(--brand-surface-card-bg, rgba(10, 10, 13, 0.75)) 65%, transparent)
|
|
247
|
+
);
|
|
248
|
+
box-shadow: 0 25px 60px
|
|
249
|
+
color-mix(in srgb, var(--brand-surface-card-shadow, rgba(12, 9, 26, 0.45)) 100%, transparent);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.value-copy {
|
|
253
|
+
padding: clamp(24px, 4vw, 36px);
|
|
254
|
+
color: var(--ui-text-primary, var(--brand-card-text));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.value-badge {
|
|
258
|
+
display: inline-flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
padding: 6px 14px;
|
|
261
|
+
border-radius: 999px;
|
|
262
|
+
letter-spacing: 0.12em;
|
|
263
|
+
font-size: 11px;
|
|
264
|
+
font-weight: 600;
|
|
265
|
+
background: color-mix(in srgb, var(--brand-accent-electric, #ffffff) 15%, transparent);
|
|
266
|
+
color: color-mix(in srgb, var(--ui-text-primary, #ffffff) 78%, transparent);
|
|
267
|
+
margin-bottom: 18px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.value-heading {
|
|
271
|
+
margin-bottom: 16px;
|
|
272
|
+
color: var(--ui-text-primary, rgba(255, 255, 255, 0.96));
|
|
273
|
+
font-size: clamp(1.75rem, 4vw, 2.4rem);
|
|
274
|
+
font-weight: 600;
|
|
275
|
+
line-height: 1.2;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.value-intro,
|
|
279
|
+
.value-secondary {
|
|
280
|
+
font-size: 16px;
|
|
281
|
+
line-height: 1.6;
|
|
282
|
+
margin-bottom: 18px;
|
|
283
|
+
color: var(--ui-text-muted, rgba(255, 255, 255, 0.88));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.value-benefits {
|
|
287
|
+
list-style: none;
|
|
288
|
+
margin: 0 0 24px;
|
|
289
|
+
padding: 0;
|
|
290
|
+
display: grid;
|
|
291
|
+
gap: 12px;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.value-benefit {
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: flex-start;
|
|
297
|
+
gap: 12px;
|
|
298
|
+
padding: 12px 16px;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.value-benefit-icon {
|
|
302
|
+
font-size: 18px;
|
|
303
|
+
line-height: 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.value-benefit-text {
|
|
307
|
+
font-size: 15px;
|
|
308
|
+
line-height: 1.5;
|
|
309
|
+
color: var(--ui-text-primary, rgba(255, 255, 255, 0.92));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.value-actions {
|
|
313
|
+
display: flex;
|
|
314
|
+
flex-wrap: wrap;
|
|
315
|
+
gap: 18px;
|
|
316
|
+
align-items: center;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.value-cta {
|
|
320
|
+
padding-inline: 32px;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.value-cta__label {
|
|
324
|
+
color: #000;
|
|
325
|
+
font-weight: 700;
|
|
326
|
+
letter-spacing: 0.1em;
|
|
327
|
+
text-transform: uppercase;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.value-cta:focus-visible .value-cta__label,
|
|
331
|
+
.value-cta:hover .value-cta__label {
|
|
332
|
+
color: #000;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.value-disclaimer-link {
|
|
336
|
+
font-size: 13px;
|
|
337
|
+
font-weight: 600;
|
|
338
|
+
letter-spacing: 0.05em;
|
|
339
|
+
text-transform: uppercase;
|
|
340
|
+
color: var(--ui-text-muted, var(--trust-accent-primary));
|
|
341
|
+
text-decoration: underline;
|
|
342
|
+
transition: color 0.2s ease;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.value-disclaimer-link:hover {
|
|
346
|
+
color: var(--trust-accent-secondary, #ff2d86);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
@media (max-width: 991px) {
|
|
350
|
+
.about-value__content {
|
|
351
|
+
justify-content: center;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
@media (max-width: 575px) {
|
|
356
|
+
.value-benefit {
|
|
357
|
+
flex-direction: row;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
type="button"
|
|
4
|
+
class="ui-backtotop"
|
|
5
|
+
:class="{ 'ui-backtotop--visible': isVisible }"
|
|
6
|
+
aria-label="Back to top"
|
|
7
|
+
@click="scrollToTop"
|
|
8
|
+
>
|
|
9
|
+
<span class="ui-backtotop__icon" aria-hidden="true">↑</span>
|
|
10
|
+
</button>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup>
|
|
14
|
+
import { onMounted, onUnmounted, ref } from 'vue';
|
|
15
|
+
|
|
16
|
+
const isVisible = ref(false);
|
|
17
|
+
|
|
18
|
+
const handleScroll = () => {
|
|
19
|
+
if (typeof window === 'undefined') return;
|
|
20
|
+
isVisible.value = window.scrollY > 400;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const scrollToTop = () => {
|
|
24
|
+
if (typeof window === 'undefined') return;
|
|
25
|
+
const prefersReducedMotion =
|
|
26
|
+
window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
27
|
+
|
|
28
|
+
window.scrollTo({
|
|
29
|
+
top: 0,
|
|
30
|
+
behavior: prefersReducedMotion ? 'auto' : 'smooth',
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
onMounted(() => {
|
|
35
|
+
handleScroll();
|
|
36
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
onUnmounted(() => {
|
|
40
|
+
window.removeEventListener('scroll', handleScroll);
|
|
41
|
+
});
|
|
42
|
+
</script>
|