@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.
Files changed (49) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/.playground/app.vue +12 -6
  3. package/.playground/pages/blog/detail.vue +341 -249
  4. package/.playground/pages/index.vue +2 -2
  5. package/.playground/pages/layout/x-legal.vue +12 -12
  6. package/.playground/pages/notifications/social-proof-toast.vue +330 -70
  7. package/LICENSE +60 -0
  8. package/README.md +210 -64
  9. package/app/app.config.ts +2 -72
  10. package/app/assets/css/x-marketing.css +1 -0
  11. package/app/components/X/Footer/index.vue +3 -4
  12. package/app/components/X/Mark/Affiliate/ComparisonTable.vue +382 -0
  13. package/app/components/X/Mark/Affiliate/Disclosure.vue +138 -0
  14. package/app/components/X/Mark/Affiliate/ProductCard.vue +431 -0
  15. package/app/components/X/Mark/Affiliate/ProductDetail.vue +410 -0
  16. package/app/components/X/Mark/Affiliate/ProductGrid.vue +101 -0
  17. package/app/components/X/Mark/Blog/Author.vue +26 -20
  18. package/app/components/X/Mark/Blog/Card.vue +28 -15
  19. package/app/components/X/Mark/Blog/Detail.vue +82 -127
  20. package/app/components/X/Mark/Blog/List.vue +17 -16
  21. package/app/components/X/Mark/CTA/index.vue +74 -70
  22. package/app/components/X/Mark/Card/Promo.vue +149 -139
  23. package/app/components/X/Mark/CodeBlock/index.vue +69 -0
  24. package/app/components/X/Mark/Directory/CategoryHero.vue +124 -0
  25. package/app/components/X/Mark/Directory/ListingCard.vue +336 -0
  26. package/app/components/X/Mark/Directory/ListingDetail.vue +306 -0
  27. package/app/components/X/Mark/Directory/ListingGrid.vue +190 -0
  28. package/app/components/X/Mark/Directory/SubmitCTA.vue +201 -0
  29. package/app/components/X/Mark/FAQ/index.vue +191 -0
  30. package/app/components/X/Mark/Features/index.vue +87 -35
  31. package/app/components/X/Mark/Hero/index.vue +41 -38
  32. package/app/components/X/Mark/Layout/AnnouncementBar.vue +20 -28
  33. package/app/components/X/Mark/Modal/Chat.vue +124 -124
  34. package/app/components/X/Mark/Modal/Video.vue +46 -36
  35. package/app/components/X/Mark/Pricing/Comparison.vue +76 -21
  36. package/app/components/X/Mark/Pricing/Plans.vue +75 -25
  37. package/app/components/X/Mark/SocialProof/Testimonials.vue +89 -21
  38. package/app/components/X/Mark/SocialProof/Toast.vue +155 -102
  39. package/app/components/X/X/Footer/index.vue +101 -0
  40. package/app/components/X/X/Header/Nav/index.vue +62 -0
  41. package/app/components/X/X/Header/index.vue +72 -0
  42. package/app/components/X/X/Legal/index.vue +94 -0
  43. package/app/composables/useParallax.ts +32 -19
  44. package/app/composables/useScrollReveal.ts +20 -13
  45. package/app/pages/index.vue +230 -28
  46. package/componentList.json +1856 -238
  47. package/nuxt.config.ts +12 -3
  48. package/package.json +5 -2
  49. package/screenshots/playground-home.png +0 -0
@@ -2,17 +2,27 @@
2
2
  <div>
3
3
  <!-- Billing Toggle -->
4
4
  <div v-if="hasBillingToggle" class="flex justify-center mb-12">
5
- <div class="inline-flex items-center gap-4 rounded-full bg-neutral-100 dark:bg-neutral-900 p-1">
5
+ <div
6
+ class="inline-flex items-center gap-4 rounded-full bg-neutral-100 dark:bg-neutral-900 p-1"
7
+ >
6
8
  <button
7
9
  class="px-4 py-2 rounded-full text-sm font-medium transition-colors"
