@xenterprises/nuxt-x-marketing 0.0.23 → 1.0.0

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 (115) hide show
  1. package/.claude/settings.local.json +22 -0
  2. package/.playground/app.config.ts +44 -45
  3. package/.playground/app.vue +263 -0
  4. package/.playground/components/AppFooter.vue +103 -0
  5. package/.playground/components/AppHeader.vue +41 -0
  6. package/.playground/components/CodeExample.vue +70 -0
  7. package/.playground/components/ComponentPlayground.vue +117 -0
  8. package/.playground/components/DocsHeader.vue +19 -0
  9. package/.playground/layouts/default.vue +13 -3
  10. package/.playground/layouts/docs-layout.vue +161 -0
  11. package/.playground/layouts/docs.vue +11 -0
  12. package/.playground/package-lock.json +6 -0
  13. package/.playground/pages/blog/author.vue +145 -0
  14. package/.playground/pages/blog/card.vue +149 -0
  15. package/.playground/pages/blog/cta.vue +131 -0
  16. package/.playground/pages/blog/detail.vue +379 -0
  17. package/.playground/pages/blog/list.vue +261 -0
  18. package/.playground/pages/blog/navigation.vue +228 -0
  19. package/.playground/pages/blog/sidebar.vue +279 -0
  20. package/.playground/pages/config.vue +289 -0
  21. package/.playground/pages/content/comparison.vue +253 -0
  22. package/.playground/pages/content/cta.vue +245 -0
  23. package/.playground/pages/content/features.vue +217 -0
  24. package/.playground/pages/content/newsletter-section.vue +260 -0
  25. package/.playground/pages/content/newsletter.vue +200 -0
  26. package/.playground/pages/content/pricing.vue +335 -0
  27. package/.playground/pages/content/testimonials.vue +188 -0
  28. package/.playground/pages/hero.vue +257 -0
  29. package/.playground/pages/index.vue +379 -2
  30. package/.playground/pages/layout/footer.vue +210 -0
  31. package/.playground/pages/layout/legal-footer.vue +121 -0
  32. package/.playground/pages/layout/navbar.vue +285 -0
  33. package/.playground/pages/layout/section.vue +266 -0
  34. package/.playground/pages/layout/x-legal.vue +199 -0
  35. package/.playground/pages/modals/feature-modal.vue +320 -0
  36. package/.playground/pages/modals/image-lightbox.vue +277 -0
  37. package/.playground/pages/modals/video-modal.vue +222 -0
  38. package/.playground/pages/notifications/announcement-bar.vue +164 -0
  39. package/.playground/pages/notifications/cookie-banner.vue +210 -0
  40. package/.playground/pages/notifications/cookie-toast.vue +143 -0
  41. package/.playground/pages/notifications/gdpr.vue +276 -0
  42. package/.playground/pages/notifications/social-proof-toast.vue +255 -0
  43. package/.playground/pages/ui/badge.vue +161 -0
  44. package/.playground/pages/ui/bg-pattern.vue +177 -0
  45. package/.playground/pages/ui/glass-card.vue +179 -0
  46. package/.playground/pages/ui/glow-divider.vue +179 -0
  47. package/.playground/pages/ui/promo-card.vue +259 -0
  48. package/.playground/pages/ui/section-stitch.vue +411 -0
  49. package/.playground/pages/utilities/back-to-top.vue +153 -0
  50. package/.playground/pages/utilities/popup-chat.vue +224 -0
  51. package/README.md +799 -164
  52. package/app/app.config.ts +88 -2
  53. package/app/app.vue +106 -11
  54. package/app/assets/css/x-marketing.css +668 -0
  55. package/app/components/X/Footer/XLegal/index.vue +63 -26
  56. package/app/components/X/Footer/index.vue +89 -89
  57. package/app/components/X/Header/App.vue +63 -0
  58. package/app/components/X/Mark/Blog/Author.vue +170 -0
  59. package/app/components/X/Mark/Blog/CTANewsletter.vue +197 -0
  60. package/app/components/X/Mark/Blog/Card.vue +117 -0
  61. package/app/components/X/Mark/Blog/Detail.vue +240 -0
  62. package/app/components/X/Mark/Blog/List.vue +156 -0
  63. package/app/components/X/Mark/Blog/Navigation.vue +100 -0
  64. package/app/components/X/Mark/Blog/Sidebar.vue +160 -0
  65. package/app/components/X/Mark/Button/BackToTop.vue +99 -0
  66. package/app/components/X/Mark/CTA/index.vue +294 -0
  67. package/app/components/X/Mark/Card/Glass.vue +64 -0
  68. package/app/components/X/Mark/Card/Promo.vue +164 -0
  69. package/app/components/X/Mark/Divider/Glow.vue +113 -0
  70. package/app/components/X/Mark/Features/index.vue +194 -0
  71. package/app/components/X/Mark/Hero/index.vue +217 -0
  72. package/app/components/X/Mark/Layout/AnnouncementBar.vue +139 -0
  73. package/app/components/X/Mark/Layout/Footer.vue +160 -0
  74. package/app/components/X/Mark/Layout/FooterLegal.vue +42 -0
  75. package/app/components/X/Mark/Layout/Navbar.vue +267 -0
  76. package/app/components/X/Mark/Modal/Chat.vue +151 -0
  77. package/app/components/X/Mark/Modal/Feature.vue +197 -0
  78. package/app/components/X/Mark/Modal/Image.vue +226 -0
  79. package/app/components/X/Mark/Modal/Video.vue +203 -0
  80. package/app/components/X/Mark/Modal/index.vue +133 -0
  81. package/app/components/X/Mark/Newsletter/Form.vue +174 -0
  82. package/app/components/X/Mark/Pattern/Bg.vue +86 -0
  83. package/app/components/X/Mark/Pricing/Comparison.vue +126 -0
  84. package/app/components/X/Mark/Pricing/Plans.vue +181 -0
  85. package/app/components/X/Mark/Privacy/CookieBanner.vue +112 -0
  86. package/app/components/X/Mark/Privacy/CookieToast.vue +43 -0
  87. package/app/components/X/Mark/Privacy/GDPR.vue +245 -0
  88. package/app/components/X/Mark/Section/Newsletter.vue +183 -0
  89. package/app/components/X/Mark/Section/Stitch.vue +156 -0
  90. package/app/components/X/Mark/Section/index.vue +151 -0
  91. package/app/components/X/Mark/SocialProof/Badge.vue +102 -0
  92. package/app/components/X/Mark/SocialProof/Testimonials.vue +326 -0
  93. package/app/components/X/Mark/SocialProof/Toast.vue +136 -0
  94. package/app/components/X/Mark/_archive/Pattern/Background.vue +83 -0
  95. package/app/components/X/Mark/_archive/Pattern/CheckBg.vue +35 -0
  96. package/app/components/X/Mark/_archive/Pattern/CircuitBg.vue +41 -0
  97. package/app/components/X/Mark/_archive/Pattern/DotsBg.vue +35 -0
  98. package/app/components/X/Mark/_archive/Pattern/GlowDivider.vue +86 -0
  99. package/app/components/X/Mark/_archive/Pattern/HexBg.vue +35 -0
  100. package/app/components/X/Mark/_archive/Pattern/PlaidBg.vue +47 -0
  101. package/app/components/X/Mark/_archive/Pattern/SectionStitch.vue +168 -0
  102. package/app/components/X/Mark/_archive/Pattern/WaveBg.vue +41 -0
  103. package/app/components/X/_archive/Newsletter/Form.vue +123 -0
  104. package/app/components/X/_archive/Newsletter/Section.vue +131 -0
  105. package/app/composables/useParallax.ts +124 -0
  106. package/app/composables/useScrollReveal.ts +92 -0
  107. package/app/pages/index.vue +210 -0
  108. package/app/plugins/marketing.client.ts +13 -0
  109. package/app/types/marketing.d.ts +268 -0
  110. package/componentList.json +511 -0
  111. package/nuxt.config.ts +6 -13
  112. package/package.json +21 -9
  113. package/.editorconfig +0 -12
  114. package/app/assets/css/main.css +0 -2
  115. package/app/components/X/Marketing/Sections/EmailSubscription/index.vue +0 -129
