@xenterprises/nuxt-x-marketing 1.0.0 → 1.0.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/.claude/settings.local.json +2 -1
- package/.playground/app.vue +12 -6
- package/.playground/pages/blog/detail.vue +341 -249
- package/.playground/pages/index.vue +2 -2
- package/.playground/pages/layout/x-legal.vue +12 -12
- package/.playground/pages/notifications/social-proof-toast.vue +330 -70
- package/LICENSE +60 -0
- package/README.md +210 -64
- package/app/app.config.ts +2 -72
- package/app/assets/css/x-marketing.css +1 -0
- package/app/components/X/Footer/index.vue +3 -4
- package/app/components/X/Mark/Affiliate/ComparisonTable.vue +382 -0
- package/app/components/X/Mark/Affiliate/Disclosure.vue +138 -0
- package/app/components/X/Mark/Affiliate/ProductCard.vue +431 -0
- package/app/components/X/Mark/Affiliate/ProductDetail.vue +410 -0
- package/app/components/X/Mark/Affiliate/ProductGrid.vue +101 -0
- package/app/components/X/Mark/Blog/Author.vue +26 -20
- package/app/components/X/Mark/Blog/Card.vue +28 -15
- package/app/components/X/Mark/Blog/Detail.vue +82 -127
- package/app/components/X/Mark/Blog/List.vue +17 -16
- package/app/components/X/Mark/CTA/index.vue +74 -70
- package/app/components/X/Mark/Card/Promo.vue +149 -139
- package/app/components/X/Mark/CodeBlock/index.vue +69 -0
- package/app/components/X/Mark/Directory/CategoryHero.vue +124 -0
- package/app/components/X/Mark/Directory/ListingCard.vue +336 -0
- package/app/components/X/Mark/Directory/ListingDetail.vue +306 -0
- package/app/components/X/Mark/Directory/ListingGrid.vue +190 -0
- package/app/components/X/Mark/Directory/SubmitCTA.vue +201 -0
- package/app/components/X/Mark/FAQ/index.vue +191 -0
- package/app/components/X/Mark/Features/index.vue +87 -35
- package/app/components/X/Mark/Hero/index.vue +41 -38
- package/app/components/X/Mark/Layout/AnnouncementBar.vue +20 -28
- package/app/components/X/Mark/Modal/Chat.vue +124 -124
- package/app/components/X/Mark/Modal/Video.vue +46 -36
- package/app/components/X/Mark/Pricing/Comparison.vue +76 -21
- package/app/components/X/Mark/Pricing/Plans.vue +75 -25
- package/app/components/X/Mark/SocialProof/Testimonials.vue +89 -21
- package/app/components/X/Mark/SocialProof/Toast.vue +155 -102
- package/app/components/X/X/Footer/index.vue +101 -0
- package/app/components/X/X/Header/Nav/index.vue +62 -0
- package/app/components/X/X/Header/index.vue +72 -0
- package/app/components/X/X/Legal/index.vue +94 -0
- package/app/composables/useParallax.ts +32 -19
- package/app/composables/useScrollReveal.ts +20 -13
- package/app/pages/index.vue +230 -28
- package/componentList.json +1856 -238
- package/nuxt.config.ts +12 -3
- package/package.json +5 -2
- package/screenshots/playground-home.png +0 -0
|
@@ -1,151 +1,151 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
>
|
|
19
|
-
<UIcon name="i-lucide-x" class="w-4 h-4 text-gray-400" />
|
|
20
|
-
</button>
|
|
2
|
+
<Transition
|
|
3
|
+
enter-active-class="transition-all duration-300 ease-out"
|
|
4
|
+
enter-from-class="translate-y-4 opacity-0"
|
|
5
|
+
leave-active-class="transition-all duration-200 ease-in"
|
|
6
|
+
leave-to-class="translate-y-4 opacity-0"
|
|
7
|
+
>
|
|
8
|
+
<aside v-if="isVisible" class="fixed bottom-6 right-6 z-40 max-w-sm">
|
|
9
|
+
<div class="xGlass rounded-2xl p-4 shadow-xl">
|
|
10
|
+
<!-- Close button -->
|
|
11
|
+
<button
|
|
12
|
+
class="absolute top-2 right-2 p-1 rounded-full hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
|
|
13
|
+
aria-label="Close"
|
|
14
|
+
@click="dismiss"
|
|
15
|
+
>
|
|
16
|
+
<UIcon name="i-lucide-x" class="w-4 h-4 text-gray-400" />
|
|
17
|
+
</button>
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
19
|
+
<!-- Content -->
|
|
20
|
+
<div class="flex gap-3">
|
|
21
|
+
<!-- Avatar -->
|
|
22
|
+
<img
|
|
23
|
+
v-if="avatar"
|
|
24
|
+
:src="avatar"
|
|
25
|
+
:alt="name"
|
|
26
|
+
class="w-12 h-12 rounded-full object-cover flex-shrink-0"
|
|
27
|
+
/>
|
|
28
|
+
<div
|
|
29
|
+
v-else
|
|
30
|
+
class="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center flex-shrink-0"
|
|
31
|
+
>
|
|
32
|
+
<span
|
|
33
|
+
class="text-primary-600 dark:text-primary-400 font-medium text-lg"
|
|
34
|
+
>
|
|
35
|
+
{{ name.charAt(0) }}
|
|
36
|
+
</span>
|
|
37
|
+
</div>
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class="w-2 h-2 rounded-full bg-green-500"
|
|
49
|
-
/>
|
|
50
|
-
</div>
|
|
39
|
+
<div class="flex-1 min-w-0">
|
|
40
|
+
<!-- Header -->
|
|
41
|
+
<div class="flex items-center gap-2 mb-1">
|
|
42
|
+
<span class="font-medium text-gray-900 dark:text-white">
|
|
43
|
+
{{ name }}
|
|
44
|
+
</span>
|
|
45
|
+
<span v-if="online" class="w-2 h-2 rounded-full bg-green-500" />
|
|
46
|
+
</div>
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
<!-- Message -->
|
|
49
|
+
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
50
|
+
{{ message }}
|
|
51
|
+
</p>
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
53
|
+
<!-- Action -->
|
|
54
|
+
<UButton
|
|
55
|
+
v-if="actionText"
|
|
56
|
+
size="sm"
|
|
57
|
+
class="mt-3"
|
|
58
|
+
@click="handleAction"
|
|
59
|
+
>
|
|
60
|
+
{{ actionText }}
|
|
61
|
+
</UButton>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</aside>
|
|
66
|
+
</Transition>
|
|
71
67
|
</template>
|
|
72
68
|
|
|
73
69
|
<script setup>
|
|
74
70
|
const props = defineProps({
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
71
|
+
/** v-model visibility */
|
|
72
|
+
modelValue: {
|
|
73
|
+
type: Boolean,
|
|
74
|
+
default: false,
|
|
75
|
+
},
|
|
76
|
+
/** Avatar image URL */
|
|
77
|
+
avatar: {
|
|
78
|
+
type: String,
|
|
79
|
+
default: "",
|
|
80
|
+
},
|
|
81
|
+
/** Person/bot name */
|
|
82
|
+
name: {
|
|
83
|
+
type: String,
|
|
84
|
+
default: "Support",
|
|
85
|
+
},
|
|
86
|
+
/** Chat message */
|
|
87
|
+
message: {
|
|
88
|
+
type: String,
|
|
89
|
+
default: "Hi there! 👋 How can I help you today?",
|
|
90
|
+
},
|
|
91
|
+
/** Show online indicator */
|
|
92
|
+
online: {
|
|
93
|
+
type: Boolean,
|
|
94
|
+
default: true,
|
|
95
|
+
},
|
|
96
|
+
/** Button text */
|
|
97
|
+
actionText: {
|
|
98
|
+
type: String,
|
|
99
|
+
default: "",
|
|
100
|
+
},
|
|
101
|
+
/** Auto-dismiss after ms (0 = never) */
|
|
102
|
+
autoDismiss: {
|
|
103
|
+
type: Number,
|
|
104
|
+
default: 0,
|
|
105
|
+
},
|
|
106
|
+
/** Delay before showing (ms) */
|
|
107
|
+
delay: {
|
|
108
|
+
type: Number,
|
|
109
|
+
default: 3000,
|
|
110
|
+
},
|
|
115
111
|
});
|
|
116
112
|
|
|
117
|
-
const emit = defineEmits([
|
|
113
|
+
const emit = defineEmits(["update:modelValue", "action", "dismiss"]);
|
|
118
114
|
|
|
119
115
|
const isVisible = ref(false);
|
|
120
116
|
let dismissTimeout = null;
|
|
121
117
|
let showTimeout = null;
|
|
122
118
|
|
|
123
119
|
const dismiss = () => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
isVisible.value = false;
|
|
121
|
+
emit("update:modelValue", false);
|
|
122
|
+
emit("dismiss");
|
|
127
123
|
};
|
|
128
124
|
|
|
129
125
|
const handleAction = () => {
|
|
130
|
-
|
|
126
|
+
emit("action");
|
|
131
127
|
};
|
|
132
128
|
|
|
133
|
-
watch(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
watch(
|
|
130
|
+
() => props.modelValue,
|
|
131
|
+
(val) => {
|
|
132
|
+
if (val) {
|
|
133
|
+
showTimeout = setTimeout(() => {
|
|
134
|
+
isVisible.value = true;
|
|
137
135
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
},
|
|
136
|
+
if (props.autoDismiss > 0) {
|
|
137
|
+
dismissTimeout = setTimeout(dismiss, props.autoDismiss);
|
|
138
|
+
}
|
|
139
|
+
}, props.delay);
|
|
140
|
+
} else {
|
|
141
|
+
isVisible.value = false;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
{ immediate: true }
|
|
145
|
+
);
|
|
146
146
|
|
|
147
147
|
onUnmounted(() => {
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
if (dismissTimeout) clearTimeout(dismissTimeout);
|
|
149
|
+
if (showTimeout) clearTimeout(showTimeout);
|
|
150
150
|
});
|
|
151
151
|
</script>
|
|
@@ -20,8 +20,12 @@
|
|
|
20
20
|
:alt="title || 'Video thumbnail'"
|
|
21
21
|
class="w-full aspect-video object-cover transition-transform group-hover:scale-105"
|
|
22
22
|
/>
|
|
23
|
-
<div
|
|
24
|
-
|
|
23
|
+
<div
|
|
24
|
+
class="absolute inset-0 bg-black/40 flex items-center justify-center group-hover:bg-black/50 transition-colors"
|
|
25
|
+
>
|
|
26
|
+
<div
|
|
27
|
+
class="w-16 h-16 rounded-full bg-white/90 flex items-center justify-center shadow-lg"
|
|
28
|
+
>
|
|
25
29
|
<UIcon name="i-lucide-play" class="w-8 h-8 text-neutral-900 ml-1" />
|
|
26
30
|
</div>
|
|
27
31
|
</div>
|
|
@@ -40,7 +44,10 @@
|
|
|
40
44
|
|
|
41
45
|
<!-- Video Content -->
|
|
42
46
|
<template #content>
|
|
43
|
-
<div
|
|
47
|
+
<div
|
|
48
|
+
class="aspect-video bg-black overflow-hidden"
|
|
49
|
+
:class="title || description ? 'rounded-t-lg' : 'rounded-lg'"
|
|
50
|
+
>
|
|
44
51
|
<!-- YouTube Embed -->
|
|
45
52
|
<iframe
|
|
46
53
|
v-if="modalRef?.isOpen && embedType === 'youtube'"
|
|
@@ -89,33 +96,36 @@ const props = defineProps({
|
|
|
89
96
|
/** Video source (direct URL, YouTube, or Vimeo) */
|
|
90
97
|
src: {
|
|
91
98
|
type: String,
|
|
92
|
-
|
|
99
|
+
default: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
93
100
|
},
|
|
94
101
|
/** Thumbnail image URL (shows image with play button) */
|
|
95
102
|
thumbnail: {
|
|
96
103
|
type: String,
|
|
97
|
-
default:
|
|
104
|
+
default: "",
|
|
98
105
|
},
|
|
99
106
|
/** Poster image for direct videos */
|
|
100
107
|
poster: {
|
|
101
108
|
type: String,
|
|
102
|
-
default:
|
|
109
|
+
default: "",
|
|
103
110
|
},
|
|
104
111
|
/** Video title (shown in modal footer) */
|
|
105
112
|
title: {
|
|
106
113
|
type: String,
|
|
107
|
-
default:
|
|
114
|
+
default: "",
|
|
108
115
|
},
|
|
109
116
|
/** Video description (shown in modal footer) */
|
|
110
117
|
description: {
|
|
111
118
|
type: String,
|
|
112
|
-
default:
|
|
119
|
+
default: "",
|
|
113
120
|
},
|
|
114
121
|
/** Modal size */
|
|
115
122
|
size: {
|
|
116
123
|
type: String,
|
|
117
|
-
default:
|
|
118
|
-
validator: (v) =>
|
|
124
|
+
default: "4xl",
|
|
125
|
+
validator: (v) =>
|
|
126
|
+
["lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "full"].includes(
|
|
127
|
+
v
|
|
128
|
+
),
|
|
119
129
|
},
|
|
120
130
|
/** Autoplay video when modal opens */
|
|
121
131
|
autoplay: {
|
|
@@ -130,63 +140,63 @@ const props = defineProps({
|
|
|
130
140
|
/** Captions/subtitles file URL */
|
|
131
141
|
captions: {
|
|
132
142
|
type: String,
|
|
133
|
-
default:
|
|
143
|
+
default: "",
|
|
134
144
|
},
|
|
135
145
|
/** Captions language code */
|
|
136
146
|
captionsLang: {
|
|
137
147
|
type: String,
|
|
138
|
-
default:
|
|
148
|
+
default: "en",
|
|
139
149
|
},
|
|
140
150
|
/** Captions label */
|
|
141
151
|
captionsLabel: {
|
|
142
152
|
type: String,
|
|
143
|
-
default:
|
|
153
|
+
default: "English",
|
|
144
154
|
},
|
|
145
155
|
});
|
|
146
156
|
|
|
147
|
-
const emit = defineEmits([
|
|
157
|
+
const emit = defineEmits(["open", "close"]);
|
|
148
158
|
|
|
149
159
|
const modalRef = ref(null);
|
|
150
160
|
const videoRef = ref(null);
|
|
151
161
|
|
|
152
162
|
// Button props with defaults
|
|
153
163
|
const buttonProps = computed(() => ({
|
|
154
|
-
label: props.button.label ||
|
|
155
|
-
color: props.button.color ||
|
|
156
|
-
variant: props.button.variant ||
|
|
157
|
-
size: props.button.size ||
|
|
158
|
-
icon: props.button.icon ||
|
|
164
|
+
label: props.button.label || "Watch Video",
|
|
165
|
+
color: props.button.color || "primary",
|
|
166
|
+
variant: props.button.variant || "solid",
|
|
167
|
+
size: props.button.size || "md",
|
|
168
|
+
icon: props.button.icon || "i-lucide-play",
|
|
159
169
|
}));
|
|
160
170
|
|
|
161
171
|
// Detect embed type from URL
|
|
162
172
|
const embedType = computed(() => {
|
|
163
|
-
if (props.src.includes(
|
|
164
|
-
return
|
|
173
|
+
if (props.src.includes("youtube.com") || props.src.includes("youtu.be")) {
|
|
174
|
+
return "youtube";
|
|
165
175
|
}
|
|
166
|
-
if (props.src.includes(
|
|
167
|
-
return
|
|
176
|
+
if (props.src.includes("vimeo.com")) {
|
|
177
|
+
return "vimeo";
|
|
168
178
|
}
|
|
169
|
-
return
|
|
179
|
+
return "direct";
|
|
170
180
|
});
|
|
171
181
|
|
|
172
182
|
// Generate embed URL
|
|
173
183
|
const embedUrl = computed(() => {
|
|
174
|
-
const autoplayParam = props.autoplay ?
|
|
184
|
+
const autoplayParam = props.autoplay ? "1" : "0";
|
|
175
185
|
|
|
176
|
-
if (embedType.value ===
|
|
177
|
-
let videoId =
|
|
178
|
-
if (props.src.includes(
|
|
179
|
-
videoId = props.src.split(
|
|
180
|
-
} else if (props.src.includes(
|
|
181
|
-
videoId = props.src.split(
|
|
182
|
-
} else if (props.src.includes(
|
|
183
|
-
videoId = props.src.split(
|
|
186
|
+
if (embedType.value === "youtube") {
|
|
187
|
+
let videoId = "";
|
|
188
|
+
if (props.src.includes("youtu.be/")) {
|
|
189
|
+
videoId = props.src.split("youtu.be/")[1]?.split("?")[0];
|
|
190
|
+
} else if (props.src.includes("v=")) {
|
|
191
|
+
videoId = props.src.split("v=")[1]?.split("&")[0];
|
|
192
|
+
} else if (props.src.includes("embed/")) {
|
|
193
|
+
videoId = props.src.split("embed/")[1]?.split("?")[0];
|
|
184
194
|
}
|
|
185
195
|
return `https://www.youtube.com/embed/${videoId}?autoplay=${autoplayParam}&rel=0`;
|
|
186
196
|
}
|
|
187
197
|
|
|
188
|
-
if (embedType.value ===
|
|
189
|
-
const videoId = props.src.split(
|
|
198
|
+
if (embedType.value === "vimeo") {
|
|
199
|
+
const videoId = props.src.split("vimeo.com/")[1]?.split("?")[0];
|
|
190
200
|
return `https://player.vimeo.com/video/${videoId}?autoplay=${autoplayParam}`;
|
|
191
201
|
}
|
|
192
202
|
|
|
@@ -198,6 +208,6 @@ const handleClose = () => {
|
|
|
198
208
|
if (videoRef.value) {
|
|
199
209
|
videoRef.value.pause();
|
|
200
210
|
}
|
|
201
|
-
emit(
|
|
211
|
+
emit("close");
|
|
202
212
|
};
|
|
203
213
|
</script>
|
|
@@ -39,17 +39,70 @@
|
|
|
39
39
|
|
|
40
40
|
<script setup>
|
|
41
41
|
const props = defineProps({
|
|
42
|
-
/** Array of plans to compare */
|
|
42
|
+
/** Array of plans to compare [{ id, name, price?, highlighted?, button? }] */
|
|
43
43
|
plans: {
|
|
44
44
|
type: Array,
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
default: () => [
|
|
46
|
+
{
|
|
47
|
+
id: "starter",
|
|
48
|
+
name: "Starter",
|
|
49
|
+
price: "$9/mo",
|
|
50
|
+
button: { label: "Get Started" },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "pro",
|
|
54
|
+
name: "Pro",
|
|
55
|
+
price: "$29/mo",
|
|
56
|
+
highlighted: true,
|
|
57
|
+
button: { label: "Get Pro" },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "enterprise",
|
|
61
|
+
name: "Enterprise",
|
|
62
|
+
price: "$99/mo",
|
|
63
|
+
button: { label: "Contact Sales" },
|
|
64
|
+
},
|
|
65
|
+
],
|
|
47
66
|
},
|
|
48
|
-
/** Feature groups with features */
|
|
67
|
+
/** Feature groups with features [{ name?, features: [{ name, tooltip?, values: { [planId]: boolean|string } }] }] */
|
|
49
68
|
featureGroups: {
|
|
50
69
|
type: Array,
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
default: () => [
|
|
71
|
+
{
|
|
72
|
+
name: "Core Features",
|
|
73
|
+
features: [
|
|
74
|
+
{
|
|
75
|
+
name: "Projects",
|
|
76
|
+
values: { starter: "5", pro: "Unlimited", enterprise: "Unlimited" },
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "Storage",
|
|
80
|
+
values: { starter: "10GB", pro: "100GB", enterprise: "1TB" },
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "Team members",
|
|
84
|
+
values: { starter: "1", pro: "10", enterprise: "Unlimited" },
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "Support",
|
|
90
|
+
features: [
|
|
91
|
+
{
|
|
92
|
+
name: "Email support",
|
|
93
|
+
values: { starter: true, pro: true, enterprise: true },
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "Priority support",
|
|
97
|
+
values: { starter: false, pro: true, enterprise: true },
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "Dedicated manager",
|
|
101
|
+
values: { starter: false, pro: false, enterprise: true },
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
53
106
|
},
|
|
54
107
|
/** Caption for the table */
|
|
55
108
|
caption: {
|
|
@@ -64,18 +117,18 @@ const props = defineProps({
|
|
|
64
117
|
/** Position of badges */
|
|
65
118
|
badgesPosition: {
|
|
66
119
|
type: String,
|
|
67
|
-
default:
|
|
68
|
-
validator: (v) => [
|
|
120
|
+
default: "bottom",
|
|
121
|
+
validator: (v) => ["top", "bottom"].includes(v),
|
|
69
122
|
},
|
|
70
123
|
/** Alignment of badges */
|
|
71
124
|
badgesAlign: {
|
|
72
125
|
type: String,
|
|
73
|
-
default:
|
|
74
|
-
validator: (v) => [
|
|
126
|
+
default: "center",
|
|
127
|
+
validator: (v) => ["left", "center", "right"].includes(v),
|
|
75
128
|
},
|
|
76
129
|
});
|
|
77
130
|
|
|
78
|
-
const emit = defineEmits([
|
|
131
|
+
const emit = defineEmits(["select"]);
|
|
79
132
|
|
|
80
133
|
// Map XMark plans to Nuxt UI tiers format
|
|
81
134
|
const mappedTiers = computed(() => {
|
|
@@ -85,12 +138,14 @@ const mappedTiers = computed(() => {
|
|
|
85
138
|
description: plan.description,
|
|
86
139
|
price: plan.price,
|
|
87
140
|
highlight: plan.highlighted,
|
|
88
|
-
button: plan.button
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
141
|
+
button: plan.button
|
|
142
|
+
? {
|
|
143
|
+
label: plan.button.label || "Get Started",
|
|
144
|
+
color: plan.highlighted ? "primary" : "neutral",
|
|
145
|
+
variant: plan.highlighted ? "solid" : "outline",
|
|
146
|
+
onClick: () => emit("select", plan),
|
|
147
|
+
}
|
|
148
|
+
: undefined,
|
|
94
149
|
}));
|
|
95
150
|
});
|
|
96
151
|
|
|
@@ -109,16 +164,16 @@ const mappedSections = computed(() => {
|
|
|
109
164
|
// Badge alignment classes
|
|
110
165
|
const badgesAlignClasses = computed(() => {
|
|
111
166
|
const alignments = {
|
|
112
|
-
left:
|
|
113
|
-
center:
|
|
114
|
-
right:
|
|
167
|
+
left: "justify-start",
|
|
168
|
+
center: "justify-center",
|
|
169
|
+
right: "justify-end",
|
|
115
170
|
};
|
|
116
171
|
return alignments[props.badgesAlign];
|
|
117
172
|
});
|
|
118
173
|
|
|
119
174
|
// Get badge props (handles string presets and object configs)
|
|
120
175
|
const getBadgeProps = (badge) => {
|
|
121
|
-
if (typeof badge ===
|
|
176
|
+
if (typeof badge === "string") {
|
|
122
177
|
return { preset: badge };
|
|
123
178
|
}
|
|
124
179
|
return badge;
|