@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,100 @@
1
+ <template>
2
+ <nav class="grid sm:grid-cols-2 gap-4 pt-8 border-t border-neutral-200 dark:border-neutral-800">
3
+ <!-- Previous -->
4
+ <UButton
5
+ v-if="previous"
6
+ :to="getPostUrl(previous)"
7
+ variant="ghost"
8
+ color="neutral"
9
+ class="justify-start! h-auto! py-4! px-4!"
10
+ :ui="{
11
+ base: 'group',
12
+ rounded: 'rounded-xl'
13
+ }"
14
+ >
15
+ <template #leading>
16
+ <UIcon
17
+ name="i-lucide-arrow-left"
18
+ class="w-5 h-5 text-neutral-400 group-hover:text-primary-500 transition-colors flex-shrink-0"
19
+ />
20
+ </template>
21
+ <div class="flex-1 min-w-0 text-left">
22
+ <span class="text-xs text-neutral-500 uppercase tracking-wide block font-normal">
23
+ {{ previousLabel }}
24
+ </span>
25
+ <p class="font-medium text-neutral-900 dark:text-white group-hover:text-primary-500 transition-colors line-clamp-1 mt-0.5">
26
+ {{ previous.title }}
27
+ </p>
28
+ </div>
29
+ </UButton>
30
+ <div v-else />
31
+
32
+ <!-- Next -->
33
+ <UButton
34
+ v-if="next"
35
+ :to="getPostUrl(next)"
36
+ variant="ghost"
37
+ color="neutral"
38
+ class="justify-end! h-auto! py-4! px-4!"
39
+ :ui="{
40
+ base: 'group',
41
+ rounded: 'rounded-xl'
42
+ }"
43
+ >
44
+ <div class="flex-1 min-w-0 text-right">
45
+ <span class="text-xs text-neutral-500 uppercase tracking-wide block font-normal">
46
+ {{ nextLabel }}
47
+ </span>
48
+ <p class="font-medium text-neutral-900 dark:text-white group-hover:text-primary-500 transition-colors line-clamp-1 mt-0.5">
49
+ {{ next.title }}
50
+ </p>
51
+ </div>
52
+ <template #trailing>
53
+ <UIcon
54
+ name="i-lucide-arrow-right"
55
+ class="w-5 h-5 text-neutral-400 group-hover:text-primary-500 transition-colors flex-shrink-0"
56
+ />
57
+ </template>
58
+ </UButton>
59
+ </nav>
60
+ </template>
61
+
62
+ <script setup>
63
+ const props = defineProps({
64
+ /** Previous post object */
65
+ previous: {
66
+ type: Object,
67
+ default: null,
68
+ // { title, slug } or { title, _path }
69
+ },
70
+ /** Next post object */
71
+ next: {
72
+ type: Object,
73
+ default: null,
74
+ // { title, slug } or { title, _path }
75
+ },
76
+ /** Base path for blog posts */
77
+ basePath: {
78
+ type: String,
79
+ default: '/blog',
80
+ },
81
+ /** Label for previous link */
82
+ previousLabel: {
83
+ type: String,
84
+ default: 'Previous',
85
+ },
86
+ /** Label for next link */
87
+ nextLabel: {
88
+ type: String,
89
+ default: 'Next',
90
+ },
91
+ });
92
+
93
+ const getPostUrl = (post) => {
94
+ // Support both slug and _path (from Nuxt Content)
95
+ if (post._path) {
96
+ return post._path;
97
+ }
98
+ return `${props.basePath}/${post.slug}`;
99
+ };
100
+ </script>
@@ -0,0 +1,160 @@
1
+ <template>
2
+ <aside class="space-y-8">
3
+ <!-- Search -->
4
+ <section v-if="hasSearch">
5
+ <h4 class="font-medium mb-3 text-neutral-900 dark:text-white">Search</h4>
6
+ <UInput
7
+ v-model="search"
8
+ placeholder="Search posts..."
9
+ icon="i-lucide-search"
10
+ @keyup.enter="$emit('search', search)"
11
+ />
12
+ </section>
13
+
14
+ <!-- Categories -->
15
+ <section v-if="categories?.length">
16
+ <h4 class="font-medium mb-3 text-neutral-900 dark:text-white">Categories</h4>
17
+ <nav class="space-y-1">
18
+ <NuxtLink
19
+ v-for="category in categories"
20
+ :key="category.slug"
21
+ :to="categoryPath(category.slug)"
22
+ class="flex justify-between items-center py-2 px-3 rounded-lg text-sm hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
23
+ :class="{
24
+ 'bg-primary-50 dark:bg-primary-950 text-primary-600 dark:text-primary-400': isActiveCategory(category.slug)
25
+ }"
26
+ >
27
+ <span>{{ category.name }}</span>
28
+ <span class="text-neutral-400 text-xs">{{ category.count }}</span>
29
+ </NuxtLink>
30
+ </nav>
31
+ </section>
32
+
33
+ <!-- Tags -->
34
+ <section v-if="tags?.length">
35
+ <h4 class="font-medium mb-3 text-neutral-900 dark:text-white">Tags</h4>
36
+ <div class="flex flex-wrap gap-2">
37
+ <NuxtLink
38
+ v-for="tag in tags"
39
+ :key="tag.slug"
40
+ :to="tagPath(tag.slug)"
41
+ class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-neutral-100 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors"
42
+ >
43
+ {{ tag.name }}
44
+ </NuxtLink>
45
+ </div>
46
+ </section>
47
+
48
+ <!-- Recent Posts -->
49
+ <section v-if="recentPosts?.length">
50
+ <h4 class="font-medium mb-3 text-neutral-900 dark:text-white">Recent Posts</h4>
51
+ <div class="space-y-4">
52
+ <NuxtLink
53
+ v-for="post in recentPosts"
54
+ :key="post.slug"
55
+ :to="`/blog/${post.slug}`"
56
+ class="flex gap-3 group"
57
+ >
58
+ <figure class="w-16 h-16 rounded-lg overflow-hidden flex-shrink-0">
59
+ <img
60
+ v-if="post.img?.src"
61
+ :src="post.img.src"
62
+ :alt="post.img.alt || post.title"
63
+ class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
64
+ />
65
+ <div
66
+ v-else
67
+ class="w-full h-full bg-neutral-200 dark:bg-neutral-800 flex items-center justify-center"
68
+ >
69
+ <UIcon name="i-lucide-file-text" class="w-6 h-6 text-neutral-400" />
70
+ </div>
71
+ </figure>
72
+ <div class="flex-1 min-w-0">
73
+ <h5 class="text-sm font-medium text-neutral-900 dark:text-white group-hover:text-primary-500 transition-colors line-clamp-2">
74
+ {{ post.title }}
75
+ </h5>
76
+ <time
77
+ v-if="post.date"
78
+ :datetime="post.date"
79
+ class="text-xs text-neutral-500 mt-1 block"
80
+ >
81
+ {{ formatDate(post.date) }}
82
+ </time>
83
+ </div>
84
+ </NuxtLink>
85
+ </div>
86
+ </section>
87
+
88
+ <!-- Newsletter CTA -->
89
+ <slot name="cta">
90
+ <XMarkBlogCTANewsletter v-if="hasCta" variant="compact" />
91
+ </slot>
92
+
93
+ <!-- Custom Content -->
94
+ <slot />
95
+ </aside>
96
+ </template>
97
+
98
+ <script setup>
99
+ const props = defineProps({
100
+ /** Has search input */
101
+ hasSearch: {
102
+ type: Boolean,
103
+ default: true,
104
+ },
105
+ /** Categories list */
106
+ categories: {
107
+ type: Array,
108
+ default: () => [],
109
+ // Each: { name, slug, count }
110
+ },
111
+ /** Tags list */
112
+ tags: {
113
+ type: Array,
114
+ default: () => [],
115
+ // Each: { name, slug }
116
+ },
117
+ /** Recent posts */
118
+ recentPosts: {
119
+ type: Array,
120
+ default: () => [],
121
+ // Each: { title, slug, img?: { src, alt? }, date? }
122
+ },
123
+ /** Base path for categories */
124
+ categoryBasePath: {
125
+ type: String,
126
+ default: '/blog/category',
127
+ },
128
+ /** Base path for tags */
129
+ tagBasePath: {
130
+ type: String,
131
+ default: '/blog/tag',
132
+ },
133
+ /** Has newsletter CTA */
134
+ hasCta: {
135
+ type: Boolean,
136
+ default: true,
137
+ },
138
+ });
139
+
140
+ defineEmits(['search']);
141
+
142
+ const route = useRoute();
143
+ const search = ref('');
144
+
145
+ const categoryPath = (slug) => `${props.categoryBasePath}/${slug}`;
146
+ const tagPath = (slug) => `${props.tagBasePath}/${slug}`;
147
+
148
+ const isActiveCategory = (slug) => {
149
+ return route.params.category === slug || route.query.category === slug;
150
+ };
151
+
152
+ const formatDate = (dateStr) => {
153
+ const date = new Date(dateStr);
154
+ return date.toLocaleDateString('en-US', {
155
+ month: 'short',
156
+ day: 'numeric',
157
+ year: 'numeric',
158
+ });
159
+ };
160
+ </script>
@@ -0,0 +1,99 @@
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
+ <button
9
+ v-if="isVisible"
10
+ class="fixed z-40 w-11 h-11 flex items-center justify-center rounded-full shadow-lg transition-all duration-300 hover:scale-110"
11
+ :class="[positionClasses, variantClasses]"
12
+ :aria-label="label"
13
+ @click="scrollToTop"
14
+ >
15
+ <UIcon :name="icon" class="w-5 h-5" />
16
+ </button>
17
+ </Transition>
18
+ </template>
19
+
20
+ <script setup>
21
+ const props = defineProps({
22
+ /** Scroll threshold before showing button */
23
+ threshold: {
24
+ type: Number,
25
+ default: 400,
26
+ },
27
+ /** Button position */
28
+ position: {
29
+ type: String,
30
+ default: 'bottom-right',
31
+ validator: (v) => ['bottom-right', 'bottom-left', 'bottom-center'].includes(v),
32
+ },
33
+ /** Style variant */
34
+ variant: {
35
+ type: String,
36
+ default: 'primary',
37
+ validator: (v) => ['primary', 'neutral', 'glass'].includes(v),
38
+ },
39
+ /** Icon name */
40
+ icon: {
41
+ type: String,
42
+ default: 'i-lucide-arrow-up',
43
+ },
44
+ /** Aria label */
45
+ label: {
46
+ type: String,
47
+ default: 'Back to top',
48
+ },
49
+ /** Smooth scroll behavior */
50
+ smooth: {
51
+ type: Boolean,
52
+ default: true,
53
+ },
54
+ });
55
+
56
+ const isVisible = ref(false);
57
+
58
+ const positionClasses = computed(() => {
59
+ const positions = {
60
+ 'bottom-right': 'bottom-6 right-6',
61
+ 'bottom-left': 'bottom-6 left-6',
62
+ 'bottom-center': 'bottom-6 left-1/2 -translate-x-1/2',
63
+ };
64
+ return positions[props.position];
65
+ });
66
+
67
+ const variantClasses = computed(() => {
68
+ const variants = {
69
+ primary: 'bg-primary-500 text-white hover:bg-primary-600 hover:shadow-lg hover:shadow-primary-500/30',
70
+ neutral: 'bg-neutral-900 dark:bg-white text-white dark:text-neutral-900 hover:bg-neutral-800 dark:hover:bg-neutral-100',
71
+ glass: 'xGlass text-neutral-900 dark:text-white hover:bg-white/80 dark:hover:bg-black/60',
72
+ };
73
+ return variants[props.variant];
74
+ });
75
+
76
+ const scrollToTop = () => {
77
+ window.scrollTo({
78
+ top: 0,
79
+ behavior: props.smooth ? 'smooth' : 'auto',
80
+ });
81
+ };
82
+
83
+ const handleScroll = () => {
84
+ isVisible.value = window.scrollY > props.threshold;
85
+ };
86
+
87
+ onMounted(() => {
88
+ if (process.client) {
89
+ window.addEventListener('scroll', handleScroll, { passive: true });
90
+ handleScroll();
91
+ }
92
+ });
93
+
94
+ onUnmounted(() => {
95
+ if (process.client) {
96
+ window.removeEventListener('scroll', handleScroll);
97
+ }
98
+ });
99
+ </script>
@@ -0,0 +1,294 @@
1
+ <template>
2
+ <section
3
+ :class="[
4
+ 'relative overflow-hidden rounded-2xl',
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
+ >
22
+ <img
23
+ :src="resolvedBg.image"
24
+ :alt="resolvedBg.imageAlt"
25
+ class="w-full h-full object-cover"
26
+ :class="resolvedBg.imagePosition"
27
+ />
28
+ <div
29
+ v-if="resolvedBg.imageOverlay"
30
+ class="absolute inset-0"
31
+ :class="bgImageOverlayClass"
32
+ />
33
+ </div>
34
+
35
+ <!-- Content -->
36
+ <div :class="['relative z-10', containerClasses]">
37
+ <!-- Icon -->
38
+ <div v-if="icon" :class="iconContainerClasses">
39
+ <UIcon :name="icon" :class="iconClasses" />
40
+ </div>
41
+
42
+ <!-- Content -->
43
+ <div :class="contentClasses">
44
+ <!-- Eyebrow -->
45
+ <p v-if="eyebrow" :class="eyebrowClasses">
46
+ {{ eyebrow }}
47
+ </p>
48
+
49
+ <!-- Title -->
50
+ <h3 :class="titleClasses">
51
+ {{ title }}
52
+ </h3>
53
+
54
+ <!-- Description -->
55
+ <p v-if="description" :class="descriptionClasses">
56
+ {{ description }}
57
+ </p>
58
+ </div>
59
+
60
+ <!-- Buttons -->
61
+ <div v-if="buttons?.length" :class="buttonsClasses">
62
+ <UButton
63
+ v-for="(button, index) in buttons"
64
+ :key="index"
65
+ :label="button.label"
66
+ :to="button.to"
67
+ :color="getButtonColor(button, index)"
68
+ :variant="button.variant || (index === 0 ? 'solid' : 'outline')"
69
+ :icon="button.icon"
70
+ :size="size === 'lg' ? 'lg' : 'md'"
71
+ :class="getButtonClass(button, index)"
72
+ />
73
+ </div>
74
+ </div>
75
+ </section>
76
+ </template>
77
+
78
+ <script setup>
79
+ const props = defineProps({
80
+ /** Eyebrow text above title */
81
+ eyebrow: {
82
+ type: String,
83
+ default: '',
84
+ },
85
+ /** CTA title */
86
+ title: {
87
+ type: String,
88
+ required: true,
89
+ },
90
+ /** CTA description */
91
+ description: {
92
+ type: String,
93
+ default: '',
94
+ },
95
+ /** Optional icon */
96
+ icon: {
97
+ type: String,
98
+ default: '',
99
+ },
100
+ /** Action buttons */
101
+ buttons: {
102
+ type: Array,
103
+ default: () => [],
104
+ // [{ label, to?, color?, variant?, icon? }]
105
+ },
106
+ /** Layout variant */
107
+ variant: {
108
+ type: String,
109
+ default: 'default',
110
+ validator: (v) => ['default', 'centered', 'inline', 'stacked'].includes(v),
111
+ },
112
+ /**
113
+ * Background configuration object or string shorthand
114
+ * @property {string} variant - 'subtle' | 'bold' | 'gradient' | 'none'
115
+ * @property {string} color - Custom Tailwind classes (overrides variant)
116
+ * @property {string} image - Background image URL
117
+ * @property {string} imageAlt - Background image alt text
118
+ * @property {string} imageOverlay - 'light' | 'heavy' | 'gradient'
119
+ * @property {string} imagePosition - Tailwind object-position class
120
+ * @property {string} pattern - 'dots' | 'grid' | 'diagonal' | 'topography' | 'circuit' | 'waves'
121
+ * @property {number} patternOpacity - Pattern opacity (0-1)
122
+ */
123
+ bg: {
124
+ type: [Object, String],
125
+ default: 'subtle',
126
+ },
127
+ /** Size */
128
+ size: {
129
+ type: String,
130
+ default: 'md',
131
+ validator: (v) => ['sm', 'md', 'lg'].includes(v),
132
+ },
133
+ });
134
+
135
+ // Default bg values
136
+ const bgDefaults = {
137
+ variant: 'subtle',
138
+ color: '',
139
+ image: '',
140
+ imageAlt: '',
141
+ imageOverlay: '',
142
+ imagePosition: 'object-center',
143
+ pattern: '',
144
+ patternOpacity: 0.05,
145
+ };
146
+
147
+ // Resolve bg prop (supports string shorthand for variant or full object)
148
+ const resolvedBg = computed(() => {
149
+ if (typeof props.bg === 'string') {
150
+ return { ...bgDefaults, variant: props.bg };
151
+ }
152
+ return { ...bgDefaults, ...props.bg };
153
+ });
154
+
155
+ // Check if background is "bold" (dark background requiring light text)
156
+ const isBoldBg = computed(() => {
157
+ return ['bold', 'gradient'].includes(resolvedBg.value.variant) || resolvedBg.value.image;
158
+ });
159
+
160
+ const paddingClasses = computed(() => {
161
+ const sizes = {
162
+ sm: 'p-6',
163
+ md: 'p-8 md:p-12',
164
+ lg: 'p-12 md:p-16',
165
+ };
166
+ return sizes[props.size];
167
+ });
168
+
169
+ const bgColorClasses = computed(() => {
170
+ // Custom color class takes priority
171
+ if (resolvedBg.value.color) return resolvedBg.value.color;
172
+
173
+ const variants = {
174
+ subtle: 'bg-neutral-100 dark:bg-neutral-900',
175
+ bold: 'bg-primary-600 dark:bg-primary-700',
176
+ gradient: 'bg-gradient-to-br from-primary-600 to-primary-800',
177
+ none: '',
178
+ };
179
+ return variants[resolvedBg.value.variant] || variants.subtle;
180
+ });
181
+
182
+ const bgImageOverlayClass = computed(() => {
183
+ const overlays = {
184
+ light: 'bg-black/30',
185
+ heavy: 'bg-black/60',
186
+ gradient: 'bg-gradient-to-t from-black/80 via-black/40 to-transparent',
187
+ };
188
+ return overlays[resolvedBg.value.imageOverlay] || '';
189
+ });
190
+
191
+ const containerClasses = computed(() => {
192
+ const layouts = {
193
+ default: 'flex flex-col md:flex-row md:items-center md:justify-between gap-6',
194
+ centered: 'text-center max-w-2xl mx-auto',
195
+ inline: 'flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4',
196
+ stacked: 'flex flex-col items-center text-center gap-6',
197
+ };
198
+ return layouts[props.variant];
199
+ });
200
+
201
+ const iconContainerClasses = computed(() => {
202
+ return [
203
+ 'flex items-center justify-center rounded-xl',
204
+ props.size === 'lg' ? 'w-16 h-16' : 'w-12 h-12',
205
+ isBoldBg.value ? 'bg-white/20' : 'bg-primary-100 dark:bg-primary-900/50',
206
+ ];
207
+ });
208
+
209
+ const iconClasses = computed(() => {
210
+ return [
211
+ props.size === 'lg' ? 'w-8 h-8' : 'w-6 h-6',
212
+ isBoldBg.value ? 'text-white' : 'text-primary-500',
213
+ ];
214
+ });
215
+
216
+ const contentClasses = computed(() => {
217
+ if (props.variant === 'default') {
218
+ return 'flex-1';
219
+ }
220
+ return '';
221
+ });
222
+
223
+ const eyebrowClasses = computed(() => {
224
+ return [
225
+ 'xText-eyebrow mb-2',
226
+ isBoldBg.value ? 'text-white/70' : 'text-primary-600 dark:text-primary-400',
227
+ ];
228
+ });
229
+
230
+ const titleClasses = computed(() => {
231
+ const sizeClasses = {
232
+ sm: 'text-lg font-semibold',
233
+ md: 'text-xl font-semibold',
234
+ lg: 'xText-headline',
235
+ };
236
+ return [
237
+ sizeClasses[props.size],
238
+ isBoldBg.value ? 'text-white' : 'text-neutral-900 dark:text-white',
239
+ ];
240
+ });
241
+
242
+ const descriptionClasses = computed(() => {
243
+ return [
244
+ 'mt-2',
245
+ props.size === 'sm' ? 'text-sm' : 'text-base',
246
+ isBoldBg.value ? 'text-white/80' : 'text-neutral-600 dark:text-neutral-400',
247
+ ];
248
+ });
249
+
250
+ const buttonsClasses = computed(() => {
251
+ const layouts = {
252
+ default: 'flex flex-wrap gap-3',
253
+ centered: 'flex flex-wrap justify-center gap-3 mt-6',
254
+ inline: 'flex flex-wrap gap-3',
255
+ stacked: 'flex flex-wrap justify-center gap-3',
256
+ };
257
+ return layouts[props.variant];
258
+ });
259
+
260
+ const getButtonColor = (button, index) => {
261
+ // Allow explicit color override
262
+ if (button.color && button.color !== 'white') return button.color;
263
+ if (index === 0) {
264
+ // Use neutral instead of white (not a valid Nuxt UI v4 color)
265
+ // Custom classes handle the actual styling
266
+ return isBoldBg.value ? 'neutral' : 'primary';
267
+ }
268
+ return 'neutral';
269
+ };
270
+
271
+ const getButtonClass = (button, index) => {
272
+ const isOutline = button.variant === 'outline' || (index > 0 && !button.variant);
273
+ const isGhost = button.variant === 'ghost';
274
+ const isSolid = button.variant === 'solid' || (index === 0 && !button.variant);
275
+
276
+ // Solid buttons on bold/gradient backgrounds - white bg with dark text
277
+ if (isBoldBg.value && isSolid) {
278
+ return 'bg-white text-neutral-900 hover:bg-white/90';
279
+ }
280
+ // Outline buttons on bold/gradient backgrounds - transparent with white border
281
+ if (isBoldBg.value && isOutline) {
282
+ return 'bg-white/10 backdrop-blur-sm border-white/40 hover:bg-white/20 hover:border-white/60 text-white';
283
+ }
284
+ // Ghost buttons on bold/gradient backgrounds - transparent with white text
285
+ if (isBoldBg.value && isGhost) {
286
+ return 'text-white hover:bg-white/10';
287
+ }
288
+ // Outline buttons on subtle backgrounds
289
+ if (!isBoldBg.value && isOutline) {
290
+ return 'border-neutral-400 dark:border-neutral-600 hover:border-neutral-500 dark:hover:border-neutral-500';
291
+ }
292
+ return '';
293
+ };
294
+ </script>