@xenterprises/nuxt-x-marketing 1.0.1 → 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.
Files changed (45) hide show
  1. package/.claude/settings.local.json +23 -0
  2. package/.playground/app.vue +12 -6
  3. package/.playground/package-lock.json +6 -0
  4. package/.playground/pages/index.vue +2 -2
  5. package/.playground/pages/layout/x-legal.vue +12 -12
  6. package/LICENSE +60 -0
  7. package/README.md +210 -64
  8. package/app/app.config.ts +2 -72
  9. package/app/components/X/Footer/index.vue +3 -4
  10. package/app/components/X/Mark/Affiliate/ComparisonTable.vue +382 -0
  11. package/app/components/X/Mark/Affiliate/Disclosure.vue +138 -0
  12. package/app/components/X/Mark/Affiliate/ProductCard.vue +431 -0
  13. package/app/components/X/Mark/Affiliate/ProductDetail.vue +410 -0
  14. package/app/components/X/Mark/Affiliate/ProductGrid.vue +101 -0
  15. package/app/components/X/Mark/Blog/Author.vue +26 -20
  16. package/app/components/X/Mark/Blog/Card.vue +28 -15
  17. package/app/components/X/Mark/Blog/Detail.vue +177 -169
  18. package/app/components/X/Mark/Blog/List.vue +17 -16
  19. package/app/components/X/Mark/CTA/index.vue +74 -70
  20. package/app/components/X/Mark/Card/Promo.vue +149 -139
  21. package/app/components/X/Mark/Directory/CategoryHero.vue +124 -0
  22. package/app/components/X/Mark/Directory/ListingCard.vue +336 -0
  23. package/app/components/X/Mark/Directory/ListingDetail.vue +306 -0
  24. package/app/components/X/Mark/Directory/ListingGrid.vue +190 -0
  25. package/app/components/X/Mark/Directory/SubmitCTA.vue +201 -0
  26. package/app/components/X/Mark/FAQ/index.vue +191 -0
  27. package/app/components/X/Mark/Features/index.vue +87 -35
  28. package/app/components/X/Mark/Hero/index.vue +41 -38
  29. package/app/components/X/Mark/Layout/AnnouncementBar.vue +20 -28
  30. package/app/components/X/Mark/Modal/Chat.vue +124 -124
  31. package/app/components/X/Mark/Modal/Video.vue +46 -36
  32. package/app/components/X/Mark/Pricing/Comparison.vue +76 -21
  33. package/app/components/X/Mark/Pricing/Plans.vue +75 -25
  34. package/app/components/X/Mark/SocialProof/Testimonials.vue +89 -21
  35. package/app/components/X/Mark/SocialProof/Toast.vue +146 -138
  36. package/app/components/X/X/Footer/index.vue +101 -0
  37. package/app/components/X/X/Header/Nav/index.vue +62 -0
  38. package/app/components/X/X/Header/index.vue +72 -0
  39. package/app/components/X/X/Legal/index.vue +94 -0
  40. package/app/composables/useParallax.ts +32 -19
  41. package/app/composables/useScrollReveal.ts +20 -13
  42. package/app/pages/index.vue +230 -28
  43. package/componentList.json +1856 -238
  44. package/package.json +2 -1
  45. package/screenshots/playground-home.png +0 -0
