@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
@@ -1,151 +1,151 @@
1
1
  <template>
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
9
- v-if="isVisible"
10
- class="fixed bottom-6 right-6 z-40 max-w-sm"
11
- >
12
- <div class="xGlass rounded-2xl p-4 shadow-xl">
13
- <!-- Close button -->
14
- <button
15
- class="absolute top-2 right-2 p-1 rounded-full hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
16
- aria-label="Close"
17
- @click="dismiss"
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
- <!-- Content -->
23
- <div class="flex gap-3">
24
- <!-- Avatar -->
25
- <img
26
- v-if="avatar"
27
- :src="avatar"
28
- :alt="name"
29
- class="w-12 h-12 rounded-full object-cover flex-shrink-0"
30
- />
31
- <div
32
- v-else
33
- class="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center flex-shrink-0"
34
- >
35
- <span class="text-primary-600 dark:text-primary-400 font-medium text-lg">
36
- {{ name.charAt(0) }}
37
- </span>
38
- </div>
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
- <div class="flex-1 min-w-0">
41
- <!-- Header -->
42
- <div class="flex items-center gap-2 mb-1">
43
- <span class="font-medium text-gray-900 dark:text-white">
44
- {{ name }}
45
- </span>
46
- <span
47
- v-if="online"
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
- <!-- Message -->
53
- <p class="text-sm text-gray-600 dark:text-gray-400">
54
- {{ message }}
55
- </p>
48
+ <!-- Message -->
49
+ <p class="text-sm text-gray-600 dark:text-gray-400">
50
+ {{ message }}
51
+ </p>
56
52
 
57
- <!-- Action -->
58
- <UButton
59
- v-if="actionText"
60
- size="sm"
61
- class="mt-3"
62
- @click="handleAction"
63
- >
64
- {{ actionText }}
65
- </UButton>
66
- </div>
67
- </div>
68
- </div>
69
- </aside>
70
- </Transition>
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
- /** v-model visibility */
76
- modelValue: {
77
- type: Boolean,
78
- default: false,
79
- },
80
- /** Avatar image URL */
81
- avatar: {
82
- type: String,
83
- default: '',
84
- },
85
- /** Person/bot name */
86
- name: {
87
- type: String,
88
- required: true,
89
- },
90
- /** Chat message */
91
- message: {
92
- type: String,
93
- required: true,
94
- },
95
- /** Show online indicator */
96
- online: {
97
- type: Boolean,
98
- default: true,
99
- },
100
- /** Button text */
101
- actionText: {
102
- type: String,
103
- default: '',
104
- },
105
- /** Auto-dismiss after ms (0 = never) */
106
- autoDismiss: {
107
- type: Number,
108
- default: 0,
109
- },
110
- /** Delay before showing (ms) */
111
- delay: {
112
- type: Number,
113
- default: 3000,
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(['update:modelValue', 'action', 'dismiss']);
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
- isVisible.value = false;
125
- emit('update:modelValue', false);
126
- emit('dismiss');
120
+ isVisible.value = false;
121
+ emit("update:modelValue", false);
122
+ emit("dismiss");
127
123
  };
128
124
 
129
125
  const handleAction = () => {
130
- emit('action');
126
+ emit("action");
131
127
  };
132
128
 
133
- watch(() => props.modelValue, (val) => {
134
- if (val) {
135
- showTimeout = setTimeout(() => {
136
- isVisible.value = true;
129
+ watch(
130
+ () => props.modelValue,
131
+ (val) => {
132
+ if (val) {
133
+ showTimeout = setTimeout(() => {
134
+ isVisible.value = true;
137
135
 
138
- if (props.autoDismiss > 0) {
139
- dismissTimeout = setTimeout(dismiss, props.autoDismiss);
140
- }
141
- }, props.delay);
142
- } else {
143
- isVisible.value = false;
144
- }
145
- }, { immediate: true });
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
- if (dismissTimeout) clearTimeout(dismissTimeout);
149
- if (showTimeout) clearTimeout(showTimeout);
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 class="absolute inset-0 bg-black/40 flex items-center justify-center group-hover:bg-black/50 transition-colors">
24
- <div class="w-16 h-16 rounded-full bg-white/90 flex items-center justify-center shadow-lg">
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 class="aspect-video bg-black overflow-hidden" :class="title || description ? 'rounded-t-lg' : 'rounded-lg'">
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
- required: true,
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: '4xl',
118
- validator: (v) => ['lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl', 'full'].includes(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: 'en',
148
+ default: "en",
139
149
  },
140
150
  /** Captions label */
141
151
  captionsLabel: {
142
152
  type: String,
143
- default: 'English',
153
+ default: "English",
144
154
  },
145
155
  });
146
156
 
147
- const emit = defineEmits(['open', 'close']);
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 || 'Watch Video',
155
- color: props.button.color || 'primary',
156
- variant: props.button.variant || 'solid',
157
- size: props.button.size || 'md',
158
- icon: props.button.icon || 'i-lucide-play',
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('youtube.com') || props.src.includes('youtu.be')) {
164
- return 'youtube';
173
+ if (props.src.includes("youtube.com") || props.src.includes("youtu.be")) {
174
+ return "youtube";
165
175
  }
166
- if (props.src.includes('vimeo.com')) {
167
- return 'vimeo';
176
+ if (props.src.includes("vimeo.com")) {
177
+ return "vimeo";
168
178
  }
169
- return 'direct';
179
+ return "direct";
170
180
  });
171
181
 
172
182
  // Generate embed URL
173
183
  const embedUrl = computed(() => {
174
- const autoplayParam = props.autoplay ? '1' : '0';
184
+ const autoplayParam = props.autoplay ? "1" : "0";
175
185
 
176
- if (embedType.value === 'youtube') {
177
- let videoId = '';
178
- if (props.src.includes('youtu.be/')) {
179
- videoId = props.src.split('youtu.be/')[1]?.split('?')[0];
180
- } else if (props.src.includes('v=')) {
181
- videoId = props.src.split('v=')[1]?.split('&')[0];
182
- } else if (props.src.includes('embed/')) {
183
- videoId = props.src.split('embed/')[1]?.split('?')[0];
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 === 'vimeo') {
189
- const videoId = props.src.split('vimeo.com/')[1]?.split('?')[0];
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('close');
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
- required: true,
46
- // Each plan: { id, name, price?, highlighted?, button? }
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
- required: true,
52
- // Each group: { name?, features: [{ name, tooltip?, values: { [planId]: boolean|string } }] }
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: 'bottom',
68
- validator: (v) => ['top', 'bottom'].includes(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: 'center',
74
- validator: (v) => ['left', 'center', 'right'].includes(v),
126
+ default: "center",
127
+ validator: (v) => ["left", "center", "right"].includes(v),
75
128
  },
76
129
  });
77
130
 
78
- const emit = defineEmits(['select']);
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
- label: plan.button.label || 'Get Started',
90
- color: plan.highlighted ? 'primary' : 'neutral',
91
- variant: plan.highlighted ? 'solid' : 'outline',
92
- onClick: () => emit('select', plan),
93
- } : undefined,
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: 'justify-start',
113
- center: 'justify-center',
114
- right: 'justify-end',
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 === 'string') {
176
+ if (typeof badge === "string") {
122
177
  return { preset: badge };
123
178
  }
124
179
  return badge;