@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,267 @@
1
+ <template>
2
+ <header
3
+ class="fixed top-0 inset-x-0 z-50 transition-all duration-300"
4
+ :class="[
5
+ isScrolled
6
+ ? 'bg-white/90 dark:bg-neutral-950/90 backdrop-blur-lg shadow-sm'
7
+ : transparent
8
+ ? 'bg-gradient-to-b from-black/90 to-transparent'
9
+ : 'bg-white dark:bg-neutral-950',
10
+ isScrolled ? 'py-3' : 'py-5'
11
+ ]"
12
+ >
13
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
14
+ <nav class="flex items-center justify-between">
15
+ <!-- Logo -->
16
+ <slot name="logo">
17
+ <NuxtLink to="/" class="flex-shrink-0">
18
+ <img
19
+ :src="isScrolled || !transparent ? resolvedLogoDark : resolvedLogoLight"
20
+ :alt="resolvedLogoAlt"
21
+ class="h-8 w-auto"
22
+ />
23
+ </NuxtLink>
24
+ </slot>
25
+
26
+ <!-- Desktop Navigation -->
27
+ <div class="hidden lg:flex items-center gap-8">
28
+ <slot name="links">
29
+ <NuxtLink
30
+ v-for="link in resolvedLinks"
31
+ :key="link.to"
32
+ :to="link.to"
33
+ class="relative text-sm font-medium transition-colors group"
34
+ :class="linkColor"
35
+ >
36
+ {{ link.label }}
37
+
38
+ </NuxtLink>
39
+ </slot>
40
+ </div>
41
+
42
+ <!-- Actions -->
43
+ <div class="hidden lg:flex items-center gap-4">
44
+ <slot name="actions">
45
+ <template v-if="resolvedButtons.length > 0">
46
+ <UButton
47
+ v-for="(button, index) in resolvedButtons"
48
+ :key="index"
49
+ :to="button.to"
50
+ :icon="button.icon"
51
+ :target="button.target || '_self'"
52
+ :color="getButtonColor(button, index)"
53
+ :variant="getButtonVariant(button, index)"
54
+ :class="getButtonClass(button, index)"
55
+ >
56
+ {{ button.label }}
57
+ </UButton>
58
+ </template>
59
+ <template v-else>
60
+ <UButton
61
+ variant="outline"
62
+ color="neutral"
63
+ :class="!isScrolled && transparent ? '!bg-transparent !text-white !border-white/60 hover:!bg-white/10' : ''"
64
+ >
65
+ Sign In
66
+ </UButton>
67
+ <UButton color="primary">
68
+ Get Started
69
+ </UButton>
70
+ </template>
71
+ </slot>
72
+ </div>
73
+
74
+ <!-- Mobile Menu Button -->
75
+ <button
76
+ class="lg:hidden p-2"
77
+ @click="mobileOpen = true"
78
+ >
79
+ <UIcon name="i-lucide-menu" class="w-6 h-6" :class="linkColor" />
80
+ </button>
81
+ </nav>
82
+ </div>
83
+
84
+ <!-- Mobile Overlay -->
85
+ <Teleport to="body">
86
+ <Transition
87
+ enter-active-class="transition-opacity duration-300"
88
+ enter-from-class="opacity-0"
89
+ leave-active-class="transition-opacity duration-300"
90
+ leave-to-class="opacity-0"
91
+ >
92
+ <div
93
+ v-if="mobileOpen"
94
+ class="fixed inset-0 z-50 bg-white dark:bg-neutral-950 lg:hidden"
95
+ >
96
+ <div class="flex flex-col h-full p-6">
97
+ <div class="flex justify-between items-center mb-8">
98
+ <img :src="resolvedLogoDark" :alt="resolvedLogoAlt" class="h-8" />
99
+ <button @click="mobileOpen = false">
100
+ <UIcon name="i-lucide-x" class="w-6 h-6" />
101
+ </button>
102
+ </div>
103
+
104
+ <nav class="flex-1 flex flex-col gap-2">
105
+ <NuxtLink
106
+ v-for="(link, index) in resolvedLinks"
107
+ :key="link.to"
108
+ :to="link.to"
109
+ class="text-2xl font-medium py-3 border-b border-neutral-100 dark:border-neutral-800 transition-colors hover:text-primary-500"
110
+ :style="{ transitionDelay: `${index * 50}ms` }"
111
+ @click="mobileOpen = false"
112
+ >
113
+ {{ link.label }}
114
+ </NuxtLink>
115
+ </nav>
116
+
117
+ <div class="flex flex-col gap-3">
118
+ <slot name="mobile-actions">
119
+ <template v-if="resolvedButtons.length > 0">
120
+ <UButton
121
+ v-for="(button, index) in resolvedButtons"
122
+ :key="index"
123
+ block
124
+ :to="button.to"
125
+ :icon="button.icon"
126
+ :target="button.target || '_self'"
127
+ :color="button.color || 'primary'"
128
+ :variant="button.variant || (index === 0 ? 'outline' : 'solid')"
129
+ @click="mobileOpen = false"
130
+ >
131
+ {{ button.label }}
132
+ </UButton>
133
+ </template>
134
+ <template v-else>
135
+ <UButton block variant="outline">Sign In</UButton>
136
+ <UButton block color="primary">Get Started</UButton>
137
+ </template>
138
+ </slot>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </Transition>
143
+ </Teleport>
144
+ </header>
145
+ </template>
146
+
147
+ <script setup>
148
+ const appConfig = useAppConfig();
149
+ const header = appConfig?.xMarketing?.header;
150
+
151
+ const props = defineProps({
152
+ /** Start transparent over hero */
153
+ transparent: {
154
+ type: Boolean,
155
+ default: true,
156
+ },
157
+ /** Navigation links - falls back to appConfig.xMarketing.header.nav.links */
158
+ links: {
159
+ type: Array,
160
+ default: null,
161
+ },
162
+ /** Logo object { src: string, srcDark?: string, alt?: string } - falls back to appConfig */
163
+ logo: {
164
+ type: Object,
165
+ default: null,
166
+ validator: (v) => !v || typeof v.src === 'string',
167
+ },
168
+ /** Pixels before transition */
169
+ scrollThreshold: {
170
+ type: Number,
171
+ default: 100,
172
+ },
173
+ /** Action buttons - falls back to appConfig.xMarketing.header.nav.buttons */
174
+ buttons: {
175
+ type: Array,
176
+ default: null,
177
+ },
178
+ });
179
+
180
+ // Computed properties with appConfig fallbacks
181
+ const resolvedLinks = computed(() => {
182
+ if (props.links && props.links.length > 0) return props.links;
183
+ return header?.nav?.links || [];
184
+ });
185
+
186
+ const resolvedLogoLight = computed(() => {
187
+ return props.logo?.src || header?.logo?.src || '';
188
+ });
189
+
190
+ const resolvedLogoDark = computed(() => {
191
+ return props.logo?.srcDark || props.logo?.src || header?.logo?.srcDark || header?.logo?.src || '';
192
+ });
193
+
194
+ const resolvedLogoAlt = computed(() => {
195
+ return props.logo?.alt || header?.logo?.alt || 'Logo';
196
+ });
197
+
198
+ const resolvedButtons = computed(() => {
199
+ if (props.buttons && props.buttons.length > 0) return props.buttons;
200
+ return header?.nav?.buttons || [];
201
+ });
202
+
203
+ const mobileOpen = ref(false);
204
+ const isScrolled = ref(false);
205
+
206
+ const linkColor = computed(() => {
207
+ if (isScrolled.value || !props.transparent) {
208
+ return 'text-neutral-900 dark:text-white';
209
+ }
210
+ return 'text-white drop-shadow-[0_1px_2px_rgba(0,0,0,0.5)]';
211
+ });
212
+
213
+ // Button color/variant helpers for transparent vs scrolled states
214
+ const getButtonColor = (button, index) => {
215
+ const isLastButton = index === resolvedButtons.value.length - 1;
216
+
217
+ // Last button is always primary
218
+ if (isLastButton || button.color === 'primary') return 'primary';
219
+
220
+ // Non-last buttons use neutral (we'll override colors with classes in transparent state)
221
+ if (button.color && button.color !== 'neutral') return button.color;
222
+ return 'neutral';
223
+ };
224
+
225
+ // Custom classes for transparent state (Nuxt UI doesn't have 'white' color)
226
+ const getButtonClass = (button, index) => {
227
+ const isLastButton = index === resolvedButtons.value.length - 1;
228
+
229
+ // In transparent state, non-last buttons need white text/border on transparent bg
230
+ if (!isScrolled.value && props.transparent && !isLastButton && button.color !== 'primary') {
231
+ return '!bg-transparent !text-white !border-white/60 hover:!bg-white/10';
232
+ }
233
+ return '';
234
+ };
235
+
236
+ const getButtonVariant = (button, index) => {
237
+ // If button has custom variant, respect it but convert ghost to outline for visibility
238
+ if (button.variant) {
239
+ // Ghost variant: always convert to outline for consistent text visibility
240
+ if (button.variant === 'ghost') {
241
+ return 'outline';
242
+ }
243
+ return button.variant;
244
+ }
245
+
246
+ const isLastButton = index === resolvedButtons.value.length - 1;
247
+
248
+ // Last button is always solid
249
+ if (isLastButton) return 'solid';
250
+
251
+ // Non-last buttons: soft when transparent (visible bg), outline when scrolled
252
+ return isScrolled.value || !props.transparent ? 'outline' : 'soft';
253
+ };
254
+
255
+ onMounted(() => {
256
+ const handleScroll = () => {
257
+ isScrolled.value = window.scrollY > props.scrollThreshold;
258
+ };
259
+
260
+ window.addEventListener('scroll', handleScroll, { passive: true });
261
+ handleScroll();
262
+
263
+ onUnmounted(() => {
264
+ window.removeEventListener('scroll', handleScroll);
265
+ });
266
+ });
267
+ </script>
@@ -0,0 +1,151 @@
1
+ <template>
2
+ <Transition
3
+ enter-active-class="transition-all duration-300 ease-out"
4
+ enter-from-class="translate-y-4 opacity-0"
5
+ leave-active-class="transition-all duration-200 ease-in"
6
+ leave-to-class="translate-y-4 opacity-0"
7
+ >
8
+ <aside
9
+ v-if="isVisible"
10
+ class="fixed bottom-6 right-6 z-40 max-w-sm"
11
+ >
12
+ <div class="xGlass rounded-2xl p-4 shadow-xl">
13
+ <!-- Close button -->
14
+ <button
15
+ class="absolute top-2 right-2 p-1 rounded-full hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
16
+ aria-label="Close"
17
+ @click="dismiss"
18
+ >
19
+ <UIcon name="i-lucide-x" class="w-4 h-4 text-gray-400" />
20
+ </button>
21
+
22
+ <!-- Content -->
23
+ <div class="flex gap-3">
24
+ <!-- Avatar -->
25
+ <img
26
+ v-if="avatar"
27
+ :src="avatar"
28
+ :alt="name"
29
+ class="w-12 h-12 rounded-full object-cover flex-shrink-0"
30
+ />
31
+ <div
32
+ v-else
33
+ class="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center flex-shrink-0"
34
+ >
35
+ <span class="text-primary-600 dark:text-primary-400 font-medium text-lg">
36
+ {{ name.charAt(0) }}
37
+ </span>
38
+ </div>
39
+
40
+ <div class="flex-1 min-w-0">
41
+ <!-- Header -->
42
+ <div class="flex items-center gap-2 mb-1">
43
+ <span class="font-medium text-gray-900 dark:text-white">
44
+ {{ name }}
45
+ </span>
46
+ <span
47
+ v-if="online"
48
+ class="w-2 h-2 rounded-full bg-green-500"
49
+ />
50
+ </div>
51
+
52
+ <!-- Message -->
53
+ <p class="text-sm text-gray-600 dark:text-gray-400">
54
+ {{ message }}
55
+ </p>
56
+
57
+ <!-- Action -->
58
+ <UButton
59
+ v-if="actionText"
60
+ size="sm"
61
+ class="mt-3"
62
+ @click="handleAction"
63
+ >
64
+ {{ actionText }}
65
+ </UButton>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </aside>
70
+ </Transition>
71
+ </template>
72
+
73
+ <script setup>
74
+ const props = defineProps({
75
+ /** v-model visibility */
76
+ modelValue: {
77
+ type: Boolean,
78
+ default: false,
79
+ },
80
+ /** Avatar image URL */
81
+ avatar: {
82
+ type: String,
83
+ default: '',
84
+ },
85
+ /** Person/bot name */
86
+ name: {
87
+ type: String,
88
+ required: true,
89
+ },
90
+ /** Chat message */
91
+ message: {
92
+ type: String,
93
+ required: true,
94
+ },
95
+ /** Show online indicator */
96
+ online: {
97
+ type: Boolean,
98
+ default: true,
99
+ },
100
+ /** Button text */
101
+ actionText: {
102
+ type: String,
103
+ default: '',
104
+ },
105
+ /** Auto-dismiss after ms (0 = never) */
106
+ autoDismiss: {
107
+ type: Number,
108
+ default: 0,
109
+ },
110
+ /** Delay before showing (ms) */
111
+ delay: {
112
+ type: Number,
113
+ default: 3000,
114
+ },
115
+ });
116
+
117
+ const emit = defineEmits(['update:modelValue', 'action', 'dismiss']);
118
+
119
+ const isVisible = ref(false);
120
+ let dismissTimeout = null;
121
+ let showTimeout = null;
122
+
123
+ const dismiss = () => {
124
+ isVisible.value = false;
125
+ emit('update:modelValue', false);
126
+ emit('dismiss');
127
+ };
128
+
129
+ const handleAction = () => {
130
+ emit('action');
131
+ };
132
+
133
+ watch(() => props.modelValue, (val) => {
134
+ if (val) {
135
+ showTimeout = setTimeout(() => {
136
+ isVisible.value = true;
137
+
138
+ if (props.autoDismiss > 0) {
139
+ dismissTimeout = setTimeout(dismiss, props.autoDismiss);
140
+ }
141
+ }, props.delay);
142
+ } else {
143
+ isVisible.value = false;
144
+ }
145
+ }, { immediate: true });
146
+
147
+ onUnmounted(() => {
148
+ if (dismissTimeout) clearTimeout(dismissTimeout);
149
+ if (showTimeout) clearTimeout(showTimeout);
150
+ });
151
+ </script>
@@ -0,0 +1,197 @@
1
+ <template>
2
+ <UModal
3
+ v-model:open="isOpen"
4
+ fullscreen
5
+ :ui="{
6
+ header: 'hidden',
7
+ body: 'p-0 overflow-y-auto',
8
+ footer: 'hidden',
9
+ overlay: 'bg-black/90'
10
+ }"
11
+ >
12
+ <!-- Trigger Slot -->
13
+ <slot>
14
+ <UButton
15
+ :label="buttonProps.label"
16
+ :color="buttonProps.color"
17
+ :variant="buttonProps.variant"
18
+ :size="buttonProps.size"
19
+ :leading-icon="buttonProps.icon"
20
+ />
21
+ </slot>
22
+
23
+ <!-- Modal Content -->
24
+ <template #body>
25
+ <!-- Close Button -->
26
+ <UButton
27
+ icon="i-lucide-x"
28
+ color="neutral"
29
+ variant="ghost"
30
+ class="fixed top-4 right-4 z-50 !bg-white/10 backdrop-blur-sm hover:!bg-white/20 !text-white"
31
+ size="lg"
32
+ aria-label="Close"
33
+ @click="isOpen = false"
34
+ />
35
+
36
+ <!-- Scrollable Content -->
37
+ <div class="min-h-screen">
38
+ <!-- Hero Section -->
39
+ <section
40
+ v-if="hero"
41
+ class="relative h-screen flex items-center justify-center"
42
+ >
43
+ <img
44
+ :src="hero.image"
45
+ :alt="hero.title"
46
+ class="absolute inset-0 w-full h-full object-cover"
47
+ />
48
+ <div class="absolute inset-0 bg-black/40" />
49
+ <div class="relative z-10 text-center text-white px-6">
50
+ <h2 class="xText-display">{{ hero.title }}</h2>
51
+ <p class="mt-4 text-xl text-white/80 max-w-2xl mx-auto">
52
+ {{ hero.subtitle }}
53
+ </p>
54
+ </div>
55
+ </section>
56
+
57
+ <!-- Feature Blocks -->
58
+ <template v-for="(block, index) in blocks" :key="index">
59
+ <!-- Full Image Block -->
60
+ <section
61
+ v-if="block.type === 'image'"
62
+ class="relative h-screen"
63
+ >
64
+ <img
65
+ :src="block.image"
66
+ :alt="block.alt"
67
+ class="w-full h-full object-cover"
68
+ />
69
+ </section>
70
+
71
+ <!-- Text Block -->
72
+ <section
73
+ v-else-if="block.type === 'text'"
74
+ class="py-32 px-6"
75
+ :class="block.bg || 'bg-white dark:bg-neutral-950'"
76
+ >
77
+ <div class="max-w-3xl mx-auto">
78
+ <h3 class="xText-headline text-neutral-900 dark:text-white">{{ block.title }}</h3>
79
+ <p class="mt-6 text-lg text-neutral-600 dark:text-neutral-400">
80
+ {{ block.body }}
81
+ </p>
82
+ </div>
83
+ </section>
84
+
85
+ <!-- Split Block -->
86
+ <section
87
+ v-else-if="block.type === 'split'"
88
+ class="grid lg:grid-cols-2 min-h-screen"
89
+ :class="{ 'lg:grid-flow-col-dense': block.imagePosition === 'left' }"
90
+ >
91
+ <div
92
+ class="flex items-center p-12 lg:p-20"
93
+ :class="[
94
+ block.bg || 'bg-white dark:bg-neutral-950',
95
+ block.imagePosition === 'left' ? 'lg:col-start-2' : ''
96
+ ]"
97
+ >
98
+ <div>
99
+ <h3 class="xText-headline text-neutral-900 dark:text-white">{{ block.title }}</h3>
100
+ <p class="mt-6 text-lg text-neutral-600 dark:text-neutral-400">
101
+ {{ block.body }}
102
+ </p>
103
+ <ul v-if="block.features" class="mt-8 space-y-4">
104
+ <li
105
+ v-for="feature in block.features"
106
+ :key="feature"
107
+ class="flex items-center gap-3"
108
+ >
109
+ <UIcon name="i-lucide-check-circle" class="w-5 h-5 text-primary-500" />
110
+ <span class="text-neutral-700 dark:text-neutral-300">{{ feature }}</span>
111
+ </li>
112
+ </ul>
113
+ </div>
114
+ </div>
115
+ <div
116
+ class="relative min-h-[50vh] lg:min-h-0"
117
+ :class="block.imagePosition === 'left' ? 'lg:col-start-1 lg:row-start-1' : ''"
118
+ >
119
+ <img
120
+ :src="block.image"
121
+ :alt="block.title"
122
+ class="absolute inset-0 w-full h-full object-cover"
123
+ />
124
+ </div>
125
+ </section>
126
+ </template>
127
+
128
+ <!-- CTA Section -->
129
+ <section
130
+ v-if="cta"
131
+ class="py-32 px-6 text-center"
132
+ :class="cta.bg || 'bg-neutral-950'"
133
+ >
134
+ <h3 class="xText-headline text-white">{{ cta.title }}</h3>
135
+ <p class="mt-4 text-xl text-white/70">{{ cta.subtitle }}</p>
136
+ <div class="mt-10">
137
+ <UButton size="xl" :color="cta.button?.color || 'primary'" @click="handleCtaClick">
138
+ {{ cta.button?.label || 'Get Started' }}
139
+ </UButton>
140
+ </div>
141
+ </section>
142
+ </div>
143
+ </template>
144
+ </UModal>
145
+ </template>
146
+
147
+ <script setup>
148
+ const props = defineProps({
149
+ /** Hero section configuration */
150
+ hero: {
151
+ type: Object,
152
+ default: null,
153
+ },
154
+ /** Content blocks array */
155
+ blocks: {
156
+ type: Array,
157
+ default: () => [],
158
+ },
159
+ /** CTA section configuration */
160
+ cta: {
161
+ type: Object,
162
+ default: null,
163
+ },
164
+ /** Button configuration object */
165
+ button: {
166
+ type: Object,
167
+ default: () => ({}),
168
+ },
169
+ });
170
+
171
+ const emit = defineEmits(['open', 'close', 'cta-click']);
172
+
173
+ const isOpen = ref(false);
174
+
175
+ // Button props with defaults
176
+ const buttonProps = computed(() => ({
177
+ label: props.button.label || 'Learn More',
178
+ color: props.button.color || 'primary',
179
+ variant: props.button.variant || 'solid',
180
+ size: props.button.size || 'md',
181
+ icon: props.button.icon || '',
182
+ }));
183
+
184
+ // Handle CTA click
185
+ const handleCtaClick = () => {
186
+ emit('cta-click');
187
+ };
188
+
189
+ // Emit events
190
+ watch(isOpen, (open) => {
191
+ if (open) {
192
+ emit('open');
193
+ } else {
194
+ emit('close');
195
+ }
196
+ });
197
+ </script>