@@ -0,0 +1,151 @@
1
+ <template>
2
+ <section
3
+ :class="[
4
+ 'relative overflow-hidden',
5
+ paddingClasses,
6
+ bgColorClasses
7
+ ]"
8
+ >
9
+ <!-- Background Pattern -->
10
+ <XMarkPatternBg
11
+ v-if="resolvedBg.pattern"
12
+ :pattern="resolvedBg.pattern"
13
+ :opacity="resolvedBg.patternOpacity"
14
+ class="absolute inset-0 pointer-events-none"
15
+ />
16
+
17
+ <!-- Background Image -->
18
+ <div
19
+ v-if="resolvedBg.image"
20
+ class="absolute inset-0"
21
+ :class="{ 'xParallax': resolvedBg.parallax }"
22
+ :data-parallax-speed="resolvedBg.parallaxSpeed"
23
+ >
24
+ <img
25
+ :src="resolvedBg.image"
26
+ :alt="resolvedBg.imageAlt"
27
+ class="w-full h-full object-cover"
28
+ :class="resolvedBg.imagePosition"
29
+ />
30
+ <div
31
+ v-if="resolvedBg.imageOverlay"
32
+ class="absolute inset-0"
33
+ :class="bgImageOverlayClass"
34
+ />
35
+ </div>
36
+
37
+ <!-- Custom Background Slot -->
38
+ <slot name="background" />
39
+
40
+ <!-- Content -->
41
+ <div
42
+ class="relative z-10"
43
+ :class="containerClasses"
44
+ >
45
+ <slot />
46
+ </div>
47
+ </section>
48
+ </template>
49
+
50
+ <script setup>
51
+ const props = defineProps({
52
+ /**
53
+ * Background configuration object
54
+ * @property {string} variant - 'default' | 'subtle' | 'elevated' | 'bold' | 'transparent'
55
+ * @property {string} color - Custom Tailwind classes (overrides variant)
56
+ * @property {string} image - Background image URL
57
+ * @property {string} imageAlt - Background image alt text
58
+ * @property {string} imageOverlay - 'light' | 'heavy' | 'gradient'
59
+ * @property {string} imagePosition - Tailwind object-position class
60
+ * @property {string} pattern - 'dots' | 'grid' | 'diagonal' | 'topography' | 'circuit' | 'waves'
61
+ * @property {number} patternOpacity - Pattern opacity (0-1)
62
+ * @property {boolean} parallax - Enable parallax on bg image
63
+ * @property {number} parallaxSpeed - Parallax intensity (0.1 - 1.0)
64
+ */
65
+ bg: {
66
+ type: [Object, String],
67
+ default: () => ({}),
68
+ },
69
+ /** Vertical padding size */
70
+ padding: {
71
+ type: String,
72
+ default: 'lg',
73
+ validator: (v) => ['none', 'xs', 'sm', 'md', 'lg', 'xl'].includes(v),
74
+ },
75
+ /** Max-width container size */
76
+ container: {
77
+ type: String,
78
+ default: 'lg',
79
+ validator: (v) => ['sm', 'md', 'lg', 'xl', 'full', 'none'].includes(v),
80
+ },
81
+ });
82
+
83
+ // Default bg values
84
+ const bgDefaults = {
85
+ variant: 'default',
86
+ color: '',
87
+ image: '',
88
+ imageAlt: '',
89
+ imageOverlay: '',
90
+ imagePosition: 'object-center',
91
+ pattern: '',
92
+ patternOpacity: 0.05,
93
+ parallax: false,
94
+ parallaxSpeed: 0.3,
95
+ };
96
+
97
+ // Resolve bg prop (supports string shorthand for variant or full object)
98
+ const resolvedBg = computed(() => {
99
+ if (typeof props.bg === 'string') {
100
+ return { ...bgDefaults, variant: props.bg };
101
+ }
102
+ return { ...bgDefaults, ...props.bg };
103
+ });
104
+
105
+ const paddingClasses = computed(() => {
106
+ const sizes = {
107
+ none: '',
108
+ xs: 'py-8 md:py-10',
109
+ sm: 'py-12 md:py-16',
110
+ md: 'py-16 md:py-20',
111
+ lg: 'py-20 md:py-28 lg:py-32',
112
+ xl: 'py-28 md:py-36 lg:py-40',
113
+ };
114
+ return sizes[props.padding];
115
+ });
116
+
117
+ const bgColorClasses = computed(() => {
118
+ // Custom color class takes priority
119
+ if (resolvedBg.value.color) return resolvedBg.value.color;
120
+
121
+ const colors = {
122
+ default: 'bg-white dark:bg-neutral-950',
123
+ subtle: 'bg-neutral-50 dark:bg-neutral-900',
124
+ elevated: 'bg-primary-50/50 dark:bg-primary-950/20',
125
+ bold: 'bg-neutral-950 dark:bg-white text-white dark:text-neutral-950',
126
+ transparent: 'bg-transparent',
127
+ };
128
+ return colors[resolvedBg.value.variant] || colors.default;
129
+ });
130
+
131
+ const containerClasses = computed(() => {
132
+ const containers = {
133
+ sm: 'max-w-[42rem] mx-auto px-4 sm:px-6',
134
+ md: 'max-w-[64rem] mx-auto px-4 sm:px-6 lg:px-8',
135
+ lg: 'max-w-[80rem] mx-auto px-4 sm:px-6 lg:px-8',
136
+ xl: 'max-w-[96rem] mx-auto px-4 sm:px-6 lg:px-8',
137
+ full: 'w-full px-4 sm:px-6 lg:px-8',
138
+ none: '',
139
+ };
140
+ return containers[props.container];
141
+ });
142
+
143
+ const bgImageOverlayClass = computed(() => {
144
+ const overlays = {
145
+ light: 'bg-black/30',
146
+ heavy: 'bg-black/60',
147
+ gradient: 'bg-gradient-to-t from-black/80 via-black/40 to-transparent',
148
+ };
149
+ return overlays[resolvedBg.value.imageOverlay] || '';
150
+ });
151
+ </script>
@@ -0,0 +1,102 @@
1
+ <template>
2
+ <span
3
+ :class="[
4
+ 'inline-flex items-center gap-2 text-sm font-medium',
5
+ variant === 'text' ? 'px-0 py-0' : 'px-3 py-1.5 rounded-full',
6
+ variantClasses
7
+ ]"
8
+ >
9
+ <UIcon :name="iconName" class="w-4 h-4" />
10
+ <span>{{ label }}</span>
11
+ </span>
12
+ </template>
13
+
14
+ <script>
15
+ // Module-scope constant for use in defineProps validator
16
+ const presets = {
17
+ 'no-credit-card': {
18
+ icon: 'i-lucide-credit-card',
19
+ label: 'No credit card required',
20
+ },
21
+ 'cancel-anytime': {
22
+ icon: 'i-lucide-x-circle',
23
+ label: 'Cancel anytime',
24
+ },
25
+ 'free-trial': {
26
+ icon: 'i-lucide-gift',
27
+ label: 'Free 14-day trial',
28
+ },
29
+ 'secure': {
30
+ icon: 'i-lucide-shield-check',
31
+ label: 'Bank-level security',
32
+ },
33
+ 'support': {
34
+ icon: 'i-lucide-message-circle',
35
+ label: '24/7 support',
36
+ },
37
+ 'setup': {
38
+ icon: 'i-lucide-zap',
39
+ label: 'Setup in minutes',
40
+ },
41
+ 'gdpr': {
42
+ icon: 'i-lucide-lock',
43
+ label: 'GDPR compliant',
44
+ },
45
+ 'money-back': {
46
+ icon: 'i-lucide-undo-2',
47
+ label: '30-day money back',
48
+ },
49
+ }
50
+ </script>
51
+
52
+ <script setup>
53
+ const props = defineProps({
54
+ /** Use predefined badge preset */
55
+ preset: {
56
+ type: String,
57
+ default: '',
58
+ validator: (v) => v === '' || Object.keys(presets).includes(v),
59
+ },
60
+ /** Custom icon name (if not using preset) */
61
+ icon: {
62
+ type: String,
63
+ default: '',
64
+ },
65
+ /** Custom label (if not using preset) */
66
+ label: {
67
+ type: String,
68
+ default: '',
69
+ },
70
+ /** Style variant */
71
+ variant: {
72
+ type: String,
73
+ default: 'default',
74
+ validator: (v) => ['default', 'subtle', 'outline', 'text', 'text-muted'].includes(v),
75
+ },
76
+ });
77
+
78
+ const iconName = computed(() => {
79
+ if (props.preset && presets[props.preset]) {
80
+ return presets[props.preset].icon;
81
+ }
82
+ return props.icon || 'i-lucide-check-circle';
83
+ });
84
+
85
+ const label = computed(() => {
86
+ if (props.preset && presets[props.preset]) {
87
+ return presets[props.preset].label;
88
+ }
89
+ return props.label;
90
+ });
91
+
92
+ const variantClasses = computed(() => {
93
+ const variants = {
94
+ default: 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300',
95
+ subtle: 'bg-neutral-100 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300',
96
+ outline: 'border border-neutral-300 dark:border-neutral-700 text-neutral-700 dark:text-neutral-300',
97
+ text: 'text-neutral-600 dark:text-neutral-400',
98
+ 'text-muted': 'text-neutral-400 dark:text-neutral-500',
99
+ };
100
+ return variants[props.variant];
101
+ });
102
+ </script>
@@ -0,0 +1,326 @@
1
+ <template>
2
+ <div>
3
+ <!-- Single Featured -->
4
+ <template v-if="layout === 'featured'">
5
+ <div class="relative max-w-4xl mx-auto text-center">
6
+ <!-- Navigation Arrows -->
7
+ <button
8
+ v-if="testimonials.length > 1"
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
+ @click="prev"
11
+ >
12
+ <UIcon name="i-lucide-chevron-left" class="w-5 h-5 text-neutral-700 dark:text-neutral-300" />
13
+ </button>
14
+ <button
15
+ v-if="testimonials.length > 1"
16
+ 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
+ @click="next"
18
+ >
19
+ <UIcon name="i-lucide-chevron-right" class="w-5 h-5 text-neutral-700 dark:text-neutral-300" />
20
+ </button>
21
+
22
+ <!-- Quote Icon -->
23
+ <!-- <div class="relative inline-block mb-8">
24
+ <span class="text-8xl font-serif text-primary-500/20 select-none leading-none serif">"</span>
25
+ </div> -->
26
+
27
+ <!-- Quote with crossfade -->
28
+ <Transition name="fade" mode="out-in">
29
+ <blockquote :key="currentIndex" class="xText-headline text-neutral-900 dark:text-white xText-balance">
30
+ "{{ currentTestimonial.quote }}"
31
+ </blockquote>
32
+ </Transition>
33
+
34
+ <!-- Stars -->
35
+ <div v-if="hasStars && currentTestimonial.rating" class="flex justify-center gap-1 mt-6">
36
+ <UIcon
37
+ v-for="i in 5"
38
+ :key="i"
39
+ name="i-lucide-star"
40
+ class="w-5 h-5"
41
+ :class="i <= currentTestimonial.rating ? 'text-yellow-400' : 'text-neutral-300 dark:text-neutral-600'"
42
+ />
43
+ </div>
44
+
45
+ <!-- Attribution with crossfade -->
46
+ <Transition name="fade" mode="out-in">
47
+ <div :key="currentIndex" class="mt-8 flex items-center justify-center gap-4">
48
+ <div class="relative">
49
+ <img
50
+ v-if="currentTestimonial.avatar"
51
+ :src="currentTestimonial.avatar"
52
+ :alt="currentTestimonial.name"
53
+ class="w-14 h-14 rounded-full object-cover ring-2 ring-white dark:ring-neutral-800 shadow-lg"
54
+ />
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" />
56
+ </div>
57
+ <div class="text-left">
58
+ <p class="font-semibold text-neutral-900 dark:text-white">
59
+ {{ currentTestimonial.name }}
60
+ </p>
61
+ <p class="text-sm text-neutral-600 dark:text-neutral-400">
62
+ {{ currentTestimonial.title }}
63
+ <span v-if="currentTestimonial.company">
64
+ at {{ currentTestimonial.company }}
65
+ </span>
66
+ </p>
67
+ </div>
68
+ <img
69
+ v-if="currentTestimonial.logo"
70
+ :src="currentTestimonial.logo"
71
+ :alt="currentTestimonial.company"
72
+ class="h-8 w-auto ml-4 opacity-60"
73
+ />
74
+ </div>
75
+ </Transition>
76
+
77
+ <!-- Navigation Dots -->
78
+ <div
79
+ v-if="testimonials.length > 1"
80
+ class="mt-8 flex justify-center gap-2"
81
+ >
82
+ <button
83
+ v-for="(_, index) in testimonials"
84
+ :key="index"
85
+ 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'"
87
+ @click="goToIndex(index)"
88
+ />
89
+ </div>
90
+ </div>
91
+ </template>
92
+
93
+ <!-- Grid Layout -->
94
+ <template v-else-if="layout === 'grid'">
95
+ <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
96
+ <article
97
+ v-for="(testimonial, index) in testimonials"
98
+ :key="testimonial.name"
99
+ class="xGlass rounded-2xl p-6 xFadeUp transition-all duration-300 ease-out hover:-translate-y-1 hover:shadow-[0_12px_40px_rgba(128,128,128,0.2)] dark:hover:shadow-[0_12px_40px_rgba(255,255,255,0.1)]"
100
+ :style="{ transitionDelay: `${index * 100}ms` }"
101
+ >
102
+ <!-- Stars -->
103
+ <div v-if="hasStars" class="flex gap-1 mb-4">
104
+ <UIcon
105
+ v-for="i in 5"
106
+ :key="i"
107
+ name="i-lucide-star"
108
+ class="w-5 h-5"
109
+ :class="i <= (testimonial.rating || 5) ? 'text-yellow-400' : 'text-neutral-300 dark:text-neutral-600'"
110
+ />
111
+ </div>
112
+
113
+ <!-- Quote -->
114
+ <p class="text-neutral-700 dark:text-neutral-300">
115
+ "{{ testimonial.quote }}"
116
+ </p>
117
+
118
+ <!-- Attribution -->
119
+ <div class="mt-6 flex items-center gap-3">
120
+ <img
121
+ v-if="testimonial.avatar"
122
+ :src="testimonial.avatar"
123
+ :alt="testimonial.name"
124
+ class="w-10 h-10 rounded-full object-cover"
125
+ />
126
+ <div
127
+ v-else
128
+ class="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center"
129
+ >
130
+ <span class="text-primary-600 dark:text-primary-400 font-medium">
131
+ {{ testimonial.name.charAt(0) }}
132
+ </span>
133
+ </div>
134
+ <div>
135
+ <p class="font-medium text-neutral-900 dark:text-white text-sm">
136
+ {{ testimonial.name }}
137
+ </p>
138
+ <p class="text-xs text-neutral-500">
139
+ {{ testimonial.title }}
140
+ <span v-if="testimonial.company"> at {{ testimonial.company }}</span>
141
+ </p>
142
+ </div>
143
+ </div>
144
+ </article>
145
+ </div>
146
+ </template>
147
+
148
+ <!-- Carousel Layout -->
149
+ <template v-else-if="layout === 'carousel'">
150
+ <div class="relative overflow-hidden">
151
+ <div
152
+ class="flex transition-transform duration-500"
153
+ :style="{ transform: `translateX(-${currentIndex * 100}%)`, transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)' }"
154
+ >
155
+ <article
156
+ v-for="testimonial in testimonials"
157
+ :key="testimonial.name"
158
+ class="w-full flex-shrink-0 px-4"
159
+ >
160
+ <div class="max-w-2xl mx-auto text-center">
161
+ <!-- Stars -->
162
+ <div v-if="hasStars" class="flex justify-center gap-1 mb-6">
163
+ <UIcon
164
+ v-for="i in 5"
165
+ :key="i"
166
+ name="i-lucide-star"
167
+ class="w-5 h-5"
168
+ :class="i <= (testimonial.rating || 5) ? 'text-yellow-400' : 'text-neutral-300'"
169
+ />
170
+ </div>
171
+
172
+ <!-- Quote -->
173
+ <blockquote class="text-xl text-neutral-900 dark:text-white">
174
+ "{{ testimonial.quote }}"
175
+ </blockquote>
176
+
177
+ <!-- Attribution -->
178
+ <div class="mt-6 flex items-center justify-center gap-3">
179
+ <img
180
+ v-if="testimonial.avatar"
181
+ :src="testimonial.avatar"
182
+ :alt="testimonial.name"
183
+ class="w-12 h-12 rounded-full object-cover"
184
+ />
185
+ <div class="text-left">
186
+ <p class="font-medium text-neutral-900 dark:text-white">
187
+ {{ testimonial.name }}
188
+ </p>
189
+ <p class="text-sm text-neutral-500">
190
+ {{ testimonial.title }}
191
+ </p>
192
+ </div>
193
+ </div>
194
+ </div>
195
+ </article>
196
+ </div>
197
+
198
+ <!-- Navigation Arrows -->
199
+ <button
200
+ v-if="testimonials.length > 1"
201
+ class="absolute left-0 top-1/2 -translate-y-1/2 w-10 h-10 flex items-center justify-center rounded-full bg-white/80 dark:bg-neutral-800/80 shadow-lg hover:bg-white dark:hover:bg-neutral-800 transition-colors"
202
+ @click="prev"
203
+ >
204
+ <UIcon name="i-lucide-chevron-left" class="w-5 h-5" />
205
+ </button>
206
+ <button
207
+ v-if="testimonials.length > 1"
208
+ class="absolute right-0 top-1/2 -translate-y-1/2 w-10 h-10 flex items-center justify-center rounded-full bg-white/80 dark:bg-neutral-800/80 shadow-lg hover:bg-white dark:hover:bg-neutral-800 transition-colors"
209
+ @click="next"
210
+ >
211
+ <UIcon name="i-lucide-chevron-right" class="w-5 h-5" />
212
+ </button>
213
+
214
+ <!-- Dots -->
215
+ <div
216
+ v-if="testimonials.length > 1"
217
+ class="mt-8 flex justify-center gap-2"
218
+ >
219
+ <button
220
+ v-for="(_, index) in testimonials"
221
+ :key="index"
222
+ class="w-2 h-2 rounded-full transition-colors"
223
+ :class="index === currentIndex ? 'bg-primary-500' : 'bg-neutral-300 dark:bg-neutral-700'"
224
+ @click="goToIndex(index)"
225
+ />
226
+ </div>
227
+ </div>
228
+ </template>
229
+ </div>
230
+ </template>
231
+
232
+ <script setup>
233
+ // Initialize scroll reveal animations
234
+ useScrollReveal()
235
+
236
+ const props = defineProps({
237
+ /** Array of testimonials */
238
+ testimonials: {
239
+ type: Array,
240
+ required: true,
241
+ },
242
+ /** Display layout */
243
+ layout: {
244
+ type: String,
245
+ default: 'featured',
246
+ validator: (v) => ['featured', 'grid', 'carousel'].includes(v),
247
+ },
248
+ /** Show star ratings */
249
+ hasStars: {
250
+ type: Boolean,
251
+ default: true,
252
+ },
253
+ /** Auto-rotate (featured/carousel) */
254
+ autoplay: {
255
+ type: Boolean,
256
+ default: true,
257
+ },
258
+ /** Autoplay interval (ms) */
259
+ interval: {
260
+ type: Number,
261
+ default: 5000,
262
+ },
263
+ });
264
+
265
+ const currentIndex = ref(0);
266
+ let autoplayInterval = null;
267
+
268
+ const currentTestimonial = computed(() => {
269
+ return props.testimonials[currentIndex.value] || props.testimonials[0];
270
+ });
271
+
272
+ const goToIndex = (index) => {
273
+ currentIndex.value = index;
274
+ resetAutoplay();
275
+ };
276
+
277
+ const next = () => {
278
+ currentIndex.value = (currentIndex.value + 1) % props.testimonials.length;
279
+ resetAutoplay();
280
+ };
281
+
282
+ const prev = () => {
283
+ currentIndex.value = currentIndex.value === 0
284
+ ? props.testimonials.length - 1
285
+ : currentIndex.value - 1;
286
+ resetAutoplay();
287
+ };
288
+
289
+ const startAutoplay = () => {
290
+ if (props.autoplay && props.testimonials.length > 1) {
291
+ autoplayInterval = setInterval(() => {
292
+ currentIndex.value = (currentIndex.value + 1) % props.testimonials.length;
293
+ }, props.interval);
294
+ }
295
+ };
296
+
297
+ const resetAutoplay = () => {
298
+ if (autoplayInterval) {
299
+ clearInterval(autoplayInterval);
300
+ startAutoplay();
301
+ }
302
+ };
303
+
304
+ onMounted(() => {
305
+ startAutoplay();
306
+ });
307
+
308
+ onUnmounted(() => {
309
+ if (autoplayInterval) {
310
+ clearInterval(autoplayInterval);
311
+ }
312
+ });
313
+ </script>
314
+
315
+ <style scoped>
316
+ /* Fade transition for testimonial changes */
317
+ .fade-enter-active,
318
+ .fade-leave-active {
319
+ transition: opacity 400ms cubic-bezier(0.16, 1, 0.3, 1);
320
+ }
321
+
322
+ .fade-enter-from,
323
+ .fade-leave-to {
324
+ opacity: 0;
325
+ }
326
+ </style>