@xenterprises/nuxt-x-marketing 1.4.13 → 1.4.15
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,18 @@ 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.4.15] - 2026-10-12
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Scroll reveal never initialized from the global `marketing.client.ts` plugin. `useScrollReveal()` returned a no-op stub outside a component instance, so `.xFadeUp` / `.xFadeIn` / `[data-reveal]` content (including hero text) stayed at opacity 0. The composable now returns working `initScrollReveal` / `cleanup` outside components (lifecycle hooks are still only registered inside one), and the plugin runs it on `page:finish`, re-scanning on every client navigation. Consumers who added an `app.vue` workaround can remove it.
|
|
38
|
+
|
|
39
|
+
## [1.4.14] - 2026-09-06
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- `XMarkPricingPlans` `freeTrial` prop for an honest free-trial callout (default off; enable only when the product has a trial).
|
|
44
|
+
|
|
33
45
|
## [1.4.13] - 2026-09-06
|
|
34
46
|
|
|
35
47
|
### Changed
|
|
@@ -33,6 +33,26 @@
|
|
|
33
33
|
</div>
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
|
+
<!-- Free trial callout (opt-in; never invent a trial) -->
|
|
37
|
+
<div
|
|
38
|
+
v-if="trialCallout"
|
|
39
|
+
class="flex justify-center mb-8"
|
|
40
|
+
data-testid="x-mark-pricing-free-trial"
|
|
41
|
+
>
|
|
42
|
+
<div
|
|
43
|
+
class="inline-flex items-center gap-2 rounded-full border border-primary-200 bg-primary-50 px-4 py-2 text-sm text-primary-800 dark:border-primary-800 dark:bg-primary-950 dark:text-primary-200"
|
|
44
|
+
>
|
|
45
|
+
<UIcon name="i-lucide-gift" class="size-4 shrink-0" />
|
|
46
|
+
<span class="font-medium">{{ trialCallout.label }}</span>
|
|
47
|
+
<span
|
|
48
|
+
v-if="trialCallout.description"
|
|
49
|
+
class="text-primary-700/80 dark:text-primary-300/80"
|
|
50
|
+
>
|
|
51
|
+
{{ trialCallout.description }}
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
36
56
|
<!-- Badges (top position) -->
|
|
37
57
|
<div
|
|
38
58
|
v-if="badges?.length && badgesPosition === 'top'"
|
|
@@ -165,12 +185,54 @@ const props = defineProps({
|
|
|
165
185
|
default: "center",
|
|
166
186
|
validator: (v) => ["left", "center", "right"].includes(v),
|
|
167
187
|
},
|
|
188
|
+
/**
|
|
189
|
+
* Free trial callout. Default off (honest; do not invent trials).
|
|
190
|
+
* true → "Free trial available"
|
|
191
|
+
* number → "Free {n}-day trial"
|
|
192
|
+
* string → custom label
|
|
193
|
+
* { days?, label?, description? }
|
|
194
|
+
*/
|
|
195
|
+
freeTrial: {
|
|
196
|
+
type: [Boolean, Number, String, Object],
|
|
197
|
+
default: false,
|
|
198
|
+
},
|
|
168
199
|
});
|
|
169
200
|
|
|
170
201
|
const emit = defineEmits(["select"]);
|
|
171
202
|
|
|
172
203
|
const billingPeriod = ref("monthly");
|
|
173
204
|
|
|
205
|
+
const trialCallout = computed(() => {
|
|
206
|
+
const v = props.freeTrial;
|
|
207
|
+
if (v === false || v === null || v === undefined || v === "") return null;
|
|
208
|
+
if (v === true) {
|
|
209
|
+
return { label: "Free trial available", description: "" };
|
|
210
|
+
}
|
|
211
|
+
if (typeof v === "number" && Number.isFinite(v) && v > 0) {
|
|
212
|
+
const days = Math.floor(v);
|
|
213
|
+
return {
|
|
214
|
+
label: `Free ${days}-day trial`,
|
|
215
|
+
description: "",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (typeof v === "string") {
|
|
219
|
+
return { label: v, description: "" };
|
|
220
|
+
}
|
|
221
|
+
if (typeof v === "object") {
|
|
222
|
+
const days = Number(v.days);
|
|
223
|
+
const label =
|
|
224
|
+
v.label
|
|
225
|
+
|| (Number.isFinite(days) && days > 0
|
|
226
|
+
? `Free ${Math.floor(days)}-day trial`
|
|
227
|
+
: "Free trial available");
|
|
228
|
+
return {
|
|
229
|
+
label,
|
|
230
|
+
description: v.description || "",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
});
|
|
235
|
+
|
|
174
236
|
function normalizePeriod(period) {
|
|
175
237
|
if (!period) return period;
|
|
176
238
|
const trimmed = String(period).trim().toLowerCase();
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* - .xFadeUp-stagger for staggered child animations
|
|
10
10
|
*/
|
|
11
11
|
export const useScrollReveal = (options = {}) => {
|
|
12
|
-
//
|
|
12
|
+
// Lifecycle hooks are only registered inside a component instance.
|
|
13
|
+
// Outside one (e.g. a plugin) the caller drives initScrollReveal/cleanup itself.
|
|
13
14
|
const instance = getCurrentInstance();
|
|
14
|
-
if (!instance) return { initScrollReveal: () => {}, cleanup: () => {} };
|
|
15
15
|
|
|
16
16
|
const defaultOptions = {
|
|
17
17
|
threshold: 0.15,
|
|
@@ -33,7 +33,8 @@ export const useScrollReveal = (options = {}) => {
|
|
|
33
33
|
].join(', ');
|
|
34
34
|
|
|
35
35
|
const initScrollReveal = () => {
|
|
36
|
-
if (import.meta.client) {
|
|
36
|
+
if (import.meta.client && typeof IntersectionObserver !== 'undefined') {
|
|
37
|
+
if (observer.value) observer.value.disconnect();
|
|
37
38
|
observer.value = new IntersectionObserver((entries) => {
|
|
38
39
|
entries.forEach((entry) => {
|
|
39
40
|
if (entry.isIntersecting) {
|
|
@@ -62,7 +63,7 @@ export const useScrollReveal = (options = {}) => {
|
|
|
62
63
|
}
|
|
63
64
|
};
|
|
64
65
|
|
|
65
|
-
if (import.meta.client) {
|
|
66
|
+
if (import.meta.client && instance) {
|
|
66
67
|
onMounted(() => {
|
|
67
68
|
// Small delay to ensure DOM is ready
|
|
68
69
|
setTimeout(initScrollReveal, 100);
|
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
*
|
|
5
5
|
* From spec-prompt.md specification
|
|
6
6
|
*/
|
|
7
|
-
export default defineNuxtPlugin(() => {
|
|
8
|
-
// Initialize scroll reveal globally
|
|
9
|
-
|
|
7
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
8
|
+
// Initialize scroll reveal globally. A plugin has no component instance,
|
|
9
|
+
// so drive the observer from page lifecycle hooks: initial hydration and
|
|
10
|
+
// every client-side navigation re-scan the DOM for reveal elements.
|
|
11
|
+
const { initScrollReveal, cleanup } = useScrollReveal();
|
|
12
|
+
nuxtApp.hook('page:finish', () => {
|
|
13
|
+
cleanup();
|
|
14
|
+
nextTick(() => setTimeout(initScrollReveal, 100));
|
|
15
|
+
});
|
|
10
16
|
|
|
11
17
|
// Initialize parallax globally
|
|
12
18
|
useParallax();
|