@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,64 @@
1
+ <template>
2
+ <div
3
+ :class="[
4
+ 'xGlass rounded-2xl transition-all duration-300 hover:shadow-lg',
5
+ sizeClasses,
6
+ { 'xGlow-border': glow }
7
+ ]"
8
+ >
9
+ <!-- Header -->
10
+ <header v-if="title || $slots.header" class="mb-4">
11
+ <slot name="header">
12
+ <h3 class="xText-title text-neutral-900 dark:text-white">{{ title }}</h3>
13
+ <p v-if="subtitle" class="mt-1 text-neutral-600 dark:text-neutral-400">
14
+ {{ subtitle }}
15
+ </p>
16
+ </slot>
17
+ </header>
18
+
19
+ <!-- Content -->
20
+ <slot />
21
+
22
+ <!-- Footer -->
23
+ <footer v-if="$slots.footer" class="mt-6 pt-4 border-t border-white/10 dark:border-neutral-800">
24
+ <slot name="footer" />
25
+ </footer>
26
+ </div>
27
+ </template>
28
+
29
+ <script setup>
30
+ const props = defineProps({
31
+ /** Card title */
32
+ title: {
33
+ type: String,
34
+ default: '',
35
+ },
36
+ /** Card subtitle */
37
+ subtitle: {
38
+ type: String,
39
+ default: '',
40
+ },
41
+ /** Card size (padding) */
42
+ size: {
43
+ type: String,
44
+ default: 'md',
45
+ validator: (v) => ['xs', 'sm', 'md', 'lg', 'xl'].includes(v),
46
+ },
47
+ /** Add glow border effect */
48
+ glow: {
49
+ type: Boolean,
50
+ default: false,
51
+ },
52
+ });
53
+
54
+ const sizeClasses = computed(() => {
55
+ const sizes = {
56
+ xs: 'p-3',
57
+ sm: 'p-4',
58
+ md: 'p-6',
59
+ lg: 'p-8',
60
+ xl: 'p-10',
61
+ };
62
+ return sizes[props.size];
63
+ });
64
+ </script>
@@ -0,0 +1,164 @@
1
+ <template>
2
+ <component
3
+ :is="to ? 'NuxtLink' : 'article'"
4
+ :to="to"
5
+ :class="[
6
+ 'group relative aspect-[9/16] rounded-2xl overflow-hidden cursor-pointer block',
7
+ { 'xHover-lift': hoverable }
8
+ ]"
9
+ @click="!to && $emit('click')"
10
+ >
11
+ <!-- Background Image -->
12
+ <img
13
+ :src="img.src"
14
+ :alt="img.alt || ''"
15
+ class="absolute inset-0 w-full h-full object-cover transition-transform duration-700 ease-out group-hover:scale-[1.08]"
16
+ />
17
+
18
+ <!-- Overlay -->
19
+ <div
20
+ class="absolute inset-0"
21
+ :class="overlayClasses"
22
+ />
23
+
24
+ <!-- Content -->
25
+ <div
26
+ :class="[
27
+ 'absolute inset-0 p-6 flex flex-col',
28
+ alignmentClasses
29
+ ]"
30
+ >
31
+ <!-- Badge -->
32
+ <span
33
+ v-if="badge"
34
+ class="inline-flex self-start px-3 py-1 rounded-full text-xs font-medium bg-white/20 text-white backdrop-blur-sm"
35
+ >
36
+ {{ badge }}
37
+ </span>
38
+
39
+ <!-- Spacer -->
40
+ <div class="flex-1" />
41
+
42
+ <!-- Text Content -->
43
+ <div class="space-y-3">
44
+ <h3 :class="['font-semibold text-white', titleSizeClass]">
45
+ {{ title }}
46
+ </h3>
47
+
48
+ <div v-if="$slots.default" class="text-white/80 text-sm">
49
+ <slot />
50
+ </div>
51
+
52
+ <div
53
+ v-if="buttonLabel && to"
54
+ class="mt-4 inline-flex items-center justify-center px-4 py-2 rounded-md text-sm font-medium bg-white text-neutral-900 transition-all duration-300 group-hover:scale-110 group-hover:shadow-lg group-hover:shadow-white/50"
55
+ >
56
+ {{ buttonLabel }}
57
+ </div>
58
+
59
+ <button
60
+ v-if="buttonLabel && !to"
61
+ class="mt-4 inline-flex items-center justify-center px-4 py-2 rounded-md text-sm font-medium bg-white text-neutral-900 transition-all duration-300 group-hover:scale-110 group-hover:shadow-lg group-hover:shadow-white/50"
62
+ >
63
+ {{ buttonLabel }}
64
+ </button>
65
+ </div>
66
+ </div>
67
+ </component>
68
+ </template>
69
+
70
+ <script setup>
71
+ defineEmits(['click']);
72
+
73
+ const props = defineProps({
74
+ /** Navigation URL (uses NuxtLink) */
75
+ to: {
76
+ type: String,
77
+ default: '',
78
+ },
79
+ /** Image object { src: string, alt?: string } */
80
+ img: {
81
+ type: Object,
82
+ required: true,
83
+ validator: (v) => v && typeof v.src === 'string',
84
+ },
85
+ /** Card title */
86
+ title: {
87
+ type: String,
88
+ required: true,
89
+ },
90
+ /** Optional badge text */
91
+ badge: {
92
+ type: String,
93
+ default: '',
94
+ },
95
+ /** Button object { label: string, color?: string, variant?: string } */
96
+ button: {
97
+ type: Object,
98
+ default: null,
99
+ validator: (v) => !v || (v.label !== undefined),
100
+ },
101
+ /** Content alignment */
102
+ align: {
103
+ type: String,
104
+ default: 'bottomLeft',
105
+ validator: (v) => ['bottomLeft', 'bottomRight', 'bottomCenter', 'topLeft'].includes(v),
106
+ },
107
+ /** Overlay style */
108
+ overlay: {
109
+ type: String,
110
+ default: 'gradient',
111
+ validator: (v) => ['light', 'heavy', 'gradient'].includes(v),
112
+ },
113
+ /** Enable lift on hover */
114
+ hoverable: {
115
+ type: Boolean,
116
+ default: true,
117
+ },
118
+ /** Title text size (Tailwind sizes) */
119
+ titleSize: {
120
+ type: String,
121
+ default: 'xl',
122
+ validator: (v) => ['xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl', '8xl'].includes(v),
123
+ },
124
+ });
125
+
126
+ const buttonLabel = computed(() => props.button?.label || '');
127
+
128
+ const overlayClasses = computed(() => {
129
+ const overlays = {
130
+ light: 'bg-black/30',
131
+ heavy: 'bg-black/60',
132
+ gradient: 'bg-gradient-to-t from-black/80 via-black/30 to-transparent',
133
+ };
134
+ return overlays[props.overlay];
135
+ });
136
+
137
+ const alignmentClasses = computed(() => {
138
+ const alignments = {
139
+ bottomLeft: 'justify-end items-start text-left',
140
+ bottomRight: 'justify-end items-end text-right',
141
+ bottomCenter: 'justify-end items-center text-center',
142
+ topLeft: 'justify-start items-start text-left',
143
+ };
144
+ return alignments[props.align];
145
+ });
146
+
147
+ const titleSizeClass = computed(() => {
148
+ const sizes = {
149
+ xs: 'text-xs',
150
+ sm: 'text-sm',
151
+ base: 'text-base',
152
+ lg: 'text-lg',
153
+ xl: 'text-xl',
154
+ '2xl': 'text-2xl',
155
+ '3xl': 'text-3xl',
156
+ '4xl': 'text-4xl',
157
+ '5xl': 'text-5xl',
158
+ '6xl': 'text-6xl',
159
+ '7xl': 'text-7xl',
160
+ '8xl': 'text-8xl',
161
+ };
162
+ return sizes[props.titleSize] || 'text-xl';
163
+ });
164
+ </script>
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <div :class="spacing">
3
+ <div class="relative" :class="heightClass">
4
+ <!-- Line -->
5
+ <div
6
+ class="absolute inset-0 bg-gradient-to-r from-transparent to-transparent"
7
+ :class="lineClass"
8
+ />
9
+ <!-- Small glow -->
10
+ <div
11
+ v-if="intensity !== 'none'"
12
+ class="absolute inset-0 bg-gradient-to-r from-transparent to-transparent"
13
+ :class="[smallGlowClass, blurSmClass]"
14
+ />
15
+ <!-- Large glow -->
16
+ <div
17
+ v-if="intensity === 'medium' || intensity === 'strong'"
18
+ class="absolute bg-gradient-to-r from-transparent to-transparent"
19
+ :class="[largeGlowClass, blurLgClass, largeGlowInset]"
20
+ />
21
+ <!-- Extra large glow for strong -->
22
+ <div
23
+ v-if="intensity === 'strong'"
24
+ class="absolute bg-gradient-to-r from-transparent to-transparent blur-2xl"
25
+ :class="[extraGlowClass, extraGlowInset]"
26
+ />
27
+ </div>
28
+ </div>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ const props = withDefaults(
33
+ defineProps<{
34
+ /** Color theme - any Tailwind color or 'white' */
35
+ color?: string
36
+ /** Vertical spacing (padding) */
37
+ spacing?: string
38
+ /** Glow intensity */
39
+ intensity?: 'none' | 'subtle' | 'medium' | 'strong'
40
+ /** Line height */
41
+ height?: 'thin' | 'default' | 'thick'
42
+ }>(),
43
+ {
44
+ color: 'primary',
45
+ spacing: 'py-16',
46
+ intensity: 'medium',
47
+ height: 'default',
48
+ }
49
+ )
50
+
51
+ const heightClass = computed(() => {
52
+ const heights = {
53
+ thin: 'h-px',
54
+ default: 'h-0.5',
55
+ thick: 'h-1',
56
+ }
57
+ return heights[props.height]
58
+ })
59
+
60
+ // Line classes - handle white/neutral specially
61
+ const lineClass = computed(() => {
62
+ if (props.color === 'white') return 'via-white/50'
63
+ if (props.color === 'neutral') return 'via-neutral-400/50'
64
+ return `via-${props.color}-500/50`
65
+ })
66
+
67
+ // Small glow blur based on intensity
68
+ const blurSmClass = computed(() => {
69
+ const blurs = {
70
+ none: '',
71
+ subtle: 'blur-[2px]',
72
+ medium: 'blur-sm',
73
+ strong: 'blur-sm',
74
+ }
75
+ return blurs[props.intensity]
76
+ })
77
+
78
+ const smallGlowClass = computed(() => {
79
+ if (props.color === 'white') return 'via-white/60'
80
+ if (props.color === 'neutral') return 'via-neutral-300/60'
81
+ return `via-${props.color}-400/60`
82
+ })
83
+
84
+ // Large glow blur based on intensity
85
+ const blurLgClass = computed(() => {
86
+ const blurs = {
87
+ none: '',
88
+ subtle: 'blur-md',
89
+ medium: 'blur-xl',
90
+ strong: 'blur-xl',
91
+ }
92
+ return blurs[props.intensity]
93
+ })
94
+
95
+ const largeGlowInset = computed(() => {
96
+ return props.intensity === 'strong' ? '-inset-2' : '-inset-1'
97
+ })
98
+
99
+ const largeGlowClass = computed(() => {
100
+ if (props.color === 'white') return 'via-white/30'
101
+ if (props.color === 'neutral') return 'via-neutral-400/30'
102
+ return `via-${props.color}-500/30`
103
+ })
104
+
105
+ // Extra glow for strong intensity
106
+ const extraGlowInset = '-inset-4'
107
+
108
+ const extraGlowClass = computed(() => {
109
+ if (props.color === 'white') return 'via-white/20'
110
+ if (props.color === 'neutral') return 'via-neutral-400/20'
111
+ return `via-${props.color}-500/20`
112
+ })
113
+ </script>
@@ -0,0 +1,194 @@
1
+ <template>
2
+ <div :class="containerClasses">
3
+ <!-- Grid Layout -->
4
+ <template v-if="layout === 'grid'">
5
+ <div :class="gridClasses">
6
+ <article
7
+ v-for="(feature, index) in features"
8
+ :key="feature.title"
9
+ class="xFadeUp group"
10
+ :style="{ transitionDelay: `${index * 100}ms` }"
11
+ >
12
+ <!-- Icon -->
13
+ <div
14
+ v-if="feature.icon"
15
+ class="w-12 h-12 rounded-xl bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center mb-4 transition-transform duration-300 group-hover:scale-110"
16
+ >
17
+ <UIcon
18
+ :name="feature.icon"
19
+ class="w-6 h-6 text-primary-500"
20
+ />
21
+ </div>
22
+
23
+ <!-- Image -->
24
+ <figure
25
+ v-if="feature.img?.src"
26
+ class="aspect-video rounded-xl overflow-hidden mb-4 xHover-zoom"
27
+ >
28
+ <img
29
+ :src="feature.img.src"
30
+ :alt="feature.img.alt || feature.title"
31
+ class="w-full h-full object-cover"
32
+ />
33
+ </figure>
34
+
35
+ <!-- Title -->
36
+ <h3 class="xText-title text-neutral-900 dark:text-white">
37
+ {{ feature.title }}
38
+ </h3>
39
+
40
+ <!-- Description -->
41
+ <p class="mt-2 text-neutral-600 dark:text-neutral-400">
42
+ {{ feature.description }}
43
+ </p>
44
+
45
+ <!-- Link -->
46
+ <NuxtLink
47
+ v-if="feature.link"
48
+ :to="typeof feature.link === 'string' ? feature.link : feature.link.to"
49
+ :target="typeof feature.link === 'object' ? feature.link.target : undefined"
50
+ class="inline-flex items-center gap-1 mt-4 text-primary-500 hover:text-primary-600 font-medium text-sm transition-colors group/link"
51
+ >
52
+ {{ typeof feature.link === 'object' && feature.link.label ? feature.link.label : 'Learn more' }}
53
+ <UIcon name="i-lucide-arrow-right" class="w-4 h-4 transition-transform duration-300 group-hover/link:translate-x-1" />
54
+ </NuxtLink>
55
+ </article>
56
+ </div>
57
+ </template>
58
+
59
+ <!-- List Layout -->
60
+ <template v-else-if="layout === 'list'">
61
+ <div class="space-y-12">
62
+ <article
63
+ v-for="(feature, index) in features"
64
+ :key="feature.title"
65
+ class="flex flex-col md:flex-row gap-6 xFadeUp group"
66
+ :style="{ transitionDelay: `${index * 100}ms` }"
67
+ >
68
+ <!-- Icon -->
69
+ <div
70
+ v-if="feature.icon"
71
+ class="flex-shrink-0 w-12 h-12 rounded-xl bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center transition-transform duration-300 group-hover:scale-110"
72
+ >
73
+ <UIcon
74
+ :name="feature.icon"
75
+ class="w-6 h-6 text-primary-500"
76
+ />
77
+ </div>
78
+
79
+ <!-- Content -->
80
+ <div class="flex-1">
81
+ <h3 class="xText-title text-neutral-900 dark:text-white">
82
+ {{ feature.title }}
83
+ </h3>
84
+ <p class="mt-2 text-neutral-600 dark:text-neutral-400">
85
+ {{ feature.description }}
86
+ </p>
87
+ <NuxtLink
88
+ v-if="feature.link"
89
+ :to="typeof feature.link === 'string' ? feature.link : feature.link.to"
90
+ :target="typeof feature.link === 'object' ? feature.link.target : undefined"
91
+ class="inline-flex items-center gap-1 mt-4 text-primary-500 hover:text-primary-600 font-medium text-sm transition-colors group/link"
92
+ >
93
+ {{ typeof feature.link === 'object' && feature.link.label ? feature.link.label : 'Learn more' }}
94
+ <UIcon name="i-lucide-arrow-right" class="w-4 h-4 transition-transform duration-300 group-hover/link:translate-x-1" />
95
+ </NuxtLink>
96
+ </div>
97
+ </article>
98
+ </div>
99
+ </template>
100
+
101
+ <!-- Alternating Layout -->
102
+ <template v-else-if="layout === 'alternating'">
103
+ <div class="space-y-24 lg:space-y-32">
104
+ <article
105
+ v-for="(feature, index) in features"
106
+ :key="feature.title"
107
+ class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center xFadeUp group"
108
+ :style="{ transitionDelay: `${index * 100}ms` }"
109
+ >
110
+ <!-- Image -->
111
+ <figure
112
+ v-if="feature.img?.src"
113
+ class="aspect-[4/3] rounded-2xl overflow-hidden xHover-zoom shadow-lg"
114
+ :class="{ 'lg:order-2': index % 2 !== 0 }"
115
+ >
116
+ <img
117
+ :src="feature.img.src"
118
+ :alt="feature.img.alt || feature.title"
119
+ class="w-full h-full object-cover"
120
+ />
121
+ </figure>
122
+
123
+ <!-- Content -->
124
+ <div :class="{ 'lg:order-1': index % 2 !== 0 }">
125
+ <!-- Icon -->
126
+ <div
127
+ v-if="feature.icon"
128
+ class="w-14 h-14 rounded-xl bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center mb-6 transition-transform duration-300 group-hover:scale-110"
129
+ >
130
+ <UIcon
131
+ :name="feature.icon"
132
+ class="w-7 h-7 text-primary-500"
133
+ />
134
+ </div>
135
+
136
+ <h3 class="xText-headline text-neutral-900 dark:text-white xText-balance">
137
+ {{ feature.title }}
138
+ </h3>
139
+ <p class="mt-4 text-lg text-neutral-600 dark:text-neutral-400 leading-relaxed">
140
+ {{ feature.description }}
141
+ </p>
142
+ <NuxtLink
143
+ v-if="feature.link"
144
+ :to="typeof feature.link === 'string' ? feature.link : feature.link.to"
145
+ :target="typeof feature.link === 'object' ? feature.link.target : undefined"
146
+ class="inline-flex items-center gap-2 mt-6 text-primary-500 hover:text-primary-600 font-medium transition-colors group/link"
147
+ >
148
+ {{ typeof feature.link === 'object' && feature.link.label ? feature.link.label : 'Learn more' }}
149
+ <UIcon name="i-lucide-arrow-right" class="w-5 h-5 transition-transform duration-300 group-hover/link:translate-x-1" />
150
+ </NuxtLink>
151
+ </div>
152
+ </article>
153
+ </div>
154
+ </template>
155
+ </div>
156
+ </template>
157
+
158
+ <script setup>
159
+ // Initialize scroll reveal animations automatically
160
+ useScrollReveal()
161
+
162
+ const props = defineProps({
163
+ /** Array of features */
164
+ features: {
165
+ type: Array,
166
+ required: true,
167
+ },
168
+ /** Display layout */
169
+ layout: {
170
+ type: String,
171
+ default: 'grid',
172
+ validator: (v) => ['grid', 'list', 'alternating'].includes(v),
173
+ },
174
+ /** Grid columns (for grid layout) */
175
+ columns: {
176
+ type: Number,
177
+ default: 3,
178
+ validator: (v) => [2, 3, 4].includes(v),
179
+ },
180
+ });
181
+
182
+ const containerClasses = computed(() => {
183
+ return props.layout === 'alternating' ? 'max-w-6xl mx-auto' : '';
184
+ });
185
+
186
+ const gridClasses = computed(() => {
187
+ const cols = {
188
+ 2: 'grid md:grid-cols-2 gap-8',
189
+ 3: 'grid md:grid-cols-2 lg:grid-cols-3 gap-8',
190
+ 4: 'grid md:grid-cols-2 lg:grid-cols-4 gap-8',
191
+ };
192
+ return cols[props.columns];
193
+ });
194
+ </script>