@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,43 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
defineOptions({
|
|
7
|
+
inheritAttrs: false
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const toast = useToast()
|
|
11
|
+
|
|
12
|
+
onMounted(() => {
|
|
13
|
+
const accepted = localStorage.getItem('gdpr-cookies-accepted')
|
|
14
|
+
if (!accepted) {
|
|
15
|
+
toast.add({
|
|
16
|
+
title: 'We use cookies',
|
|
17
|
+
description: 'We use cookies to enhance your browsing experience and analyze site traffic. By clicking "Accept", you consent to our use of cookies.',
|
|
18
|
+
icon: 'i-lucide-cookie',
|
|
19
|
+
timeout: 0,
|
|
20
|
+
actions: [
|
|
21
|
+
{
|
|
22
|
+
label: 'Accept',
|
|
23
|
+
color: 'primary',
|
|
24
|
+
onClick: (e) => {
|
|
25
|
+
e?.stopPropagation()
|
|
26
|
+
localStorage.setItem('gdpr-cookies-accepted', 'true')
|
|
27
|
+
toast.clear()
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: 'Decline',
|
|
32
|
+
color: 'neutral',
|
|
33
|
+
variant: 'outline',
|
|
34
|
+
onClick: (e) => {
|
|
35
|
+
e?.stopPropagation()
|
|
36
|
+
toast.clear()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
</script>
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<UModal v-model:open="isOpen">
|
|
3
|
+
<!-- Trigger Slot -->
|
|
4
|
+
<slot>
|
|
5
|
+
<UButton
|
|
6
|
+
:label="buttonProps.label"
|
|
7
|
+
:color="buttonProps.color"
|
|
8
|
+
:variant="buttonProps.variant"
|
|
9
|
+
:size="buttonProps.size"
|
|
10
|
+
:leading-icon="buttonProps.icon"
|
|
11
|
+
/>
|
|
12
|
+
</slot>
|
|
13
|
+
|
|
14
|
+
<template #content>
|
|
15
|
+
<div class="p-6">
|
|
16
|
+
<!-- Header -->
|
|
17
|
+
<header class="mb-6">
|
|
18
|
+
<h3 class="text-xl font-semibold text-neutral-900 dark:text-white">
|
|
19
|
+
{{ title }}
|
|
20
|
+
</h3>
|
|
21
|
+
<p class="mt-2 text-sm text-neutral-600 dark:text-neutral-400">
|
|
22
|
+
{{ description }}
|
|
23
|
+
</p>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<!-- Cookie Categories -->
|
|
27
|
+
<div class="space-y-4">
|
|
28
|
+
<div
|
|
29
|
+
v-for="category in categories"
|
|
30
|
+
:key="category.id"
|
|
31
|
+
class="p-4 rounded-xl border border-neutral-200 dark:border-neutral-700"
|
|
32
|
+
>
|
|
33
|
+
<div class="flex items-start justify-between gap-4">
|
|
34
|
+
<div class="flex-1">
|
|
35
|
+
<div class="flex items-center gap-2">
|
|
36
|
+
<h4 class="font-medium text-neutral-900 dark:text-white">
|
|
37
|
+
{{ category.name }}
|
|
38
|
+
</h4>
|
|
39
|
+
<UBadge
|
|
40
|
+
v-if="category.required"
|
|
41
|
+
label="Required"
|
|
42
|
+
size="xs"
|
|
43
|
+
variant="subtle"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
<p class="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
|
|
47
|
+
{{ category.description }}
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
<UToggle
|
|
51
|
+
v-model="selections[category.id]"
|
|
52
|
+
:disabled="category.required"
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Actions -->
|
|
59
|
+
<footer class="mt-6 flex flex-col sm:flex-row gap-3">
|
|
60
|
+
<UButton
|
|
61
|
+
label="Reject All"
|
|
62
|
+
variant="ghost"
|
|
63
|
+
color="neutral"
|
|
64
|
+
class="flex-1"
|
|
65
|
+
@click="rejectAll"
|
|
66
|
+
/>
|
|
67
|
+
<UButton
|
|
68
|
+
label="Accept Selected"
|
|
69
|
+
variant="outline"
|
|
70
|
+
class="flex-1"
|
|
71
|
+
@click="acceptSelected"
|
|
72
|
+
/>
|
|
73
|
+
<UButton
|
|
74
|
+
label="Accept All"
|
|
75
|
+
class="flex-1"
|
|
76
|
+
@click="acceptAll"
|
|
77
|
+
/>
|
|
78
|
+
</footer>
|
|
79
|
+
|
|
80
|
+
<!-- Policy Links -->
|
|
81
|
+
<div class="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-700 text-center">
|
|
82
|
+
<div class="flex justify-center gap-4 text-sm">
|
|
83
|
+
<NuxtLink
|
|
84
|
+
v-if="privacyLink"
|
|
85
|
+
:to="privacyLink"
|
|
86
|
+
class="text-primary-500 hover:underline"
|
|
87
|
+
>
|
|
88
|
+
Privacy Policy
|
|
89
|
+
</NuxtLink>
|
|
90
|
+
<NuxtLink
|
|
91
|
+
v-if="cookieLink"
|
|
92
|
+
:to="cookieLink"
|
|
93
|
+
class="text-primary-500 hover:underline"
|
|
94
|
+
>
|
|
95
|
+
Cookie Policy
|
|
96
|
+
</NuxtLink>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
101
|
+
</UModal>
|
|
102
|
+
</template>
|
|
103
|
+
|
|
104
|
+
<script setup>
|
|
105
|
+
const props = defineProps({
|
|
106
|
+
/** v-model for modal visibility */
|
|
107
|
+
modelValue: {
|
|
108
|
+
type: Boolean,
|
|
109
|
+
default: false,
|
|
110
|
+
},
|
|
111
|
+
/** Modal title */
|
|
112
|
+
title: {
|
|
113
|
+
type: String,
|
|
114
|
+
default: 'Privacy Preferences',
|
|
115
|
+
},
|
|
116
|
+
/** Modal description */
|
|
117
|
+
description: {
|
|
118
|
+
type: String,
|
|
119
|
+
default: 'We use cookies to enhance your browsing experience. You can choose which categories to allow.',
|
|
120
|
+
},
|
|
121
|
+
/** Cookie categories */
|
|
122
|
+
categories: {
|
|
123
|
+
type: Array,
|
|
124
|
+
default: () => [
|
|
125
|
+
{
|
|
126
|
+
id: 'necessary',
|
|
127
|
+
name: 'Necessary',
|
|
128
|
+
description: 'Essential cookies required for the website to function properly.',
|
|
129
|
+
required: true,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'analytics',
|
|
133
|
+
name: 'Analytics',
|
|
134
|
+
description: 'Cookies that help us understand how visitors interact with our website.',
|
|
135
|
+
required: false,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'marketing',
|
|
139
|
+
name: 'Marketing',
|
|
140
|
+
description: 'Cookies used for advertising and promotional purposes.',
|
|
141
|
+
required: false,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'preferences',
|
|
145
|
+
name: 'Preferences',
|
|
146
|
+
description: 'Cookies that remember your preferences and settings.',
|
|
147
|
+
required: false,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
/** Privacy policy link */
|
|
152
|
+
privacyLink: {
|
|
153
|
+
type: String,
|
|
154
|
+
default: '/privacy',
|
|
155
|
+
},
|
|
156
|
+
/** Cookie policy link */
|
|
157
|
+
cookieLink: {
|
|
158
|
+
type: String,
|
|
159
|
+
default: '/cookies',
|
|
160
|
+
},
|
|
161
|
+
/** Storage key for preferences */
|
|
162
|
+
storageKey: {
|
|
163
|
+
type: String,
|
|
164
|
+
default: 'gdpr-preferences',
|
|
165
|
+
},
|
|
166
|
+
/** Button configuration object */
|
|
167
|
+
button: {
|
|
168
|
+
type: Object,
|
|
169
|
+
default: () => ({}),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const emit = defineEmits(['update:modelValue', 'accept', 'reject', 'save']);
|
|
174
|
+
|
|
175
|
+
// Support both v-model and self-managed state
|
|
176
|
+
const internalOpen = ref(false);
|
|
177
|
+
const isOpen = computed({
|
|
178
|
+
get: () => props.modelValue !== undefined ? props.modelValue : internalOpen.value,
|
|
179
|
+
set: (value) => {
|
|
180
|
+
internalOpen.value = value;
|
|
181
|
+
emit('update:modelValue', value);
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Button props with defaults
|
|
186
|
+
const buttonProps = computed(() => ({
|
|
187
|
+
label: props.button.label || 'Cookie Settings',
|
|
188
|
+
color: props.button.color || 'neutral',
|
|
189
|
+
variant: props.button.variant || 'outline',
|
|
190
|
+
size: props.button.size || 'md',
|
|
191
|
+
icon: props.button.icon || 'i-lucide-cookie',
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
// Initialize selections
|
|
195
|
+
const selections = ref({});
|
|
196
|
+
|
|
197
|
+
onMounted(() => {
|
|
198
|
+
// Load saved preferences
|
|
199
|
+
const saved = localStorage.getItem(props.storageKey);
|
|
200
|
+
if (saved) {
|
|
201
|
+
selections.value = JSON.parse(saved);
|
|
202
|
+
} else {
|
|
203
|
+
// Default: required categories enabled
|
|
204
|
+
props.categories.forEach((cat) => {
|
|
205
|
+
selections.value[cat.id] = cat.required;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const savePreferences = (prefs) => {
|
|
211
|
+
localStorage.setItem(props.storageKey, JSON.stringify(prefs));
|
|
212
|
+
localStorage.setItem('cookie-consent', 'customized');
|
|
213
|
+
emit('save', prefs);
|
|
214
|
+
isOpen.value = false;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const acceptAll = () => {
|
|
218
|
+
const prefs = {};
|
|
219
|
+
props.categories.forEach((cat) => {
|
|
220
|
+
prefs[cat.id] = true;
|
|
221
|
+
});
|
|
222
|
+
savePreferences(prefs);
|
|
223
|
+
emit('accept');
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const acceptSelected = () => {
|
|
227
|
+
// Ensure required categories are always true
|
|
228
|
+
const prefs = { ...selections.value };
|
|
229
|
+
props.categories.forEach((cat) => {
|
|
230
|
+
if (cat.required) {
|
|
231
|
+
prefs[cat.id] = true;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
savePreferences(prefs);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const rejectAll = () => {
|
|
238
|
+
const prefs = {};
|
|
239
|
+
props.categories.forEach((cat) => {
|
|
240
|
+
prefs[cat.id] = cat.required;
|
|
241
|
+
});
|
|
242
|
+
savePreferences(prefs);
|
|
243
|
+
emit('reject');
|
|
244
|
+
};
|
|
245
|
+
</script>
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section
|
|
3
|
+
class="relative border-t overflow-hidden"
|
|
4
|
+
:class="[
|
|
5
|
+
variant === 'dark'
|
|
6
|
+
? 'bg-neutral-950 border-neutral-800'
|
|
7
|
+
: 'bg-neutral-50 dark:bg-neutral-900 border-neutral-200 dark:border-neutral-800'
|
|
8
|
+
]"
|
|
9
|
+
>
|
|
10
|
+
<!-- Background Pattern -->
|
|
11
|
+
<XMarkPatternBg
|
|
12
|
+
v-if="pattern"
|
|
13
|
+
:pattern="pattern"
|
|
14
|
+
:opacity="patternOpacity"
|
|
15
|
+
:fade="false"
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-16">
|
|
19
|
+
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-8">
|
|
20
|
+
<!-- Content -->
|
|
21
|
+
<div class="flex-1">
|
|
22
|
+
<h3
|
|
23
|
+
class="text-xl font-semibold"
|
|
24
|
+
:class="variant === 'dark' ? 'text-white' : 'text-neutral-900 dark:text-white'"
|
|
25
|
+
>
|
|
26
|
+
{{ title }}
|
|
27
|
+
</h3>
|
|
28
|
+
<p
|
|
29
|
+
v-if="description"
|
|
30
|
+
class="mt-2"
|
|
31
|
+
:class="variant === 'dark' ? 'text-neutral-400' : 'text-neutral-600 dark:text-neutral-400'"
|
|
32
|
+
>
|
|
33
|
+
{{ description }}
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Form -->
|
|
38
|
+
<div class="flex-shrink-0 w-full md:w-auto">
|
|
39
|
+
<!-- Success State -->
|
|
40
|
+
<Transition
|
|
41
|
+
enter-active-class="transition-all duration-300"
|
|
42
|
+
enter-from-class="opacity-0 translate-y-2"
|
|
43
|
+
leave-active-class="transition-all duration-200"
|
|
44
|
+
leave-to-class="opacity-0"
|
|
45
|
+
>
|
|
46
|
+
<div
|
|
47
|
+
v-if="submitted"
|
|
48
|
+
class="flex items-center gap-3 px-4 py-3 rounded-lg"
|
|
49
|
+
:class="variant === 'dark' ? 'bg-primary-500/20 text-primary-300' : 'bg-primary-50 dark:bg-primary-950/30 text-primary-600 dark:text-primary-400'"
|
|
50
|
+
>
|
|
51
|
+
<UIcon name="i-lucide-check-circle" class="w-5 h-5" />
|
|
52
|
+
<span class="text-sm font-medium">{{ successMessage }}</span>
|
|
53
|
+
</div>
|
|
54
|
+
</Transition>
|
|
55
|
+
|
|
56
|
+
<!-- Form -->
|
|
57
|
+
<form
|
|
58
|
+
v-if="!submitted"
|
|
59
|
+
class="flex flex-col sm:flex-row gap-3"
|
|
60
|
+
@submit.prevent="handleSubmit"
|
|
61
|
+
>
|
|
62
|
+
<UInput
|
|
63
|
+
v-model="email"
|
|
64
|
+
type="email"
|
|
65
|
+
:placeholder="placeholder"
|
|
66
|
+
class="w-full sm:w-72"
|
|
67
|
+
:disabled="loading"
|
|
68
|
+
required
|
|
69
|
+
/>
|
|
70
|
+
<UButton
|
|
71
|
+
type="submit"
|
|
72
|
+
:loading="loading"
|
|
73
|
+
:color="button?.color || 'primary'"
|
|
74
|
+
class="whitespace-nowrap"
|
|
75
|
+
>
|
|
76
|
+
{{ button?.label || 'Subscribe' }}
|
|
77
|
+
</UButton>
|
|
78
|
+
</form>
|
|
79
|
+
|
|
80
|
+
<!-- Privacy Note -->
|
|
81
|
+
<p
|
|
82
|
+
v-if="hasPrivacy && !submitted"
|
|
83
|
+
class="mt-3 text-xs"
|
|
84
|
+
:class="variant === 'dark' ? 'text-neutral-500' : 'text-neutral-500 dark:text-neutral-500'"
|
|
85
|
+
>
|
|
86
|
+
{{ privacyText }}
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</section>
|
|
92
|
+
</template>
|
|
93
|
+
|
|
94
|
+
<script setup>
|
|
95
|
+
const props = defineProps({
|
|
96
|
+
/** Section title */
|
|
97
|
+
title: {
|
|
98
|
+
type: String,
|
|
99
|
+
default: 'Subscribe to our newsletter',
|
|
100
|
+
},
|
|
101
|
+
/** Section description */
|
|
102
|
+
description: {
|
|
103
|
+
type: String,
|
|
104
|
+
default: 'Get the latest news and updates.',
|
|
105
|
+
},
|
|
106
|
+
/** Email placeholder */
|
|
107
|
+
placeholder: {
|
|
108
|
+
type: String,
|
|
109
|
+
default: 'Enter your email',
|
|
110
|
+
},
|
|
111
|
+
/** Button config { label: string, color?: string } */
|
|
112
|
+
button: {
|
|
113
|
+
type: Object,
|
|
114
|
+
default: () => ({ label: 'Subscribe' }),
|
|
115
|
+
},
|
|
116
|
+
/** Has privacy note */
|
|
117
|
+
hasPrivacy: {
|
|
118
|
+
type: Boolean,
|
|
119
|
+
default: true,
|
|
120
|
+
},
|
|
121
|
+
/** Privacy note text */
|
|
122
|
+
privacyText: {
|
|
123
|
+
type: String,
|
|
124
|
+
default: 'No spam. Unsubscribe anytime.',
|
|
125
|
+
},
|
|
126
|
+
/** Success message after submission */
|
|
127
|
+
successMessage: {
|
|
128
|
+
type: String,
|
|
129
|
+
default: 'Thanks for subscribing!',
|
|
130
|
+
},
|
|
131
|
+
/** Style variant */
|
|
132
|
+
variant: {
|
|
133
|
+
type: String,
|
|
134
|
+
default: 'dark',
|
|
135
|
+
validator: (v) => ['dark', 'light'].includes(v),
|
|
136
|
+
},
|
|
137
|
+
/** Background pattern type (dots, grid, diagonal, topography, circuit, waves) or falsy to disable */
|
|
138
|
+
pattern: {
|
|
139
|
+
type: String,
|
|
140
|
+
default: null,
|
|
141
|
+
validator: (v) => !v || ['dots', 'grid', 'diagonal', 'topography', 'circuit', 'waves'].includes(v),
|
|
142
|
+
},
|
|
143
|
+
/** Pattern opacity (0-1) */
|
|
144
|
+
patternOpacity: {
|
|
145
|
+
type: Number,
|
|
146
|
+
default: 0.05,
|
|
147
|
+
},
|
|
148
|
+
/** Reset form after delay (ms), 0 to not reset */
|
|
149
|
+
resetDelay: {
|
|
150
|
+
type: Number,
|
|
151
|
+
default: 5000,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const emit = defineEmits(['submit']);
|
|
156
|
+
|
|
157
|
+
const email = ref('');
|
|
158
|
+
const loading = ref(false);
|
|
159
|
+
const submitted = ref(false);
|
|
160
|
+
|
|
161
|
+
const handleSubmit = async () => {
|
|
162
|
+
if (!email.value) return;
|
|
163
|
+
|
|
164
|
+
loading.value = true;
|
|
165
|
+
|
|
166
|
+
// Emit the email for parent to handle
|
|
167
|
+
emit('submit', email.value);
|
|
168
|
+
|
|
169
|
+
// Simulate submission delay
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
171
|
+
|
|
172
|
+
loading.value = false;
|
|
173
|
+
submitted.value = true;
|
|
174
|
+
|
|
175
|
+
// Reset after delay if configured
|
|
176
|
+
if (props.resetDelay > 0) {
|
|
177
|
+
setTimeout(() => {
|
|
178
|
+
submitted.value = false;
|
|
179
|
+
email.value = '';
|
|
180
|
+
}, props.resetDelay);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
</script>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="relative w-full overflow-hidden"
|
|
4
|
+
:style="{ height: heightValue }"
|
|
5
|
+
>
|
|
6
|
+
<svg
|
|
7
|
+
class="absolute inset-0 w-full h-full"
|
|
8
|
+
:class="{ 'scale-x-[-1]': reverse, 'scale-y-[-1]': invert }"
|
|
9
|
+
preserveAspectRatio="none"
|
|
10
|
+
:viewBox="`0 0 1200 ${viewBoxHeight}`"
|
|
11
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
12
|
+
>
|
|
13
|
+
<!-- Full background with top color -->
|
|
14
|
+
<rect x="0" y="0" width="1200" :height="viewBoxHeight" :fill="topColor" />
|
|
15
|
+
<!-- Shape path with bottom color -->
|
|
16
|
+
<path :d="pathData" :fill="bottomColor" />
|
|
17
|
+
</svg>
|
|
18
|
+
|
|
19
|
+
<!-- Glow line -->
|
|
20
|
+
<div
|
|
21
|
+
v-if="glow"
|
|
22
|
+
class="absolute left-0 right-0 h-px"
|
|
23
|
+
:style="glowLineStyle"
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup lang="ts">
|
|
29
|
+
const props = withDefaults(
|
|
30
|
+
defineProps<{
|
|
31
|
+
/** Shape variant */
|
|
32
|
+
variant?: 'concave' | 'convex' | 'diagonal' | 'slant' | 'clouds' | 'waves' | 'tilt' | 'peaks' | 'scallop' | 'arrow'
|
|
33
|
+
/** Top section color */
|
|
34
|
+
topColor?: string
|
|
35
|
+
/** Bottom section color */
|
|
36
|
+
bottomColor?: string
|
|
37
|
+
/** Height size */
|
|
38
|
+
height?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'
|
|
39
|
+
/** Reverse direction (flip horizontally) */
|
|
40
|
+
reverse?: boolean
|
|
41
|
+
/** Invert vertically (swap top/bottom) */
|
|
42
|
+
invert?: boolean
|
|
43
|
+
/** Show glow line */
|
|
44
|
+
glow?: boolean
|
|
45
|
+
/** Glow line color */
|
|
46
|
+
glowColor?: string
|
|
47
|
+
}>(),
|
|
48
|
+
{
|
|
49
|
+
variant: 'diagonal',
|
|
50
|
+
topColor: 'rgb(243 244 246)',
|
|
51
|
+
bottomColor: 'rgb(249 250 251)',
|
|
52
|
+
height: 'md',
|
|
53
|
+
reverse: false,
|
|
54
|
+
invert: false,
|
|
55
|
+
glow: false,
|
|
56
|
+
glowColor: 'rgba(99, 102, 241, 0.5)',
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const heightValue = computed(() => {
|
|
61
|
+
const heights = {
|
|
62
|
+
sm: '40px',
|
|
63
|
+
md: '64px',
|
|
64
|
+
lg: '96px',
|
|
65
|
+
xl: '128px',
|
|
66
|
+
'2xl': '160px',
|
|
67
|
+
'3xl': '200px',
|
|
68
|
+
}
|
|
69
|
+
return heights[props.height]
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const viewBoxHeight = computed(() => {
|
|
73
|
+
const heights = {
|
|
74
|
+
sm: 80,
|
|
75
|
+
md: 120,
|
|
76
|
+
lg: 160,
|
|
77
|
+
xl: 200,
|
|
78
|
+
'2xl': 240,
|
|
79
|
+
'3xl': 280,
|
|
80
|
+
}
|
|
81
|
+
return heights[props.height]
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const pathData = computed(() => {
|
|
85
|
+
const h = viewBoxHeight.value
|
|
86
|
+
const mid = h / 2
|
|
87
|
+
|
|
88
|
+
const paths = {
|
|
89
|
+
// Full diagonal from top-left corner to bottom-right corner
|
|
90
|
+
diagonal: `M0 0 L1200 ${h} L0 ${h} Z`,
|
|
91
|
+
|
|
92
|
+
// Slant - opposite direction, from top-right to bottom-left
|
|
93
|
+
slant: `M1200 0 L1200 ${h} L0 ${h} Z`,
|
|
94
|
+
|
|
95
|
+
// Concave - curves inward (scoops down)
|
|
96
|
+
concave: `M0 ${mid} Q600 ${h} 1200 ${mid} L1200 ${h} L0 ${h} Z`,
|
|
97
|
+
|
|
98
|
+
// Convex - curves outward (bulges up)
|
|
99
|
+
convex: `M0 ${mid} Q600 0 1200 ${mid} L1200 ${h} L0 ${h} Z`,
|
|
100
|
+
|
|
101
|
+
// Tilt - gentle diagonal (less steep than full diagonal)
|
|
102
|
+
tilt: `M0 0 L1200 ${mid + 20} L1200 ${h} L0 ${h} Z`,
|
|
103
|
+
|
|
104
|
+
// Waves - multiple wave pattern
|
|
105
|
+
waves: `M0 ${mid}
|
|
106
|
+
Q150 ${mid - 30} 300 ${mid}
|
|
107
|
+
T600 ${mid}
|
|
108
|
+
T900 ${mid}
|
|
109
|
+
T1200 ${mid}
|
|
110
|
+
L1200 ${h} L0 ${h} Z`,
|
|
111
|
+
|
|
112
|
+
// Clouds - fluffy bumps
|
|
113
|
+
clouds: `M0 ${mid + 20}
|
|
114
|
+
Q100 ${mid - 40} 200 ${mid + 10}
|
|
115
|
+
Q300 ${mid - 50} 400 ${mid + 15}
|
|
116
|
+
Q500 ${mid - 45} 600 ${mid + 5}
|
|
117
|
+
Q700 ${mid - 55} 800 ${mid + 20}
|
|
118
|
+
Q900 ${mid - 40} 1000 ${mid + 10}
|
|
119
|
+
Q1100 ${mid - 50} 1200 ${mid + 15}
|
|
120
|
+
L1200 ${h} L0 ${h} Z`,
|
|
121
|
+
|
|
122
|
+
// Peaks - mountain-like
|
|
123
|
+
peaks: `M0 ${mid + 20}
|
|
124
|
+
L200 ${mid - 30}
|
|
125
|
+
L400 ${mid + 10}
|
|
126
|
+
L600 ${mid - 40}
|
|
127
|
+
L800 ${mid + 5}
|
|
128
|
+
L1000 ${mid - 25}
|
|
129
|
+
L1200 ${mid + 15}
|
|
130
|
+
L1200 ${h} L0 ${h} Z`,
|
|
131
|
+
|
|
132
|
+
// Scallop - repeating curves
|
|
133
|
+
scallop: `M0 ${mid}
|
|
134
|
+
Q100 ${mid - 40} 200 ${mid}
|
|
135
|
+
Q300 ${mid - 40} 400 ${mid}
|
|
136
|
+
Q500 ${mid - 40} 600 ${mid}
|
|
137
|
+
Q700 ${mid - 40} 800 ${mid}
|
|
138
|
+
Q900 ${mid - 40} 1000 ${mid}
|
|
139
|
+
Q1100 ${mid - 40} 1200 ${mid}
|
|
140
|
+
L1200 ${h} L0 ${h} Z`,
|
|
141
|
+
|
|
142
|
+
// Arrow - pointed center
|
|
143
|
+
arrow: `M0 ${mid + 20} L600 ${mid - 50} L1200 ${mid + 20} L1200 ${h} L0 ${h} Z`,
|
|
144
|
+
}
|
|
145
|
+
return paths[props.variant]
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const glowLineStyle = computed(() => {
|
|
149
|
+
return {
|
|
150
|
+
top: '50%',
|
|
151
|
+
transform: 'translateY(-50%)',
|
|
152
|
+
background: props.glowColor,
|
|
153
|
+
boxShadow: `0 0 8px ${props.glowColor}, 0 0 16px ${props.glowColor}`,
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
</script>
|