@xenterprises/nuxt-x-marketing 1.0.0 → 1.0.2
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 +2 -1
- package/.playground/app.vue +12 -6
- package/.playground/pages/blog/detail.vue +341 -249
- package/.playground/pages/index.vue +2 -2
- package/.playground/pages/layout/x-legal.vue +12 -12
- package/.playground/pages/notifications/social-proof-toast.vue +330 -70
- package/LICENSE +60 -0
- package/README.md +210 -64
- package/app/app.config.ts +2 -72
- package/app/assets/css/x-marketing.css +1 -0
- package/app/components/X/Footer/index.vue +3 -4
- package/app/components/X/Mark/Affiliate/ComparisonTable.vue +382 -0
- package/app/components/X/Mark/Affiliate/Disclosure.vue +138 -0
- package/app/components/X/Mark/Affiliate/ProductCard.vue +431 -0
- package/app/components/X/Mark/Affiliate/ProductDetail.vue +410 -0
- package/app/components/X/Mark/Affiliate/ProductGrid.vue +101 -0
- package/app/components/X/Mark/Blog/Author.vue +26 -20
- package/app/components/X/Mark/Blog/Card.vue +28 -15
- package/app/components/X/Mark/Blog/Detail.vue +82 -127
- package/app/components/X/Mark/Blog/List.vue +17 -16
- package/app/components/X/Mark/CTA/index.vue +74 -70
- package/app/components/X/Mark/Card/Promo.vue +149 -139
- package/app/components/X/Mark/CodeBlock/index.vue +69 -0
- package/app/components/X/Mark/Directory/CategoryHero.vue +124 -0
- package/app/components/X/Mark/Directory/ListingCard.vue +336 -0
- package/app/components/X/Mark/Directory/ListingDetail.vue +306 -0
- package/app/components/X/Mark/Directory/ListingGrid.vue +190 -0
- package/app/components/X/Mark/Directory/SubmitCTA.vue +201 -0
- package/app/components/X/Mark/FAQ/index.vue +191 -0
- package/app/components/X/Mark/Features/index.vue +87 -35
- package/app/components/X/Mark/Hero/index.vue +41 -38
- package/app/components/X/Mark/Layout/AnnouncementBar.vue +20 -28
- package/app/components/X/Mark/Modal/Chat.vue +124 -124
- package/app/components/X/Mark/Modal/Video.vue +46 -36
- package/app/components/X/Mark/Pricing/Comparison.vue +76 -21
- package/app/components/X/Mark/Pricing/Plans.vue +75 -25
- package/app/components/X/Mark/SocialProof/Testimonials.vue +89 -21
- package/app/components/X/Mark/SocialProof/Toast.vue +155 -102
- package/app/components/X/X/Footer/index.vue +101 -0
- package/app/components/X/X/Header/Nav/index.vue +62 -0
- package/app/components/X/X/Header/index.vue +72 -0
- package/app/components/X/X/Legal/index.vue +94 -0
- package/app/composables/useParallax.ts +32 -19
- package/app/composables/useScrollReveal.ts +20 -13
- package/app/pages/index.vue +230 -28
- package/componentList.json +1856 -238
- package/nuxt.config.ts +12 -3
- package/package.json +5 -2
- package/screenshots/playground-home.png +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="faq-container">
|
|
3
|
+
<!-- Accordion Layout -->
|
|
4
|
+
<div
|
|
5
|
+
v-if="layout === 'accordion'"
|
|
6
|
+
class="border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden"
|
|
7
|
+
>
|
|
8
|
+
<div
|
|
9
|
+
v-for="(item, index) in items"
|
|
10
|
+
:key="index"
|
|
11
|
+
class="faq-item border-b border-neutral-200 dark:border-neutral-800 last:border-b-0"
|
|
12
|
+
>
|
|
13
|
+
<!-- Question -->
|
|
14
|
+
<button
|
|
15
|
+
:aria-expanded="openIndexes.includes(index)"
|
|
16
|
+
class="faq-question w-full flex items-center justify-between p-5 text-left hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors duration-200"
|
|
17
|
+
@click="toggleItem(index)"
|
|
18
|
+
>
|
|
19
|
+
<span class="font-medium text-neutral-900 dark:text-white pr-4">
|
|
20
|
+
{{ item.question }}
|
|
21
|
+
</span>
|
|
22
|
+
<UIcon
|
|
23
|
+
name="i-lucide-chevron-down"
|
|
24
|
+
class="w-5 h-5 text-neutral-500 dark:text-neutral-400 flex-shrink-0 transition-transform duration-300"
|
|
25
|
+
:class="{ 'rotate-180': openIndexes.includes(index) }"
|
|
26
|
+
/>
|
|
27
|
+
</button>
|
|
28
|
+
|
|
29
|
+
<!-- Answer -->
|
|
30
|
+
<div
|
|
31
|
+
ref="answerRefs"
|
|
32
|
+
class="faq-answer overflow-hidden transition-all duration-300 ease-in-out"
|
|
33
|
+
:style="{
|
|
34
|
+
maxHeight: openIndexes.includes(index)
|
|
35
|
+
? answerHeights[index] + 'px'
|
|
36
|
+
: '0',
|
|
37
|
+
opacity: openIndexes.includes(index) ? 1 : 0,
|
|
38
|
+
}"
|
|
39
|
+
>
|
|
40
|
+
<div
|
|
41
|
+
class="p-5 pt-0 text-neutral-600 dark:text-neutral-400 prose prose-neutral dark:prose-invert max-w-none"
|
|
42
|
+
>
|
|
43
|
+
{{ item.answer }}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Two-Column Layout -->
|
|
50
|
+
<div
|
|
51
|
+
v-else-if="layout === 'two-column'"
|
|
52
|
+
class="grid md:grid-cols-2 gap-x-12 gap-y-8"
|
|
53
|
+
>
|
|
54
|
+
<!-- Questions Column -->
|
|
55
|
+
<div class="questions-column space-y-4">
|
|
56
|
+
<div
|
|
57
|
+
v-for="(item, index) in items"
|
|
58
|
+
:key="index"
|
|
59
|
+
class="font-medium text-neutral-900 dark:text-white"
|
|
60
|
+
>
|
|
61
|
+
{{ item.question }}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- Answers Column -->
|
|
66
|
+
<div class="answers-column space-y-4">
|
|
67
|
+
<div
|
|
68
|
+
v-for="(item, index) in items"
|
|
69
|
+
:key="index"
|
|
70
|
+
class="text-neutral-600 dark:text-neutral-400"
|
|
71
|
+
>
|
|
72
|
+
{{ item.answer }}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<script setup>
|
|
80
|
+
const props = defineProps({
|
|
81
|
+
/** Array of FAQ items [{ question, answer, open? }] */
|
|
82
|
+
items: {
|
|
83
|
+
type: Array,
|
|
84
|
+
default: () => [
|
|
85
|
+
{
|
|
86
|
+
question: "What is this?",
|
|
87
|
+
answer: "This is a frequently asked question.",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
/** Layout type: 'accordion' or 'two-column' */
|
|
92
|
+
layout: {
|
|
93
|
+
type: String,
|
|
94
|
+
default: "accordion",
|
|
95
|
+
validator: (value) => ["accordion", "two-column"].includes(value),
|
|
96
|
+
},
|
|
97
|
+
/** Whether the first item should start open */
|
|
98
|
+
openFirst: {
|
|
99
|
+
type: Boolean,
|
|
100
|
+
default: false,
|
|
101
|
+
},
|
|
102
|
+
/** Whether multiple items can be open at once */
|
|
103
|
+
allowMultiple: {
|
|
104
|
+
type: Boolean,
|
|
105
|
+
default: false,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Track which items are open
|
|
110
|
+
const openIndexes = ref([]);
|
|
111
|
+
|
|
112
|
+
// Track answer heights for smooth transitions
|
|
113
|
+
const answerHeights = ref({});
|
|
114
|
+
|
|
115
|
+
// Initialize open state based on props
|
|
116
|
+
onMounted(() => {
|
|
117
|
+
// Set initial open indexes
|
|
118
|
+
const initialOpen = [];
|
|
119
|
+
props.items.forEach((item, index) => {
|
|
120
|
+
if (item.open || (props.openFirst && index === 0)) {
|
|
121
|
+
initialOpen.push(index);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
openIndexes.value = initialOpen;
|
|
125
|
+
|
|
126
|
+
// Calculate initial heights after DOM update
|
|
127
|
+
nextTick(() => {
|
|
128
|
+
calculateHeights();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Recalculate heights on window resize
|
|
133
|
+
onMounted(() => {
|
|
134
|
+
if (typeof window !== "undefined") {
|
|
135
|
+
window.addEventListener("resize", calculateHeights);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
onUnmounted(() => {
|
|
140
|
+
if (typeof window !== "undefined") {
|
|
141
|
+
window.removeEventListener("resize", calculateHeights);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Watch for items changes to recalculate heights
|
|
146
|
+
watch(
|
|
147
|
+
() => props.items,
|
|
148
|
+
() => {
|
|
149
|
+
nextTick(() => {
|
|
150
|
+
calculateHeights();
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
{ deep: true }
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
function calculateHeights() {
|
|
157
|
+
// Need to access DOM elements - we'll estimate based on content
|
|
158
|
+
// This is a simplified approach - in production, you might measure actual heights
|
|
159
|
+
props.items.forEach((item, index) => {
|
|
160
|
+
// Estimate height based on answer length (rough approximation)
|
|
161
|
+
const answerLength = item.answer?.length || 0;
|
|
162
|
+
// Average characters per line ~50, line height ~1.6, padding ~20px
|
|
163
|
+
const lines = Math.ceil(answerLength / 50);
|
|
164
|
+
const estimatedHeight = lines * 28 + 40; // ~28px per line + padding
|
|
165
|
+
answerHeights.value[index] = Math.max(estimatedHeight, 100);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function toggleItem(index) {
|
|
170
|
+
const isOpen = openIndexes.value.includes(index);
|
|
171
|
+
|
|
172
|
+
if (isOpen) {
|
|
173
|
+
// Close the item
|
|
174
|
+
openIndexes.value = openIndexes.value.filter((i) => i !== index);
|
|
175
|
+
} else {
|
|
176
|
+
// Open the item
|
|
177
|
+
if (props.allowMultiple) {
|
|
178
|
+
// Allow multiple - just add to open indexes
|
|
179
|
+
openIndexes.value = [...openIndexes.value, index];
|
|
180
|
+
} else {
|
|
181
|
+
// Only one at a time - close others and open this one
|
|
182
|
+
openIndexes.value = [index];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Recalculate heights after toggle
|
|
187
|
+
nextTick(() => {
|
|
188
|
+
calculateHeights();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
</script>
|
|
@@ -14,10 +14,7 @@
|
|
|
14
14
|
v-if="feature.icon"
|
|
15
15
|
class="w-12 h-12 rounded-xl bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center mb-4 transition-transform duration-300 group-hover:scale-110"
|
|
16
16
|
>
|
|
17
|
-
<UIcon
|
|
18
|
-
:name="feature.icon"
|
|
19
|
-
class="w-6 h-6 text-primary-500"
|
|
20
|
-
/>
|
|
17
|
+
<UIcon :name="feature.icon" class="w-6 h-6 text-primary-500" />
|
|
21
18
|
</div>
|
|
22
19
|
|
|
23
20
|
<!-- Image -->
|
|
@@ -45,12 +42,23 @@
|
|
|
45
42
|
<!-- Link -->
|
|
46
43
|
<NuxtLink
|
|
47
44
|
v-if="feature.link"
|
|
48
|
-
:to="
|
|
49
|
-
|
|
45
|
+
:to="
|
|
46
|
+
typeof feature.link === 'string' ? feature.link : feature.link.to
|
|
47
|
+
"
|
|
48
|
+
:target="
|
|
49
|
+
typeof feature.link === 'object' ? feature.link.target : undefined
|
|
50
|
+
"
|
|
50
51
|
class="inline-flex items-center gap-1 mt-4 text-primary-500 hover:text-primary-600 font-medium text-sm transition-colors group/link"
|
|
51
52
|
>
|
|
52
|
-
{{
|
|
53
|
-
|
|
53
|
+
{{
|
|
54
|
+
typeof feature.link === "object" && feature.link.label
|
|
55
|
+
? feature.link.label
|
|
56
|
+
: "Learn more"
|
|
57
|
+
}}
|
|
58
|
+
<UIcon
|
|
59
|
+
name="i-lucide-arrow-right"
|
|
60
|
+
class="w-4 h-4 transition-transform duration-300 group-hover/link:translate-x-1"
|
|
61
|
+
/>
|
|
54
62
|
</NuxtLink>
|
|
55
63
|
</article>
|
|
56
64
|
</div>
|
|
@@ -70,10 +78,7 @@
|
|
|
70
78
|
v-if="feature.icon"
|
|
71
79
|
class="flex-shrink-0 w-12 h-12 rounded-xl bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center transition-transform duration-300 group-hover:scale-110"
|
|
72
80
|
>
|
|
73
|
-
<UIcon
|
|
74
|
-
:name="feature.icon"
|
|
75
|
-
class="w-6 h-6 text-primary-500"
|
|
76
|
-
/>
|
|
81
|
+
<UIcon :name="feature.icon" class="w-6 h-6 text-primary-500" />
|
|
77
82
|
</div>
|
|
78
83
|
|
|
79
84
|
<!-- Content -->
|
|
@@ -86,12 +91,27 @@
|
|
|
86
91
|
</p>
|
|
87
92
|
<NuxtLink
|
|
88
93
|
v-if="feature.link"
|
|
89
|
-
:to="
|
|
90
|
-
|
|
94
|
+
:to="
|
|
95
|
+
typeof feature.link === 'string'
|
|
96
|
+
? feature.link
|
|
97
|
+
: feature.link.to
|
|
98
|
+
"
|
|
99
|
+
:target="
|
|
100
|
+
typeof feature.link === 'object'
|
|
101
|
+
? feature.link.target
|
|
102
|
+
: undefined
|
|
103
|
+
"
|
|
91
104
|
class="inline-flex items-center gap-1 mt-4 text-primary-500 hover:text-primary-600 font-medium text-sm transition-colors group/link"
|
|
92
105
|
>
|
|
93
|
-
{{
|
|
94
|
-
|
|
106
|
+
{{
|
|
107
|
+
typeof feature.link === "object" && feature.link.label
|
|
108
|
+
? feature.link.label
|
|
109
|
+
: "Learn more"
|
|
110
|
+
}}
|
|
111
|
+
<UIcon
|
|
112
|
+
name="i-lucide-arrow-right"
|
|
113
|
+
class="w-4 h-4 transition-transform duration-300 group-hover/link:translate-x-1"
|
|
114
|
+
/>
|
|
95
115
|
</NuxtLink>
|
|
96
116
|
</div>
|
|
97
117
|
</article>
|
|
@@ -127,26 +147,42 @@
|
|
|
127
147
|
v-if="feature.icon"
|
|
128
148
|
class="w-14 h-14 rounded-xl bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center mb-6 transition-transform duration-300 group-hover:scale-110"
|
|
129
149
|
>
|
|
130
|
-
<UIcon
|
|
131
|
-
:name="feature.icon"
|
|
132
|
-
class="w-7 h-7 text-primary-500"
|
|
133
|
-
/>
|
|
150
|
+
<UIcon :name="feature.icon" class="w-7 h-7 text-primary-500" />
|
|
134
151
|
</div>
|
|
135
152
|
|
|
136
|
-
<h3
|
|
153
|
+
<h3
|
|
154
|
+
class="xText-headline text-neutral-900 dark:text-white xText-balance"
|
|
155
|
+
>
|
|
137
156
|
{{ feature.title }}
|
|
138
157
|
</h3>
|
|
139
|
-
<p
|
|
158
|
+
<p
|
|
159
|
+
class="mt-4 text-lg text-neutral-600 dark:text-neutral-400 leading-relaxed"
|
|
160
|
+
>
|
|
140
161
|
{{ feature.description }}
|
|
141
162
|
</p>
|
|
142
163
|
<NuxtLink
|
|
143
164
|
v-if="feature.link"
|
|
144
|
-
:to="
|
|
145
|
-
|
|
165
|
+
:to="
|
|
166
|
+
typeof feature.link === 'string'
|
|
167
|
+
? feature.link
|
|
168
|
+
: feature.link.to
|
|
169
|
+
"
|
|
170
|
+
:target="
|
|
171
|
+
typeof feature.link === 'object'
|
|
172
|
+
? feature.link.target
|
|
173
|
+
: undefined
|
|
174
|
+
"
|
|
146
175
|
class="inline-flex items-center gap-2 mt-6 text-primary-500 hover:text-primary-600 font-medium transition-colors group/link"
|
|
147
176
|
>
|
|
148
|
-
{{
|
|
149
|
-
|
|
177
|
+
{{
|
|
178
|
+
typeof feature.link === "object" && feature.link.label
|
|
179
|
+
? feature.link.label
|
|
180
|
+
: "Learn more"
|
|
181
|
+
}}
|
|
182
|
+
<UIcon
|
|
183
|
+
name="i-lucide-arrow-right"
|
|
184
|
+
class="w-5 h-5 transition-transform duration-300 group-hover/link:translate-x-1"
|
|
185
|
+
/>
|
|
150
186
|
</NuxtLink>
|
|
151
187
|
</div>
|
|
152
188
|
</article>
|
|
@@ -157,19 +193,35 @@
|
|
|
157
193
|
|
|
158
194
|
<script setup>
|
|
159
195
|
// Initialize scroll reveal animations automatically
|
|
160
|
-
useScrollReveal()
|
|
196
|
+
useScrollReveal();
|
|
161
197
|
|
|
162
198
|
const props = defineProps({
|
|
163
|
-
/** Array of features */
|
|
199
|
+
/** Array of features [{ icon?, title, description?, img?, link? }] */
|
|
164
200
|
features: {
|
|
165
201
|
type: Array,
|
|
166
|
-
|
|
202
|
+
default: () => [
|
|
203
|
+
{
|
|
204
|
+
icon: "i-lucide-zap",
|
|
205
|
+
title: "Lightning Fast",
|
|
206
|
+
description: "Built for performance from the ground up.",
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
icon: "i-lucide-shield",
|
|
210
|
+
title: "Secure by Default",
|
|
211
|
+
description: "Enterprise-grade security out of the box.",
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
icon: "i-lucide-layers",
|
|
215
|
+
title: "Fully Customizable",
|
|
216
|
+
description: "Adapt every component to match your brand.",
|
|
217
|
+
},
|
|
218
|
+
],
|
|
167
219
|
},
|
|
168
220
|
/** Display layout */
|
|
169
221
|
layout: {
|
|
170
222
|
type: String,
|
|
171
|
-
default:
|
|
172
|
-
validator: (v) => [
|
|
223
|
+
default: "grid",
|
|
224
|
+
validator: (v) => ["grid", "list", "alternating"].includes(v),
|
|
173
225
|
},
|
|
174
226
|
/** Grid columns (for grid layout) */
|
|
175
227
|
columns: {
|
|
@@ -180,14 +232,14 @@ const props = defineProps({
|
|
|
180
232
|
});
|
|
181
233
|
|
|
182
234
|
const containerClasses = computed(() => {
|
|
183
|
-
return props.layout ===
|
|
235
|
+
return props.layout === "alternating" ? "max-w-6xl mx-auto" : "";
|
|
184
236
|
});
|
|
185
237
|
|
|
186
238
|
const gridClasses = computed(() => {
|
|
187
239
|
const cols = {
|
|
188
|
-
2:
|
|
189
|
-
3:
|
|
190
|
-
4:
|
|
240
|
+
2: "grid md:grid-cols-2 gap-8",
|
|
241
|
+
3: "grid md:grid-cols-2 lg:grid-cols-3 gap-8",
|
|
242
|
+
4: "grid md:grid-cols-2 lg:grid-cols-4 gap-8",
|
|
191
243
|
};
|
|
192
244
|
return cols[props.columns];
|
|
193
245
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
:class="[
|
|
5
5
|
alignmentClasses,
|
|
6
6
|
verticalAlignClasses,
|
|
7
|
-
{ 'pt-20': includeNavPadding }
|
|
7
|
+
{ 'pt-20': includeNavPadding },
|
|
8
8
|
]"
|
|
9
9
|
>
|
|
10
10
|
<!-- Background -->
|
|
@@ -44,15 +44,9 @@
|
|
|
44
44
|
|
|
45
45
|
<!-- Content -->
|
|
46
46
|
<div class="relative z-10 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
47
|
-
<div
|
|
48
|
-
class="max-w-3xl"
|
|
49
|
-
:class="contentAlignmentClasses"
|
|
50
|
-
>
|
|
47
|
+
<div class="max-w-3xl" :class="contentAlignmentClasses">
|
|
51
48
|
<!-- Eyebrow -->
|
|
52
|
-
<p
|
|
53
|
-
v-if="eyebrow"
|
|
54
|
-
class="xText-eyebrow text-white/70 mb-4 xFadeUp"
|
|
55
|
-
>
|
|
49
|
+
<p v-if="eyebrow" class="xText-eyebrow text-white/70 mb-4 xFadeUp">
|
|
56
50
|
{{ eyebrow }}
|
|
57
51
|
</p>
|
|
58
52
|
|
|
@@ -77,7 +71,10 @@
|
|
|
77
71
|
<div
|
|
78
72
|
v-if="buttons.length || $slots.actions"
|
|
79
73
|
class="mt-10 flex flex-wrap gap-4 xFadeUp"
|
|
80
|
-
:class="{
|
|
74
|
+
:class="{
|
|
75
|
+
'justify-center': align === 'center',
|
|
76
|
+
'justify-end': align === 'right',
|
|
77
|
+
}"
|
|
81
78
|
style="transition-delay: 300ms"
|
|
82
79
|
>
|
|
83
80
|
<slot name="actions">
|
|
@@ -92,7 +89,7 @@
|
|
|
92
89
|
:class="[
|
|
93
90
|
btn.variant === 'outline' || (index > 0 && !btn.variant)
|
|
94
91
|
? 'bg-white/10 backdrop-blur-sm border-white/40 text-white hover:bg-white/20 hover:border-white/60'
|
|
95
|
-
: ''
|
|
92
|
+
: '',
|
|
96
93
|
]"
|
|
97
94
|
>
|
|
98
95
|
{{ btn.label }}
|
|
@@ -107,9 +104,15 @@
|
|
|
107
104
|
v-if="hasScrollIndicator"
|
|
108
105
|
class="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2"
|
|
109
106
|
>
|
|
110
|
-
<span class="text-xs text-white/50 uppercase tracking-widest"
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
<span class="text-xs text-white/50 uppercase tracking-widest"
|
|
108
|
+
>Scroll</span
|
|
109
|
+
>
|
|
110
|
+
<div
|
|
111
|
+
class="w-6 h-10 border-2 border-white/30 rounded-full flex justify-center pt-2"
|
|
112
|
+
>
|
|
113
|
+
<div
|
|
114
|
+
class="w-1.5 h-1.5 bg-white/60 rounded-full animate-[scroll-indicator_2s_ease-in-out_infinite]"
|
|
115
|
+
/>
|
|
113
116
|
</div>
|
|
114
117
|
</div>
|
|
115
118
|
</section>
|
|
@@ -121,52 +124,52 @@ const props = defineProps({
|
|
|
121
124
|
img: {
|
|
122
125
|
type: Object,
|
|
123
126
|
default: null,
|
|
124
|
-
validator: (v) => !v || typeof v.src ===
|
|
127
|
+
validator: (v) => !v || typeof v.src === "string",
|
|
125
128
|
},
|
|
126
129
|
/** Background video object { src: string, poster?: string } */
|
|
127
130
|
video: {
|
|
128
131
|
type: Object,
|
|
129
132
|
default: null,
|
|
130
|
-
validator: (v) => !v || typeof v.src ===
|
|
133
|
+
validator: (v) => !v || typeof v.src === "string",
|
|
131
134
|
},
|
|
132
135
|
/** Overlay style */
|
|
133
136
|
overlay: {
|
|
134
137
|
type: String,
|
|
135
|
-
default:
|
|
136
|
-
validator: (v) => [
|
|
138
|
+
default: "gradient",
|
|
139
|
+
validator: (v) => ["light", "heavy", "gradient", "none"].includes(v),
|
|
137
140
|
},
|
|
138
141
|
/** Small text above title */
|
|
139
142
|
eyebrow: {
|
|
140
143
|
type: String,
|
|
141
|
-
default:
|
|
144
|
+
default: "",
|
|
142
145
|
},
|
|
143
146
|
/** Main headline */
|
|
144
147
|
title: {
|
|
145
148
|
type: String,
|
|
146
|
-
|
|
149
|
+
default: "Your Headline Here",
|
|
147
150
|
},
|
|
148
151
|
/** Supporting text */
|
|
149
152
|
subtitle: {
|
|
150
153
|
type: String,
|
|
151
|
-
default:
|
|
154
|
+
default: "",
|
|
152
155
|
},
|
|
153
156
|
/** Action buttons array [{ label, color?, variant?, to?, icon? }] */
|
|
154
157
|
buttons: {
|
|
155
158
|
type: Array,
|
|
156
159
|
default: () => [],
|
|
157
|
-
validator: (v) => v.every(btn => btn.label),
|
|
160
|
+
validator: (v) => v.every((btn) => btn.label),
|
|
158
161
|
},
|
|
159
162
|
/** Content horizontal alignment */
|
|
160
163
|
align: {
|
|
161
164
|
type: String,
|
|
162
|
-
default:
|
|
163
|
-
validator: (v) => [
|
|
165
|
+
default: "left",
|
|
166
|
+
validator: (v) => ["left", "center", "right"].includes(v),
|
|
164
167
|
},
|
|
165
168
|
/** Content vertical position */
|
|
166
169
|
verticalAlign: {
|
|
167
170
|
type: String,
|
|
168
|
-
default:
|
|
169
|
-
validator: (v) => [
|
|
171
|
+
default: "center",
|
|
172
|
+
validator: (v) => ["center", "bottom"].includes(v),
|
|
170
173
|
},
|
|
171
174
|
/** Has scroll indicator arrow */
|
|
172
175
|
hasScrollIndicator: {
|
|
@@ -182,35 +185,35 @@ const props = defineProps({
|
|
|
182
185
|
|
|
183
186
|
const overlayClass = computed(() => {
|
|
184
187
|
const overlays = {
|
|
185
|
-
light:
|
|
186
|
-
heavy:
|
|
187
|
-
gradient:
|
|
188
|
+
light: "bg-black/30",
|
|
189
|
+
heavy: "bg-black/60",
|
|
190
|
+
gradient: "bg-gradient-to-t from-black/80 via-black/40 to-transparent",
|
|
188
191
|
};
|
|
189
|
-
return overlays[props.overlay] ||
|
|
192
|
+
return overlays[props.overlay] || "";
|
|
190
193
|
});
|
|
191
194
|
|
|
192
195
|
const alignmentClasses = computed(() => {
|
|
193
196
|
const aligns = {
|
|
194
|
-
left:
|
|
195
|
-
center:
|
|
196
|
-
right:
|
|
197
|
+
left: "text-left",
|
|
198
|
+
center: "text-center",
|
|
199
|
+
right: "text-right",
|
|
197
200
|
};
|
|
198
201
|
return aligns[props.align];
|
|
199
202
|
});
|
|
200
203
|
|
|
201
204
|
const verticalAlignClasses = computed(() => {
|
|
202
205
|
const aligns = {
|
|
203
|
-
center:
|
|
204
|
-
bottom:
|
|
206
|
+
center: "items-center",
|
|
207
|
+
bottom: "items-end pb-24",
|
|
205
208
|
};
|
|
206
209
|
return aligns[props.verticalAlign];
|
|
207
210
|
});
|
|
208
211
|
|
|
209
212
|
const contentAlignmentClasses = computed(() => {
|
|
210
213
|
const aligns = {
|
|
211
|
-
left:
|
|
212
|
-
center:
|
|
213
|
-
right:
|
|
214
|
+
left: "",
|
|
215
|
+
center: "mx-auto",
|
|
216
|
+
right: "ml-auto",
|
|
214
217
|
};
|
|
215
218
|
return aligns[props.align];
|
|
216
219
|
});
|
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<Transition name="slide-down">
|
|
3
|
-
<div
|
|
4
|
-
v-if="visible"
|
|
5
|
-
:class="[
|
|
6
|
-
'relative z-50',
|
|
7
|
-
bgClasses
|
|
8
|
-
]"
|
|
9
|
-
>
|
|
3
|
+
<div v-if="visible" :class="['relative z-50', bgClasses]">
|
|
10
4
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2.5">
|
|
11
5
|
<div class="flex items-center justify-center gap-x-4 text-sm">
|
|
12
6
|
<!-- Icon -->
|
|
13
|
-
<UIcon
|
|
14
|
-
v-if="icon"
|
|
15
|
-
:name="icon"
|
|
16
|
-
class="w-5 h-5 flex-shrink-0"
|
|
17
|
-
/>
|
|
7
|
+
<UIcon v-if="icon" :name="icon" class="w-5 h-5 flex-shrink-0" />
|
|
18
8
|
|
|
19
9
|
<!-- Message -->
|
|
20
10
|
<p class="text-center">
|
|
@@ -52,33 +42,34 @@ const props = defineProps({
|
|
|
52
42
|
/** Main message text */
|
|
53
43
|
message: {
|
|
54
44
|
type: String,
|
|
55
|
-
|
|
45
|
+
default: "🎉 New feature available — check it out!",
|
|
56
46
|
},
|
|
57
47
|
/** Optional badge/label before message */
|
|
58
48
|
badge: {
|
|
59
49
|
type: String,
|
|
60
|
-
default:
|
|
50
|
+
default: "",
|
|
61
51
|
},
|
|
62
52
|
/** Icon name to display */
|
|
63
53
|
icon: {
|
|
64
54
|
type: String,
|
|
65
|
-
default:
|
|
55
|
+
default: "",
|
|
66
56
|
},
|
|
67
57
|
/** CTA link URL */
|
|
68
58
|
link: {
|
|
69
59
|
type: String,
|
|
70
|
-
default:
|
|
60
|
+
default: "",
|
|
71
61
|
},
|
|
72
62
|
/** CTA link text */
|
|
73
63
|
linkText: {
|
|
74
64
|
type: String,
|
|
75
|
-
default:
|
|
65
|
+
default: "Learn more",
|
|
76
66
|
},
|
|
77
67
|
/** Color variant */
|
|
78
68
|
variant: {
|
|
79
69
|
type: String,
|
|
80
|
-
default:
|
|
81
|
-
validator: (v) =>
|
|
70
|
+
default: "primary",
|
|
71
|
+
validator: (v) =>
|
|
72
|
+
["primary", "neutral", "success", "warning", "error"].includes(v),
|
|
82
73
|
},
|
|
83
74
|
/** Allow dismissing the bar */
|
|
84
75
|
dismissible: {
|
|
@@ -88,11 +79,11 @@ const props = defineProps({
|
|
|
88
79
|
/** Storage key for dismissed state */
|
|
89
80
|
storageKey: {
|
|
90
81
|
type: String,
|
|
91
|
-
default:
|
|
82
|
+
default: "announcement-dismissed",
|
|
92
83
|
},
|
|
93
84
|
});
|
|
94
85
|
|
|
95
|
-
const emit = defineEmits([
|
|
86
|
+
const emit = defineEmits(["dismiss"]);
|
|
96
87
|
|
|
97
88
|
const visible = ref(true);
|
|
98
89
|
|
|
@@ -107,19 +98,20 @@ onMounted(() => {
|
|
|
107
98
|
|
|
108
99
|
const dismiss = () => {
|
|
109
100
|
if (props.storageKey) {
|
|
110
|
-
localStorage.setItem(props.storageKey,
|
|
101
|
+
localStorage.setItem(props.storageKey, "true");
|
|
111
102
|
}
|
|
112
103
|
visible.value = false;
|
|
113
|
-
emit(
|
|
104
|
+
emit("dismiss");
|
|
114
105
|
};
|
|
115
106
|
|
|
116
107
|
const bgClasses = computed(() => {
|
|
117
108
|
const variants = {
|
|
118
|
-
primary:
|
|
119
|
-
neutral:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
109
|
+
primary: "bg-primary-500 text-white",
|
|
110
|
+
neutral:
|
|
111
|
+
"bg-neutral-900 dark:bg-neutral-100 text-white dark:text-neutral-900",
|
|
112
|
+
success: "bg-green-500 text-white",
|
|
113
|
+
warning: "bg-yellow-500 text-neutral-900",
|
|
114
|
+
error: "bg-red-500 text-white",
|
|
123
115
|
};
|
|
124
116
|
return variants[props.variant];
|
|
125
117
|
});
|