8
- :class="billingPeriod === 'monthly' ? 'bg-white dark:bg-neutral-800 shadow-sm' : 'text-neutral-600 dark:text-neutral-400'"
10
+ :class="
11
+ billingPeriod === 'monthly'
12
+ ? 'bg-white dark:bg-neutral-800 shadow-sm'
13
+ : 'text-neutral-600 dark:text-neutral-400'
14
+ "
9
15
  @click="billingPeriod = 'monthly'"
10
16
  >
11
17
  Monthly
12
18
  </button>
13
19
  <button
14
20
  class="px-4 py-2 rounded-full text-sm font-medium transition-colors"
15
- :class="billingPeriod === 'yearly' ? 'bg-white dark:bg-neutral-800 shadow-sm' : 'text-neutral-600 dark:text-neutral-400'"
21
+ :class="
22
+ billingPeriod === 'yearly'
23
+ ? 'bg-white dark:bg-neutral-800 shadow-sm'
24
+ : 'text-neutral-600 dark:text-neutral-400'
25
+ "
16
26
  @click="billingPeriod = 'yearly'"
17
27
  >
18
28
  Yearly
@@ -65,15 +75,52 @@
65
75
 
66
76
  <script setup>
67
77
  const props = defineProps({
68
- /** Array of pricing plans */
78
+ /** Array of pricing plans [{ name, price, period?, description?, features, button?, popular? }] */
69
79
  plans: {
70
80
  type: Array,
71
- required: true,
81
+ default: () => [
82
+ {
83
+ name: "Starter",
84
+ price: "$9",
85
+ period: "/month",
86
+ description: "Perfect for individuals and small projects.",
87
+ features: ["5 projects", "10GB storage", "Email support"],
88
+ button: { label: "Get Started" },
89
+ },
90
+ {
91
+ name: "Pro",
92
+ price: "$29",
93
+ period: "/month",
94
+ description: "For growing teams that need more power.",
95
+ features: [
96
+ "Unlimited projects",
97
+ "100GB storage",
98
+ "Priority support",
99
+ "Advanced analytics",
100
+ ],
101
+ button: { label: "Get Pro" },
102
+ popular: true,
103
+ },
104
+ {
105
+ name: "Enterprise",
106
+ price: "$99",
107
+ period: "/month",
108
+ description: "For large organizations with custom needs.",
109
+ features: [
110
+ "Unlimited everything",
111
+ "1TB storage",
112
+ "Dedicated support",
113
+ "Custom integrations",
114
+ "SLA guarantee",
115
+ ],
116
+ button: { label: "Contact Sales" },
117
+ },
118
+ ],
72
119
  },
73
120
  /** Plan name to highlight (alternative to setting popular: true on plan) */
74
121
  highlighted: {
75
122
  type: String,
76
- default: '',
123
+ default: "",
77
124
  },
78
125
  /** Has monthly/yearly billing toggle */
79
126
  hasBillingToggle: {
@@ -99,7 +146,7 @@ const props = defineProps({
99
146
  variant: {
100
147
  type: String,
101
148
  default: undefined,
102
- validator: (v) => ['soft', 'solid', 'outline', 'subtle'].includes(v),
149
+ validator: (v) => ["soft", "solid", "outline", "subtle"].includes(v),
103
150
  },
104
151
  /** Social proof badge presets or custom badges */
105
152
  badges: {
@@ -109,33 +156,36 @@ const props = defineProps({
109
156
  /** Position of badges */
110
157
  badgesPosition: {
111
158
  type: String,
112
- default: 'bottom',
113
- validator: (v) => ['top', 'bottom'].includes(v),
159
+ default: "bottom",
160
+ validator: (v) => ["top", "bottom"].includes(v),
114
161
  },
115
162
  /** Alignment of badges */
116
163
  badgesAlign: {
117
164
  type: String,
118
- default: 'center',
119
- validator: (v) => ['left', 'center', 'right'].includes(v),
165
+ default: "center",
166
+ validator: (v) => ["left", "center", "right"].includes(v),
120
167
  },
121
168
  });
122
169
 
123
- const emit = defineEmits(['select']);
170
+ const emit = defineEmits(["select"]);
124
171
 
125
- const billingPeriod = ref('monthly');
172
+ const billingPeriod = ref("monthly");
126
173
 
127
174
  // Map XMark plan format to Nuxt UI format
128
175
  const mappedPlans = computed(() => {
129
176
  return props.plans.map((plan) => {
130
177
  // Get the price based on billing period
131
178
  let price = plan.price;
132
- let billingCycle = plan.period || '';
179
+ let billingCycle = plan.period || "";
133
180
 
134
- if (typeof plan.price === 'object') {
135
- price = billingPeriod.value === 'yearly' ? plan.price.yearly : plan.price.monthly;
136
- billingCycle = billingPeriod.value === 'yearly' ? '/year' : '/month';
181
+ if (typeof plan.price === "object") {
182
+ price =
183
+ billingPeriod.value === "yearly"
184
+ ? plan.price.yearly
185
+ : plan.price.monthly;
186
+ billingCycle = billingPeriod.value === "yearly" ? "/year" : "/month";
137
187
  } else if (props.hasBillingToggle && !plan.period) {
138
- billingCycle = billingPeriod.value === 'yearly' ? '/year' : '/month';
188
+ billingCycle = billingPeriod.value === "yearly" ? "/year" : "/month";
139
189
  }
140
190
 
141
191
  // Determine if this plan should be highlighted
@@ -152,10 +202,10 @@ const mappedPlans = computed(() => {
152
202
  features: plan.features,
153
203
  highlight: isHighlighted,
154
204
  scale: shouldScale,
155
- badge: isHighlighted ? 'Most Popular' : undefined,
205
+ badge: isHighlighted ? "Most Popular" : undefined,
156
206
  button: {
157
- label: plan.button?.label || 'Get Started',
158
- onClick: () => emit('select', plan),
207
+ label: plan.button?.label || "Get Started",
208
+ onClick: () => emit("select", plan),
159
209
  },
160
210
  };
161
211
  });
@@ -164,16 +214,16 @@ const mappedPlans = computed(() => {
164
214
  // Badge alignment classes
165
215
  const badgesAlignClasses = computed(() => {
166
216
  const alignments = {
167
- left: 'justify-start',
168
- center: 'justify-center',
169
- right: 'justify-end',
217
+ left: "justify-start",
218
+ center: "justify-center",
219
+ right: "justify-end",
170
220
  };
171
221
  return alignments[props.badgesAlign];
172
222
  });
173
223
 
174
224
  // Get badge props (handles string presets and object configs)
175
225
  const getBadgeProps = (badge) => {
176
- if (typeof badge === 'string') {
226
+ if (typeof badge === "string") {
177
227
  return { preset: badge };
178
228
  }
179
229
  return badge;
@@ -9,14 +9,20 @@
9
9
  class="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-4 lg:-translate-x-16 w-11 h-11 flex items-center justify-center rounded-full bg-white dark:bg-neutral-800 shadow-lg border border-neutral-200 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-all duration-300 opacity-60 hover:opacity-100"
10
10
  @click="prev"
11
11
  >
12
- <UIcon name="i-lucide-chevron-left" class="w-5 h-5 text-neutral-700 dark:text-neutral-300" />
12
+ <UIcon
13
+ name="i-lucide-chevron-left"
14
+ class="w-5 h-5 text-neutral-700 dark:text-neutral-300"
15
+ />
13
16
  </button>
14
17
  <button
15
18
  v-if="testimonials.length > 1"
16
19
  class="absolute right-0 top-1/2 -translate-y-1/2 translate-x-4 lg:translate-x-16 w-11 h-11 flex items-center justify-center rounded-full bg-white dark:bg-neutral-800 shadow-lg border border-neutral-200 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-all duration-300 opacity-60 hover:opacity-100"
17
20
  @click="next"
18
21
  >
19
- <UIcon name="i-lucide-chevron-right" class="w-5 h-5 text-neutral-700 dark:text-neutral-300" />
22
+ <UIcon
23
+ name="i-lucide-chevron-right"
24
+ class="w-5 h-5 text-neutral-700 dark:text-neutral-300"
25
+ />
20
26
  </button>
21
27
 
22
28
  <!-- Quote Icon -->
@@ -26,25 +32,38 @@
26
32
 
27
33
  <!-- Quote with crossfade -->
28
34
  <Transition name="fade" mode="out-in">
29
- <blockquote :key="currentIndex" class="xText-headline text-neutral-900 dark:text-white xText-balance">
35
+ <blockquote
36
+ :key="currentIndex"
37
+ class="xText-headline text-neutral-900 dark:text-white xText-balance"
38
+ >
30
39
  "{{ currentTestimonial.quote }}"
31
40
  </blockquote>
32
41
  </Transition>
33
42
 
34
43
  <!-- Stars -->
35
- <div v-if="hasStars && currentTestimonial.rating" class="flex justify-center gap-1 mt-6">
44
+ <div
45
+ v-if="hasStars && currentTestimonial.rating"
46
+ class="flex justify-center gap-1 mt-6"
47
+ >
36
48
  <UIcon
37
49
  v-for="i in 5"
38
50
  :key="i"
39
51
  name="i-lucide-star"
40
52
  class="w-5 h-5"
41
- :class="i <= currentTestimonial.rating ? 'text-yellow-400' : 'text-neutral-300 dark:text-neutral-600'"
53
+ :class="
54
+ i <= currentTestimonial.rating
55
+ ? 'text-yellow-400'
56
+ : 'text-neutral-300 dark:text-neutral-600'
57
+ "
42
58
  />
43
59
  </div>
44
60
 
45
61
  <!-- Attribution with crossfade -->
46
62
  <Transition name="fade" mode="out-in">
47
- <div :key="currentIndex" class="mt-8 flex items-center justify-center gap-4">
63
+ <div
64
+ :key="currentIndex"
65
+ class="mt-8 flex items-center justify-center gap-4"
66
+ >
48
67
  <div class="relative">
49
68
  <img
50
69
  v-if="currentTestimonial.avatar"
@@ -52,7 +71,9 @@
52
71
  :alt="currentTestimonial.name"
53
72
  class="w-14 h-14 rounded-full object-cover ring-2 ring-white dark:ring-neutral-800 shadow-lg"
54
73
  />
55
- <div class="absolute inset-0 rounded-full ring-2 ring-primary-500/20 ring-offset-2 ring-offset-white dark:ring-offset-neutral-950" />
74
+ <div
75
+ class="absolute inset-0 rounded-full ring-2 ring-primary-500/20 ring-offset-2 ring-offset-white dark:ring-offset-neutral-950"
76
+ />
56
77
  </div>
57
78
  <div class="text-left">
58
79
  <p class="font-semibold text-neutral-900 dark:text-white">
@@ -83,7 +104,11 @@
83
104
  v-for="(_, index) in testimonials"
84
105
  :key="index"
85
106
  class="w-2 h-2 rounded-full transition-colors duration-300"
86
- :class="index === currentIndex ? 'bg-primary-500' : 'bg-neutral-300 dark:bg-neutral-700 hover:bg-neutral-400 dark:hover:bg-neutral-600'"
107
+ :class="
108
+ index === currentIndex
109
+ ? 'bg-primary-500'
110
+ : 'bg-neutral-300 dark:bg-neutral-700 hover:bg-neutral-400 dark:hover:bg-neutral-600'
111
+ "
87
112
  @click="goToIndex(index)"
88
113
  />
89
114
  </div>
@@ -106,7 +131,11 @@
106
131
  :key="i"
107
132
  name="i-lucide-star"
108
133
  class="w-5 h-5"
109
- :class="i <= (testimonial.rating || 5) ? 'text-yellow-400' : 'text-neutral-300 dark:text-neutral-600'"
134
+ :class="
135
+ i <= (testimonial.rating || 5)
136
+ ? 'text-yellow-400'
137
+ : 'text-neutral-300 dark:text-neutral-600'
138
+ "
110
139
  />
111
140
  </div>
112
141
 
@@ -137,7 +166,9 @@
137
166
  </p>
138
167
  <p class="text-xs text-neutral-500">
139
168
  {{ testimonial.title }}
140
- <span v-if="testimonial.company"> at {{ testimonial.company }}</span>
169
+ <span v-if="testimonial.company">
170
+ at {{ testimonial.company }}</span
171
+ >
141
172
  </p>
142
173
  </div>
143
174
  </div>
@@ -150,7 +181,10 @@
150
181
  <div class="relative overflow-hidden">
151
182
  <div
152
183
  class="flex transition-transform duration-500"
153
- :style="{ transform: `translateX(-${currentIndex * 100}%)`, transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)' }"
184
+ :style="{
185
+ transform: `translateX(-${currentIndex * 100}%)`,
186
+ transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
187
+ }"
154
188
  >
155
189
  <article
156
190
  v-for="testimonial in testimonials"
@@ -165,7 +199,11 @@
165
199
  :key="i"
166
200
  name="i-lucide-star"
167
201
  class="w-5 h-5"
168
- :class="i <= (testimonial.rating || 5) ? 'text-yellow-400' : 'text-neutral-300'"
202
+ :class="
203
+ i <= (testimonial.rating || 5)
204
+ ? 'text-yellow-400'
205
+ : 'text-neutral-300'
206
+ "
169
207
  />
170
208
  </div>
171
209
 
@@ -220,7 +258,11 @@
220
258
  v-for="(_, index) in testimonials"
221
259
  :key="index"
222
260
  class="w-2 h-2 rounded-full transition-colors"
223
- :class="index === currentIndex ? 'bg-primary-500' : 'bg-neutral-300 dark:bg-neutral-700'"
261
+ :class="
262
+ index === currentIndex
263
+ ? 'bg-primary-500'
264
+ : 'bg-neutral-300 dark:bg-neutral-700'
265
+ "
224
266
  @click="goToIndex(index)"
225
267
  />
226
268
  </div>
@@ -231,19 +273,44 @@
231
273
 
232
274
  <script setup>
233
275
  // Initialize scroll reveal animations
234
- useScrollReveal()
276
+ useScrollReveal();
235
277
 
236
278
  const props = defineProps({
237
- /** Array of testimonials */
279
+ /** Array of testimonials [{ quote, name, title?, company?, avatar?, logo?, rating? }] */
238
280
  testimonials: {
239
281
  type: Array,
240
- required: true,
282
+ default: () => [
283
+ {
284
+ quote:
285
+ "This product completely transformed how our team works. Highly recommended.",
286
+ name: "Alex Johnson",
287
+ title: "CEO",
288
+ company: "Acme Inc",
289
+ rating: 5,
290
+ },
291
+ {
292
+ quote:
293
+ "The best investment we made this year. Setup was a breeze and results were immediate.",
294
+ name: "Sarah Chen",
295
+ title: "CTO",
296
+ company: "TechCorp",
297
+ rating: 5,
298
+ },
299
+ {
300
+ quote:
301
+ "Outstanding support and a product that actually delivers on its promises.",
302
+ name: "Marcus Williams",
303
+ title: "Product Manager",
304
+ company: "StartupXYZ",
305
+ rating: 5,
306
+ },
307
+ ],
241
308
  },
242
309
  /** Display layout */
243
310
  layout: {
244
311
  type: String,
245
- default: 'featured',
246
- validator: (v) => ['featured', 'grid', 'carousel'].includes(v),
312
+ default: "featured",
313
+ validator: (v) => ["featured", "grid", "carousel"].includes(v),
247
314
  },
248
315
  /** Show star ratings */
249
316
  hasStars: {
@@ -280,9 +347,10 @@ const next = () => {
280
347
  };
281
348
 
282
349
  const prev = () => {
283
- currentIndex.value = currentIndex.value === 0
284
- ? props.testimonials.length - 1
285
- : currentIndex.value - 1;
350
+ currentIndex.value =
351
+ currentIndex.value === 0
352
+ ? props.testimonials.length - 1
353
+ : currentIndex.value - 1;
286
354
  resetAutoplay();
287
355
  };
288
356