@@ -0,0 +1,124 @@
1
+ <template>
2
+ <section
3
+ class="relative overflow-hidden bg-neutral-900 dark:bg-neutral-950 py-16 sm:py-20 lg:py-24"
4
+ >
5
+ <!-- Subtle background pattern -->
6
+ <XMarkPatternBg
7
+ pattern="dots"
8
+ :opacity="0.04"
9
+ class="absolute inset-0 pointer-events-none"
10
+ />
11
+
12
+ <!-- Gradient overlay -->
13
+ <div
14
+ class="absolute inset-0 bg-gradient-to-b from-primary-900/20 via-transparent to-neutral-900/60 pointer-events-none"
15
+ />
16
+
17
+ <div
18
+ class="relative z-10 max-w-3xl mx-auto px-4 sm:px-6 text-center xFadeUp"
19
+ >
20
+ <!-- Category Icon -->
21
+ <div
22
+ v-if="category.icon"
23
+ class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500/20 border border-primary-500/30 mb-6"
24
+ >
25
+ <UIcon :name="category.icon" class="w-8 h-8 text-primary-400" />
26
+ </div>
27
+
28
+ <!-- Category Name -->
29
+ <h1 class="xText-headline text-white">
30
+ {{ category.name }}
31
+ </h1>
32
+
33
+ <!-- Count -->
34
+ <p
35
+ v-if="category.count !== undefined"
36
+ class="mt-3 text-sm font-medium text-neutral-400 uppercase tracking-wider"
37
+ >
38
+ {{ category.count.toLocaleString() }} listing{{
39
+ category.count !== 1 ? "s" : ""
40
+ }}
41
+ </p>
42
+
43
+ <!-- Description -->
44
+ <p
45
+ v-if="category.description"
46
+ class="mt-4 text-lg text-neutral-300 leading-relaxed max-w-xl mx-auto"
47
+ >
48
+ {{ category.description }}
49
+ </p>
50
+
51
+ <!-- Search Input -->
52
+ <div v-if="hasSearch" class="mt-8 max-w-lg mx-auto">
53
+ <div class="relative">
54
+ <UInput
55
+ v-model="searchQuery"
56
+ :placeholder="searchPlaceholder"
57
+ icon="i-lucide-search"
58
+ size="lg"
59
+ class="w-full"
60
+ :ui="{
61
+ base: 'bg-white/10 dark:bg-white/5 border-white/20 dark:border-white/10 text-white placeholder:text-neutral-400 focus:border-primary-400 focus:ring-primary-400/20',
62
+ }"
63
+ @keyup.enter="emitSearch"
64
+ />
65
+ <UButton
66
+ v-if="searchQuery"
67
+ icon="i-lucide-x"
68
+ color="neutral"
69
+ variant="ghost"
70
+ size="sm"
71
+ class="absolute right-2 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white"
72
+ @click="clearSearch"
73
+ />
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </section>
78
+ </template>
79
+
80
+ <script setup>
81
+ // Initialize scroll reveal animations
82
+ useScrollReveal();
83
+
84
+ const props = defineProps({
85
+ /** Category object { name, icon?, count?, description? } */
86
+ category: {
87
+ type: Object,
88
+ default: () => ({
89
+ name: "All Listings",
90
+ icon: "i-lucide-grid",
91
+ count: 0,
92
+ description: "Browse all listings in this category.",
93
+ }),
94
+ },
95
+ /** Show search input */
96
+ hasSearch: {
97
+ type: Boolean,
98
+ default: true,
99
+ },
100
+ /** Search input placeholder */
101
+ searchPlaceholder: {
102
+ type: String,
103
+ default: "Search listings...",
104
+ },
105
+ });
106
+
107
+ const emit = defineEmits(["search"]);
108
+
109
+ const searchQuery = ref("");
110
+
111
+ const emitSearch = () => {
112
+ emit("search", searchQuery.value);
113
+ };
114
+
115
+ const clearSearch = () => {
116
+ searchQuery.value = "";
117
+ emit("search", "");
118
+ };
119
+
120
+ // Emit on each keystroke for live filtering
121
+ watch(searchQuery, (val) => {
122
+ emit("search", val);
123
+ });
124
+ </script>
@@ -0,0 +1,336 @@
1
+ <template>
2
+ <!-- Card Layout -->
3
+ <article
4
+ v-if="layout === 'card'"
5
+ class="group relative flex flex-col rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 overflow-hidden transition-all duration-300 ease-out hover:-translate-y-1 hover:shadow-[0_12px_40px_rgba(128,128,128,0.15)] dark:hover:shadow-[0_12px_40px_rgba(255,255,255,0.07)]"
6
+ >
7
+ <!-- Badge -->
8
+ <div v-if="showBadge && listing.badge" class="absolute top-3 right-3 z-10">
9
+ <UBadge
10
+ :label="listing.badge"
11
+ :color="badgeColor"
12
+ variant="solid"
13
+ size="sm"
14
+ />
15
+ </div>
16
+
17
+ <!-- Logo / Header -->
18
+ <div class="p-5 pb-0 flex items-start gap-4">
19
+ <figure
20
+ v-if="listing.logo"
21
+ class="w-14 h-14 rounded-xl overflow-hidden flex-shrink-0 border border-neutral-100 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-800"
22
+ >
23
+ <img
24
+ :src="listing.logo"
25
+ :alt="listing.name"
26
+ class="w-full h-full object-contain"
27
+ />
28
+ </figure>
29
+ <div
30
+ v-else
31
+ class="w-14 h-14 rounded-xl flex-shrink-0 bg-primary-100 dark:bg-primary-900/40 flex items-center justify-center"
32
+ >
33
+ <span class="text-primary-600 dark:text-primary-400 font-bold text-xl">
34
+ {{ listing.name.charAt(0) }}
35
+ </span>
36
+ </div>
37
+
38
+ <div class="flex-1 min-w-0 pt-1">
39
+ <h3
40
+ class="font-semibold text-neutral-900 dark:text-white truncate group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors"
41
+ >
42
+ {{ listing.name }}
43
+ </h3>
44
+ <p
45
+ v-if="listing.category"
46
+ class="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5"
47
+ >
48
+ {{ listing.category }}
49
+ </p>
50
+ </div>
51
+ </div>
52
+
53
+ <!-- Description -->
54
+ <div class="px-5 pt-3 flex-1">
55
+ <p
56
+ v-if="listing.description"
57
+ class="text-sm text-neutral-600 dark:text-neutral-400 line-clamp-2"
58
+ >
59
+ {{ listing.description }}
60
+ </p>
61
+ </div>
62
+
63
+ <!-- Tags -->
64
+ <div
65
+ v-if="showTags && listing.tags?.length"
66
+ class="px-5 pt-3 flex flex-wrap gap-1.5"
67
+ >
68
+ <UBadge
69
+ v-for="tag in listing.tags.slice(0, 4)"
70
+ :key="tag"
71
+ :label="tag"
72
+ variant="subtle"
73
+ color="neutral"
74
+ size="xs"
75
+ />
76
+ </div>
77
+
78
+ <!-- Rating -->
79
+ <div
80
+ v-if="showRating && listing.rating"
81
+ class="px-5 pt-3 flex items-center gap-2"
82
+ >
83
+ <div class="flex items-center gap-0.5">
84
+ <UIcon
85
+ v-for="i in 5"
86
+ :key="i"
87
+ name="i-lucide-star"
88
+ class="w-3.5 h-3.5"
89
+ :class="
90
+ i <= Math.round(listing.rating)
91
+ ? 'text-yellow-400'
92
+ : 'text-neutral-300 dark:text-neutral-600'
93
+ "
94
+ />
95
+ </div>
96
+ <span class="text-sm font-medium text-neutral-700 dark:text-neutral-300">
97
+ {{ listing.rating.toFixed(1) }}
98
+ </span>
99
+ <span
100
+ v-if="listing.reviewCount"
101
+ class="text-xs text-neutral-500 dark:text-neutral-400"
102
+ >
103
+ ({{ listing.reviewCount }})
104
+ </span>
105
+ </div>
106
+
107
+ <!-- CTA -->
108
+ <div class="p-5 pt-4 mt-auto">
109
+ <UButton
110
+ :to="listing.to || '#'"
111
+ :label="ctaLabel"
112
+ color="primary"
113
+ variant="outline"
114
+ size="sm"
115
+ block
116
+ class="justify-center"
117
+ />
118
+ </div>
119
+ </article>
120
+
121
+ <!-- Row Layout -->
122
+ <article
123
+ v-else-if="layout === 'row'"
124
+ class="group flex items-center gap-4 p-4 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 transition-all duration-300 ease-out hover:-translate-y-0.5 hover:shadow-[0_8px_24px_rgba(128,128,128,0.12)] dark:hover:shadow-[0_8px_24px_rgba(255,255,255,0.06)]"
125
+ >
126
+ <!-- Logo -->
127
+ <figure
128
+ v-if="listing.logo"
129
+ class="w-12 h-12 rounded-lg overflow-hidden flex-shrink-0 border border-neutral-100 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-800"
130
+ >
131
+ <img
132
+ :src="listing.logo"
133
+ :alt="listing.name"
134
+ class="w-full h-full object-contain"
135
+ />
136
+ </figure>
137
+ <div
138
+ v-else
139
+ class="w-12 h-12 rounded-lg flex-shrink-0 bg-primary-100 dark:bg-primary-900/40 flex items-center justify-center"
140
+ >
141
+ <span class="text-primary-600 dark:text-primary-400 font-bold text-lg">
142
+ {{ listing.name.charAt(0) }}
143
+ </span>
144
+ </div>
145
+
146
+ <!-- Content -->
147
+ <div class="flex-1 min-w-0">
148
+ <div class="flex items-center gap-2 flex-wrap">
149
+ <h3
150
+ class="font-semibold text-neutral-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors"
151
+ >
152
+ {{ listing.name }}
153
+ </h3>
154
+ <UBadge
155
+ v-if="showBadge && listing.badge"
156
+ :label="listing.badge"
157
+ :color="badgeColor"
158
+ variant="solid"
159
+ size="xs"
160
+ />
161
+ <UBadge
162
+ v-if="listing.category"
163
+ :label="listing.category"
164
+ variant="subtle"
165
+ color="neutral"
166
+ size="xs"
167
+ />
168
+ </div>
169
+ <p
170
+ v-if="listing.description"
171
+ class="text-sm text-neutral-600 dark:text-neutral-400 mt-0.5 truncate"
172
+ >
173
+ {{ listing.description }}
174
+ </p>
175
+ <div
176
+ v-if="showRating && listing.rating"
177
+ class="flex items-center gap-1.5 mt-1"
178
+ >
179
+ <div class="flex items-center gap-0.5">
180
+ <UIcon
181
+ v-for="i in 5"
182
+ :key="i"
183
+ name="i-lucide-star"
184
+ class="w-3 h-3"
185
+ :class="
186
+ i <= Math.round(listing.rating)
187
+ ? 'text-yellow-400'
188
+ : 'text-neutral-300 dark:text-neutral-600'
189
+ "
190
+ />
191
+ </div>
192
+ <span class="text-xs text-neutral-500 dark:text-neutral-400">
193
+ {{ listing.rating.toFixed(1) }}
194
+ <span v-if="listing.reviewCount">({{ listing.reviewCount }})</span>
195
+ </span>
196
+ </div>
197
+ </div>
198
+
199
+ <!-- Tags (row) -->
200
+ <div
201
+ v-if="showTags && listing.tags?.length"
202
+ class="hidden sm:flex flex-wrap gap-1 flex-shrink-0"
203
+ >
204
+ <UBadge
205
+ v-for="tag in listing.tags.slice(0, 2)"
206
+ :key="tag"
207
+ :label="tag"
208
+ variant="subtle"
209
+ color="neutral"
210
+ size="xs"
211
+ />
212
+ </div>
213
+
214
+ <!-- CTA -->
215
+ <UButton
216
+ :to="listing.to || '#'"
217
+ :label="ctaLabel"
218
+ color="primary"
219
+ variant="outline"
220
+ size="sm"
221
+ class="flex-shrink-0"
222
+ />
223
+ </article>
224
+
225
+ <!-- Compact Layout -->
226
+ <article
227
+ v-else-if="layout === 'compact'"
228
+ class="group flex items-center gap-3 py-3 border-b border-neutral-100 dark:border-neutral-800 last:border-0 transition-colors hover:bg-neutral-50 dark:hover:bg-neutral-900/50 px-2 rounded-lg"
229
+ >
230
+ <!-- Logo -->
231
+ <figure
232
+ v-if="listing.logo"
233
+ class="w-9 h-9 rounded-lg overflow-hidden flex-shrink-0 border border-neutral-100 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-800"
234
+ >
235
+ <img
236
+ :src="listing.logo"
237
+ :alt="listing.name"
238
+ class="w-full h-full object-contain"
239
+ />
240
+ </figure>
241
+ <div
242
+ v-else
243
+ class="w-9 h-9 rounded-lg flex-shrink-0 bg-primary-100 dark:bg-primary-900/40 flex items-center justify-center"
244
+ >
245
+ <span class="text-primary-600 dark:text-primary-400 font-bold text-sm">
246
+ {{ listing.name.charAt(0) }}
247
+ </span>
248
+ </div>
249
+
250
+ <!-- Name + Category -->
251
+ <div class="flex-1 min-w-0">
252
+ <NuxtLink
253
+ :to="listing.to || '#'"
254
+ class="font-medium text-sm text-neutral-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors truncate block"
255
+ >
256
+ {{ listing.name }}
257
+ </NuxtLink>
258
+ <p
259
+ v-if="listing.category"
260
+ class="text-xs text-neutral-500 dark:text-neutral-400 truncate"
261
+ >
262
+ {{ listing.category }}
263
+ </p>
264
+ </div>
265
+
266
+ <!-- Rating -->
267
+ <div
268
+ v-if="showRating && listing.rating"
269
+ class="flex items-center gap-1 flex-shrink-0"
270
+ >
271
+ <UIcon name="i-lucide-star" class="w-3 h-3 text-yellow-400" />
272
+ <span class="text-xs font-medium text-neutral-700 dark:text-neutral-300">
273
+ {{ listing.rating.toFixed(1) }}
274
+ </span>
275
+ </div>
276
+
277
+ <!-- Badge -->
278
+ <UBadge
279
+ v-if="showBadge && listing.badge"
280
+ :label="listing.badge"
281
+ :color="badgeColor"
282
+ variant="solid"
283
+ size="xs"
284
+ class="flex-shrink-0"
285
+ />
286
+ </article>
287
+ </template>
288
+
289
+ <script setup>
290
+ const props = defineProps({
291
+ /** Listing object { name, description?, logo?, category?, tags?, rating?, reviewCount?, badge?, to? } */
292
+ listing: {
293
+ type: Object,
294
+ default: () => ({
295
+ name: "Example Listing",
296
+ description: "A great product or service.",
297
+ category: "General",
298
+ rating: 4.5,
299
+ reviewCount: 42,
300
+ }),
301
+ },
302
+ /** Card display layout */
303
+ layout: {
304
+ type: String,
305
+ default: "card",
306
+ validator: (v) => ["card", "row", "compact"].includes(v),
307
+ },
308
+ /** Show star rating */
309
+ showRating: {
310
+ type: Boolean,
311
+ default: true,
312
+ },
313
+ /** Show tags */
314
+ showTags: {
315
+ type: Boolean,
316
+ default: true,
317
+ },
318
+ /** Show badge (Featured, Sponsored, etc.) */
319
+ showBadge: {
320
+ type: Boolean,
321
+ default: true,
322
+ },
323
+ });
324
+
325
+ const ctaLabel = computed(() => {
326
+ return props.listing.ctaLabel || "View Listing";
327
+ });
328
+
329
+ const badgeColor = computed(() => {
330
+ const badge = props.listing.badge?.toLowerCase();
331
+ if (badge === "featured") return "primary";
332
+ if (badge === "sponsored") return "warning";
333
+ if (badge === "new") return "success";
334
+ return "primary";
335
+ });
336
+ </script>