@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,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>
|