@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.
- package/.claude/settings.local.json +22 -0
- package/.playground/app.config.ts +44 -45
- package/.playground/app.vue +263 -0
- package/.playground/components/AppFooter.vue +103 -0
- package/.playground/components/AppHeader.vue +41 -0
- package/.playground/components/CodeExample.vue +70 -0
- package/.playground/components/ComponentPlayground.vue +117 -0
- package/.playground/components/DocsHeader.vue +19 -0
- package/.playground/layouts/default.vue +13 -3
- package/.playground/layouts/docs-layout.vue +161 -0
- package/.playground/layouts/docs.vue +11 -0
- package/.playground/package-lock.json +6 -0
- package/.playground/pages/blog/author.vue +145 -0
- package/.playground/pages/blog/card.vue +149 -0
- package/.playground/pages/blog/cta.vue +131 -0
- package/.playground/pages/blog/detail.vue +379 -0
- package/.playground/pages/blog/list.vue +261 -0
- package/.playground/pages/blog/navigation.vue +228 -0
- package/.playground/pages/blog/sidebar.vue +279 -0
- package/.playground/pages/config.vue +289 -0
- package/.playground/pages/content/comparison.vue +253 -0
- package/.playground/pages/content/cta.vue +245 -0
- package/.playground/pages/content/features.vue +217 -0
- package/.playground/pages/content/newsletter-section.vue +260 -0
- package/.playground/pages/content/newsletter.vue +200 -0
- package/.playground/pages/content/pricing.vue +335 -0
- package/.playground/pages/content/testimonials.vue +188 -0
- package/.playground/pages/hero.vue +257 -0
- package/.playground/pages/index.vue +379 -2
- package/.playground/pages/layout/footer.vue +210 -0
- package/.playground/pages/layout/legal-footer.vue +121 -0
- package/.playground/pages/layout/navbar.vue +285 -0
- package/.playground/pages/layout/section.vue +266 -0
- package/.playground/pages/layout/x-legal.vue +199 -0
- package/.playground/pages/modals/feature-modal.vue +320 -0
- package/.playground/pages/modals/image-lightbox.vue +277 -0
- package/.playground/pages/modals/video-modal.vue +222 -0
- package/.playground/pages/notifications/announcement-bar.vue +164 -0
- package/.playground/pages/notifications/cookie-banner.vue +210 -0
- package/.playground/pages/notifications/cookie-toast.vue +143 -0
- package/.playground/pages/notifications/gdpr.vue +276 -0
- package/.playground/pages/notifications/social-proof-toast.vue +255 -0
- package/.playground/pages/ui/badge.vue +161 -0
- package/.playground/pages/ui/bg-pattern.vue +177 -0
- package/.playground/pages/ui/glass-card.vue +179 -0
- package/.playground/pages/ui/glow-divider.vue +179 -0
- package/.playground/pages/ui/promo-card.vue +259 -0
- package/.playground/pages/ui/section-stitch.vue +411 -0
- package/.playground/pages/utilities/back-to-top.vue +153 -0
- package/.playground/pages/utilities/popup-chat.vue +224 -0
- package/README.md +799 -164
- package/app/app.config.ts +88 -2
- package/app/app.vue +106 -11
- package/app/assets/css/x-marketing.css +668 -0
- package/app/components/X/Footer/XLegal/index.vue +63 -26
- package/app/components/X/Footer/index.vue +89 -89
- package/app/components/X/Header/App.vue +63 -0
- package/app/components/X/Mark/Blog/Author.vue +170 -0
- package/app/components/X/Mark/Blog/CTANewsletter.vue +197 -0
- package/app/components/X/Mark/Blog/Card.vue +117 -0
- package/app/components/X/Mark/Blog/Detail.vue +240 -0
- package/app/components/X/Mark/Blog/List.vue +156 -0
- package/app/components/X/Mark/Blog/Navigation.vue +100 -0
- package/app/components/X/Mark/Blog/Sidebar.vue +160 -0
- package/app/components/X/Mark/Button/BackToTop.vue +99 -0
- package/app/components/X/Mark/CTA/index.vue +294 -0
- package/app/components/X/Mark/Card/Glass.vue +64 -0
- package/app/components/X/Mark/Card/Promo.vue +164 -0
- package/app/components/X/Mark/Divider/Glow.vue +113 -0
- package/app/components/X/Mark/Features/index.vue +194 -0
- package/app/components/X/Mark/Hero/index.vue +217 -0
- package/app/components/X/Mark/Layout/AnnouncementBar.vue +139 -0
- package/app/components/X/Mark/Layout/Footer.vue +160 -0
- package/app/components/X/Mark/Layout/FooterLegal.vue +42 -0
- package/app/components/X/Mark/Layout/Navbar.vue +267 -0
- package/app/components/X/Mark/Modal/Chat.vue +151 -0
- package/app/components/X/Mark/Modal/Feature.vue +197 -0
- package/app/components/X/Mark/Modal/Image.vue +226 -0
- package/app/components/X/Mark/Modal/Video.vue +203 -0
- package/app/components/X/Mark/Modal/index.vue +133 -0
- package/app/components/X/Mark/Newsletter/Form.vue +174 -0
- package/app/components/X/Mark/Pattern/Bg.vue +86 -0
- package/app/components/X/Mark/Pricing/Comparison.vue +126 -0
- package/app/components/X/Mark/Pricing/Plans.vue +181 -0
- package/app/components/X/Mark/Privacy/CookieBanner.vue +112 -0
- package/app/components/X/Mark/Privacy/CookieToast.vue +43 -0
- package/app/components/X/Mark/Privacy/GDPR.vue +245 -0
- package/app/components/X/Mark/Section/Newsletter.vue +183 -0
- package/app/components/X/Mark/Section/Stitch.vue +156 -0
- package/app/components/X/Mark/Section/index.vue +151 -0
- package/app/components/X/Mark/SocialProof/Badge.vue +102 -0
- package/app/components/X/Mark/SocialProof/Testimonials.vue +326 -0
- package/app/components/X/Mark/SocialProof/Toast.vue +136 -0
- package/app/components/X/Mark/_archive/Pattern/Background.vue +83 -0
- package/app/components/X/Mark/_archive/Pattern/CheckBg.vue +35 -0
- package/app/components/X/Mark/_archive/Pattern/CircuitBg.vue +41 -0
- package/app/components/X/Mark/_archive/Pattern/DotsBg.vue +35 -0
- package/app/components/X/Mark/_archive/Pattern/GlowDivider.vue +86 -0
- package/app/components/X/Mark/_archive/Pattern/HexBg.vue +35 -0
- package/app/components/X/Mark/_archive/Pattern/PlaidBg.vue +47 -0
- package/app/components/X/Mark/_archive/Pattern/SectionStitch.vue +168 -0
- package/app/components/X/Mark/_archive/Pattern/WaveBg.vue +41 -0
- package/app/components/X/_archive/Newsletter/Form.vue +123 -0
- package/app/components/X/_archive/Newsletter/Section.vue +131 -0
- package/app/composables/useParallax.ts +124 -0
- package/app/composables/useScrollReveal.ts +92 -0
- package/app/pages/index.vue +210 -0
- package/app/plugins/marketing.client.ts +13 -0
- package/app/types/marketing.d.ts +268 -0
- package/componentList.json +511 -0
- package/nuxt.config.ts +6 -13
- package/package.json +21 -9
- package/.editorconfig +0 -12
- package/app/assets/css/main.css +0 -2
- 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>
|