@xenterprises/nuxt-x-marketing 1.5.0 → 1.5.2
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/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
30
30
|
section and its sample product data were dropped with them. Every other section of the
|
|
31
31
|
demo page is unchanged.
|
|
32
32
|
|
|
33
|
+
## [1.5.2] - 2026-10-12
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **Scroll reveal uses one long-lived shared IntersectionObserver.** It is created once
|
|
38
|
+
and never disconnected or rebuilt; plugin hooks (`app:mounted`, `page:finish`) and
|
|
39
|
+
components only scan for new reveal elements and observe them. Removes the observer
|
|
40
|
+
churn that left elements unrevealed when IO callbacks did not fire in time. In-viewport
|
|
41
|
+
elements are still marked visible synchronously, and the fallback timer remains.
|
|
42
|
+
XMarkHero default look is unchanged from 1.5.1 (text visible by default, new props opt-in).
|
|
43
|
+
|
|
44
|
+
## [1.5.1] - 2026-10-12
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- Scroll reveal content (`.xFadeUp`, `.xFadeIn`, `.xFadeLeft`, `.xFadeRight`, `.xScale`, `.xFadeUp-stagger`, `[data-reveal]`) could stay invisible on prerendered sites and in headless Chrome. Reveal is now progressive enhancement only:
|
|
49
|
+
- Content is visible by default. Hidden initial states apply only under `html.x-reveal-armed`, which the client plugin sets after JS runs, so SSR, prerendered and no-JS output always shows content.
|
|
50
|
+
- `prefers-reduced-motion: reduce` never hides and never animates.
|
|
51
|
+
- Elements already in the viewport are marked visible synchronously (getBoundingClientRect) before arming, so above-the-fold content never flashes hidden.
|
|
52
|
+
- Without IntersectionObserver, everything is shown. If IO never fires, on-screen elements are revealed after 1.5s.
|
|
53
|
+
- The plugin now initializes on `app:mounted` (initial hydration) as well as `page:finish` (navigations).
|
|
54
|
+
- One observer app-wide: repeated `initScrollReveal()` calls, including an `app.vue` workaround, replace the previous observer instead of stacking.
|
|
55
|
+
- `XMarkHero` eyebrow, title, subtitle and buttons no longer use reveal classes, so hero text is never gated on JS.
|
|
56
|
+
|
|
33
57
|
## [1.5.0] - 2026-10-12
|
|
34
58
|
|
|
35
59
|
### Added
|
|
@@ -103,88 +103,93 @@
|
|
|
103
103
|
* X MARKETING SCROLL ANIMATIONS
|
|
104
104
|
* ============================================ */
|
|
105
105
|
|
|
106
|
-
/*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
transition:
|
|
119
|
-
opacity 700ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
120
|
-
transform 700ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
121
|
-
box-shadow 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/* Stagger children */
|
|
125
|
-
.xFadeUp-stagger > * {
|
|
126
|
-
opacity: 0;
|
|
127
|
-
transform: translateY(24px);
|
|
128
|
-
transition:
|
|
129
|
-
opacity 700ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
130
|
-
transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
.xFadeUp-stagger.is-visible > *:nth-child(1) { transition-delay: 0ms; opacity: 1; transform: translateY(0); }
|
|
134
|
-
.xFadeUp-stagger.is-visible > *:nth-child(2) { transition-delay: 100ms; opacity: 1; transform: translateY(0); }
|
|
135
|
-
.xFadeUp-stagger.is-visible > *:nth-child(3) { transition-delay: 200ms; opacity: 1; transform: translateY(0); }
|
|
136
|
-
.xFadeUp-stagger.is-visible > *:nth-child(4) { transition-delay: 300ms; opacity: 1; transform: translateY(0); }
|
|
137
|
-
.xFadeUp-stagger.is-visible > *:nth-child(5) { transition-delay: 400ms; opacity: 1; transform: translateY(0); }
|
|
138
|
-
.xFadeUp-stagger.is-visible > *:nth-child(6) { transition-delay: 500ms; opacity: 1; transform: translateY(0); }
|
|
139
|
-
|
|
140
|
-
/* xFadeIn - Simple fade */
|
|
141
|
-
.xFadeIn {
|
|
142
|
-
opacity: 0;
|
|
143
|
-
transition: opacity 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
144
|
-
}
|
|
145
|
-
.xFadeIn.is-visible { opacity: 1; }
|
|
146
|
-
|
|
147
|
-
/* xFadeLeft - Fade from left */
|
|
148
|
-
.xFadeLeft {
|
|
149
|
-
opacity: 0;
|
|
150
|
-
transform: translateX(-24px);
|
|
151
|
-
transition:
|
|
152
|
-
opacity 700ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
153
|
-
transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
154
|
-
}
|
|
155
|
-
.xFadeLeft.is-visible { opacity: 1; transform: translateX(0); }
|
|
106
|
+
/* Reveal is progressive enhancement: hidden states apply only after the
|
|
107
|
+
* client plugin sets html.x-reveal-armed, and never with reduced motion.
|
|
108
|
+
* SSR, prerendered and no-JS output always shows content. */
|
|
109
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
110
|
+
/* xFadeUp - Global fade up on scroll */
|
|
111
|
+
html.x-reveal-armed .xFadeUp {
|
|
112
|
+
opacity: 0;
|
|
113
|
+
transform: translateY(24px);
|
|
114
|
+
transition:
|
|
115
|
+
opacity 700ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
116
|
+
transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
117
|
+
}
|
|
156
118
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
.xFadeRight.is-visible { opacity: 1; transform: translateX(0); }
|
|
119
|
+
html.x-reveal-armed .xFadeUp.is-visible {
|
|
120
|
+
opacity: 1;
|
|
121
|
+
transform: translateY(0);
|
|
122
|
+
transition:
|
|
123
|
+
opacity 700ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
124
|
+
transform 700ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
125
|
+
box-shadow 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
126
|
+
}
|
|
166
127
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
.xScale.is-visible { opacity: 1; transform: scale(1); }
|
|
128
|
+
/* Stagger children */
|
|
129
|
+
html.x-reveal-armed .xFadeUp-stagger > * {
|
|
130
|
+
opacity: 0;
|
|
131
|
+
transform: translateY(24px);
|
|
132
|
+
transition:
|
|
133
|
+
opacity 700ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
134
|
+
transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
135
|
+
}
|
|
176
136
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
opacity: 0;
|
|
180
|
-
transform: translateY(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
137
|
+
html.x-reveal-armed .xFadeUp-stagger.is-visible > *:nth-child(1) { transition-delay: 0ms; opacity: 1; transform: translateY(0); }
|
|
138
|
+
html.x-reveal-armed .xFadeUp-stagger.is-visible > *:nth-child(2) { transition-delay: 100ms; opacity: 1; transform: translateY(0); }
|
|
139
|
+
html.x-reveal-armed .xFadeUp-stagger.is-visible > *:nth-child(3) { transition-delay: 200ms; opacity: 1; transform: translateY(0); }
|
|
140
|
+
html.x-reveal-armed .xFadeUp-stagger.is-visible > *:nth-child(4) { transition-delay: 300ms; opacity: 1; transform: translateY(0); }
|
|
141
|
+
html.x-reveal-armed .xFadeUp-stagger.is-visible > *:nth-child(5) { transition-delay: 400ms; opacity: 1; transform: translateY(0); }
|
|
142
|
+
html.x-reveal-armed .xFadeUp-stagger.is-visible > *:nth-child(6) { transition-delay: 500ms; opacity: 1; transform: translateY(0); }
|
|
143
|
+
|
|
144
|
+
/* xFadeIn - Simple fade */
|
|
145
|
+
html.x-reveal-armed .xFadeIn {
|
|
146
|
+
opacity: 0;
|
|
147
|
+
transition: opacity 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
148
|
+
}
|
|
149
|
+
html.x-reveal-armed .xFadeIn.is-visible { opacity: 1; }
|
|
150
|
+
|
|
151
|
+
/* xFadeLeft - Fade from left */
|
|
152
|
+
html.x-reveal-armed .xFadeLeft {
|
|
153
|
+
opacity: 0;
|
|
154
|
+
transform: translateX(-24px);
|
|
155
|
+
transition:
|
|
156
|
+
opacity 700ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
157
|
+
transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
158
|
+
}
|
|
159
|
+
html.x-reveal-armed .xFadeLeft.is-visible { opacity: 1; transform: translateX(0); }
|
|
160
|
+
|
|
161
|
+
/* xFadeRight - Fade from right */
|
|
162
|
+
html.x-reveal-armed .xFadeRight {
|
|
163
|
+
opacity: 0;
|
|
164
|
+
transform: translateX(24px);
|
|
165
|
+
transition:
|
|
166
|
+
opacity 700ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
167
|
+
transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
168
|
+
}
|
|
169
|
+
html.x-reveal-armed .xFadeRight.is-visible { opacity: 1; transform: translateX(0); }
|
|
170
|
+
|
|
171
|
+
/* xScale - Scale in */
|
|
172
|
+
html.x-reveal-armed .xScale {
|
|
173
|
+
opacity: 0;
|
|
174
|
+
transform: scale(0.95);
|
|
175
|
+
transition:
|
|
176
|
+
opacity 700ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
177
|
+
transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
178
|
+
}
|
|
179
|
+
html.x-reveal-armed .xScale.is-visible { opacity: 1; transform: scale(1); }
|
|
180
|
+
|
|
181
|
+
/* Legacy reveal (kept for compatibility) */
|
|
182
|
+
html.x-reveal-armed [data-reveal] {
|
|
183
|
+
opacity: 0;
|
|
184
|
+
transform: translateY(40px);
|
|
185
|
+
transition: opacity 0.4s cubic-bezier(0.25, 1, 0.5, 1),
|
|
186
|
+
transform 0.4s cubic-bezier(0.25, 1, 0.5, 1);
|
|
187
|
+
}
|
|
184
188
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
189
|
+
html.x-reveal-armed [data-reveal].is-visible {
|
|
190
|
+
opacity: 1;
|
|
191
|
+
transform: translateY(0);
|
|
192
|
+
}
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
/* ============================================
|
|
@@ -50,14 +50,13 @@
|
|
|
50
50
|
<div class="relative z-10 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
51
51
|
<div class="max-w-3xl pb-6 md:pb-8" :class="contentAlignmentClasses">
|
|
52
52
|
<!-- Eyebrow -->
|
|
53
|
-
<p v-if="eyebrow" class="xText-eyebrow text-white/70 mb-4
|
|
53
|
+
<p v-if="eyebrow" class="xText-eyebrow text-white/70 mb-4">
|
|
54
54
|
{{ eyebrow }}
|
|
55
55
|
</p>
|
|
56
56
|
|
|
57
57
|
<!-- Title -->
|
|
58
58
|
<h1
|
|
59
|
-
class="xText-display text-white xText-balance
|
|
60
|
-
style="transition-delay: 100ms"
|
|
59
|
+
class="xText-display text-white xText-balance"
|
|
61
60
|
>
|
|
62
61
|
<slot name="title">{{ title }}</slot>
|
|
63
62
|
</h1>
|
|
@@ -65,8 +64,7 @@
|
|
|
65
64
|
<!-- Subtitle -->
|
|
66
65
|
<p
|
|
67
66
|
v-if="subtitle || $slots.subtitle"
|
|
68
|
-
class="mt-6 text-xl text-white/80 max-w-2xl
|
|
69
|
-
style="transition-delay: 200ms"
|
|
67
|
+
class="mt-6 text-xl text-white/80 max-w-2xl"
|
|
70
68
|
>
|
|
71
69
|
<slot name="subtitle">{{ subtitle }}</slot>
|
|
72
70
|
</p>
|
|
@@ -74,12 +72,11 @@
|
|
|
74
72
|
<!-- Actions -->
|
|
75
73
|
<div
|
|
76
74
|
v-if="buttons.length || $slots.actions"
|
|
77
|
-
class="mt-10 flex flex-wrap gap-4
|
|
75
|
+
class="mt-10 flex flex-wrap gap-4"
|
|
78
76
|
:class="{
|
|
79
77
|
'justify-center': align === 'center',
|
|
80
78
|
'justify-end': align === 'right',
|
|
81
79
|
}"
|
|
82
|
-
style="transition-delay: 300ms"
|
|
83
80
|
>
|
|
84
81
|
<slot name="actions">
|
|
85
82
|
<UButton
|
|
@@ -8,70 +8,122 @@
|
|
|
8
8
|
* - .xFadeUp, .xFadeIn, .xFadeLeft, .xFadeRight, .xScale classes
|
|
9
9
|
* - .xFadeUp-stagger for staggered child animations
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// Class set on <html> once JS has armed reveal. Hidden initial states in
|
|
12
|
+
// x-marketing.css only apply under it, so SSR, prerendered and no-JS pages
|
|
13
|
+
// always render reveal content visible (progressive enhancement).
|
|
14
|
+
export const REVEAL_ARMED_CLASS = 'x-reveal-armed';
|
|
15
|
+
export const REVEAL_FALLBACK_MS = 1500;
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const animationSelectors = [
|
|
18
|
+
'[data-reveal]',
|
|
19
|
+
'.xFadeUp',
|
|
20
|
+
'.xFadeIn',
|
|
21
|
+
'.xFadeLeft',
|
|
22
|
+
'.xFadeRight',
|
|
23
|
+
'.xScale',
|
|
24
|
+
'.xFadeUp-stagger',
|
|
25
|
+
].join(', ');
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
// One long-lived observer shared app-wide. It is created once and never
|
|
28
|
+
// rebuilt: every init (plugin hooks, components, app.vue workarounds) only
|
|
29
|
+
// scans for new elements and observes them, so there is no observer churn.
|
|
30
|
+
let sharedObserver = null;
|
|
31
|
+
let fallbackTimer = null;
|
|
32
|
+
const observed = new WeakSet();
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
'.xFadeIn',
|
|
29
|
-
'.xFadeLeft',
|
|
30
|
-
'.xFadeRight',
|
|
31
|
-
'.xScale',
|
|
32
|
-
'.xFadeUp-stagger',
|
|
33
|
-
].join(', ');
|
|
34
|
+
const prefersReducedMotion = () =>
|
|
35
|
+
typeof window !== 'undefined' &&
|
|
36
|
+
typeof window.matchMedia === 'function' &&
|
|
37
|
+
Boolean(window.matchMedia('(prefers-reduced-motion: reduce)')?.matches);
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// Re-hide if not in view and once is false
|
|
48
|
-
entry.target.classList.remove('is-visible');
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}, defaultOptions);
|
|
52
|
-
|
|
53
|
-
// Observe all animation elements
|
|
54
|
-
const elements = document.querySelectorAll(animationSelectors);
|
|
55
|
-
elements.forEach((el) => observer.value?.observe(el));
|
|
39
|
+
const inViewport = (el) => {
|
|
40
|
+
const r = el.getBoundingClientRect();
|
|
41
|
+
const vh = window.innerHeight || document.documentElement.clientHeight;
|
|
42
|
+
const vw = window.innerWidth || document.documentElement.clientWidth;
|
|
43
|
+
return r.bottom > 0 && r.right > 0 && r.top < vh && r.left < vw;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const revealInViewport = () => {
|
|
47
|
+
document.querySelectorAll(animationSelectors).forEach((el) => {
|
|
48
|
+
if (!el.classList.contains('is-visible') && inViewport(el)) {
|
|
49
|
+
el.classList.add('is-visible');
|
|
50
|
+
sharedObserver?.unobserve(el);
|
|
56
51
|
}
|
|
57
|
-
};
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const getSharedObserver = () => {
|
|
56
|
+
if (!sharedObserver) {
|
|
57
|
+
sharedObserver = new IntersectionObserver((entries) => {
|
|
58
|
+
entries.forEach((entry) => {
|
|
59
|
+
if (entry.isIntersecting) {
|
|
60
|
+
entry.target.classList.add('is-visible');
|
|
61
|
+
sharedObserver?.unobserve(entry.target);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}, { threshold: 0.15, rootMargin: '0px 0px -80px 0px' });
|
|
65
|
+
}
|
|
66
|
+
return sharedObserver;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Test-only: drop the shared observer and pending fallback.
|
|
70
|
+
export const __resetScrollRevealForTests = () => {
|
|
71
|
+
sharedObserver = null;
|
|
72
|
+
if (fallbackTimer) clearTimeout(fallbackTimer);
|
|
73
|
+
fallbackTimer = null;
|
|
74
|
+
};
|
|
58
75
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
export const useScrollReveal = () => {
|
|
77
|
+
// Lifecycle hooks are only registered inside a component instance.
|
|
78
|
+
// Outside one (e.g. a plugin) the caller drives initScrollReveal itself.
|
|
79
|
+
const instance = getCurrentInstance();
|
|
80
|
+
|
|
81
|
+
const initScrollReveal = () => {
|
|
82
|
+
if (!import.meta.client || typeof document === 'undefined') return;
|
|
83
|
+
const root = document.documentElement;
|
|
84
|
+
const elements = Array.from(document.querySelectorAll(animationSelectors));
|
|
85
|
+
|
|
86
|
+
// No IO or reduced motion: never hide, show everything, no animation.
|
|
87
|
+
if (typeof IntersectionObserver === 'undefined' || prefersReducedMotion()) {
|
|
88
|
+
root.classList.remove(REVEAL_ARMED_CLASS);
|
|
89
|
+
elements.forEach((el) => el.classList.add('is-visible'));
|
|
90
|
+
return;
|
|
63
91
|
}
|
|
92
|
+
|
|
93
|
+
// Mark what is already on screen visible synchronously, before arming,
|
|
94
|
+
// so above-the-fold content never flashes hidden.
|
|
95
|
+
elements.forEach((el) => {
|
|
96
|
+
if (!el.classList.contains('is-visible') && inViewport(el)) {
|
|
97
|
+
el.classList.add('is-visible');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
root.classList.add(REVEAL_ARMED_CLASS);
|
|
101
|
+
|
|
102
|
+
const io = getSharedObserver();
|
|
103
|
+
elements.forEach((el) => {
|
|
104
|
+
if (!el.classList.contains('is-visible') && !observed.has(el)) {
|
|
105
|
+
observed.add(el);
|
|
106
|
+
io.observe(el);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Fallback: if IO never fires, reveal anything on screen after a short wait.
|
|
111
|
+
if (fallbackTimer) clearTimeout(fallbackTimer);
|
|
112
|
+
fallbackTimer = setTimeout(() => {
|
|
113
|
+
fallbackTimer = null;
|
|
114
|
+
revealInViewport();
|
|
115
|
+
}, REVEAL_FALLBACK_MS);
|
|
64
116
|
};
|
|
65
117
|
|
|
118
|
+
// The shared observer is long-lived; per-caller cleanup is a no-op.
|
|
119
|
+
// Detached elements are released by the WeakSet and by IO itself.
|
|
120
|
+
const cleanup = () => {};
|
|
121
|
+
|
|
66
122
|
if (import.meta.client && instance) {
|
|
67
123
|
onMounted(() => {
|
|
68
124
|
// Small delay to ensure DOM is ready
|
|
69
125
|
setTimeout(initScrollReveal, 100);
|
|
70
126
|
});
|
|
71
|
-
|
|
72
|
-
onUnmounted(() => {
|
|
73
|
-
cleanup();
|
|
74
|
-
});
|
|
75
127
|
}
|
|
76
128
|
|
|
77
129
|
return {
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
* From spec-prompt.md specification
|
|
6
6
|
*/
|
|
7
7
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
8
|
-
//
|
|
9
|
-
// so drive
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
// Scroll reveal is progressive enhancement. A plugin has no component
|
|
9
|
+
// instance, so drive it from app hooks: app:mounted covers initial
|
|
10
|
+
// hydration (including prerendered pages, where page:finish can fire
|
|
11
|
+
// before mount or not at all), page:finish covers client navigations.
|
|
12
|
+
// initScrollReveal only scans and observes new elements on one shared,
|
|
13
|
+
// long-lived observer; it never tears down or rebuilds it.
|
|
14
|
+
const { initScrollReveal } = useScrollReveal();
|
|
15
|
+
const run = () => nextTick(() => initScrollReveal());
|
|
16
|
+
nuxtApp.hook('app:mounted', run);
|
|
17
|
+
nuxtApp.hook('page:finish', run);
|
|
16
18
|
|
|
17
19
|
// Initialize parallax globally
|
|
18
20
|
useParallax();
|