@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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
:class="[
|
|
4
4
|
'relative overflow-hidden rounded-2xl',
|
|
5
5
|
paddingClasses,
|
|
6
|
-
bgColorClasses
|
|
6
|
+
bgColorClasses,
|
|
7
7
|
]"
|
|
8
8
|
>
|
|
9
9
|
<!-- Background Pattern -->
|
|
@@ -15,10 +15,7 @@
|
|
|
15
15
|
/>
|
|
16
16
|
|
|
17
17
|
<!-- Background Image -->
|
|
18
|
-
<div
|
|
19
|
-
v-if="resolvedBg.image"
|
|
20
|
-
class="absolute inset-0"
|
|
21
|
-
>
|
|
18
|
+
<div v-if="resolvedBg.image" class="absolute inset-0">
|
|
22
19
|
<img
|
|
23
20
|
:src="resolvedBg.image"
|
|
24
21
|
:alt="resolvedBg.imageAlt"
|
|
@@ -80,22 +77,22 @@ const props = defineProps({
|
|
|
80
77
|
/** Eyebrow text above title */
|
|
81
78
|
eyebrow: {
|
|
82
79
|
type: String,
|
|
83
|
-
default:
|
|
80
|
+
default: "",
|
|
84
81
|
},
|
|
85
82
|
/** CTA title */
|
|
86
83
|
title: {
|
|
87
84
|
type: String,
|
|
88
|
-
|
|
85
|
+
default: "Ready to get started?",
|
|
89
86
|
},
|
|
90
87
|
/** CTA description */
|
|
91
88
|
description: {
|
|
92
89
|
type: String,
|
|
93
|
-
default:
|
|
90
|
+
default: "",
|
|
94
91
|
},
|
|
95
92
|
/** Optional icon */
|
|
96
93
|
icon: {
|
|
97
94
|
type: String,
|
|
98
|
-
default:
|
|
95
|
+
default: "",
|
|
99
96
|
},
|
|
100
97
|
/** Action buttons */
|
|
101
98
|
buttons: {
|
|
@@ -106,8 +103,8 @@ const props = defineProps({
|
|
|
106
103
|
/** Layout variant */
|
|
107
104
|
variant: {
|
|
108
105
|
type: String,
|
|
109
|
-
default:
|
|
110
|
-
validator: (v) => [
|
|
106
|
+
default: "default",
|
|
107
|
+
validator: (v) => ["default", "centered", "inline", "stacked"].includes(v),
|
|
111
108
|
},
|
|
112
109
|
/**
|
|
113
110
|
* Background configuration object or string shorthand
|
|
@@ -122,31 +119,31 @@ const props = defineProps({
|
|
|
122
119
|
*/
|
|
123
120
|
bg: {
|
|
124
121
|
type: [Object, String],
|
|
125
|
-
default:
|
|
122
|
+
default: "subtle",
|
|
126
123
|
},
|
|
127
124
|
/** Size */
|
|
128
125
|
size: {
|
|
129
126
|
type: String,
|
|
130
|
-
default:
|
|
131
|
-
validator: (v) => [
|
|
127
|
+
default: "md",
|
|
128
|
+
validator: (v) => ["sm", "md", "lg"].includes(v),
|
|
132
129
|
},
|
|
133
130
|
});
|
|
134
131
|
|
|
135
132
|
// Default bg values
|
|
136
133
|
const bgDefaults = {
|
|
137
|
-
variant:
|
|
138
|
-
color:
|
|
139
|
-
image:
|
|
140
|
-
imageAlt:
|
|
141
|
-
imageOverlay:
|
|
142
|
-
imagePosition:
|
|
143
|
-
pattern:
|
|
134
|
+
variant: "subtle",
|
|
135
|
+
color: "",
|
|
136
|
+
image: "",
|
|
137
|
+
imageAlt: "",
|
|
138
|
+
imageOverlay: "",
|
|
139
|
+
imagePosition: "object-center",
|
|
140
|
+
pattern: "",
|
|
144
141
|
patternOpacity: 0.05,
|
|
145
142
|
};
|
|
146
143
|
|
|
147
144
|
// Resolve bg prop (supports string shorthand for variant or full object)
|
|
148
145
|
const resolvedBg = computed(() => {
|
|
149
|
-
if (typeof props.bg ===
|
|
146
|
+
if (typeof props.bg === "string") {
|
|
150
147
|
return { ...bgDefaults, variant: props.bg };
|
|
151
148
|
}
|
|
152
149
|
return { ...bgDefaults, ...props.bg };
|
|
@@ -154,14 +151,17 @@ const resolvedBg = computed(() => {
|
|
|
154
151
|
|
|
155
152
|
// Check if background is "bold" (dark background requiring light text)
|
|
156
153
|
const isBoldBg = computed(() => {
|
|
157
|
-
return
|
|
154
|
+
return (
|
|
155
|
+
["bold", "gradient"].includes(resolvedBg.value.variant) ||
|
|
156
|
+
resolvedBg.value.image
|
|
157
|
+
);
|
|
158
158
|
});
|
|
159
159
|
|
|
160
160
|
const paddingClasses = computed(() => {
|
|
161
161
|
const sizes = {
|
|
162
|
-
sm:
|
|
163
|
-
md:
|
|
164
|
-
lg:
|
|
162
|
+
sm: "p-6",
|
|
163
|
+
md: "p-8 md:p-12",
|
|
164
|
+
lg: "p-12 md:p-16",
|
|
165
165
|
};
|
|
166
166
|
return sizes[props.size];
|
|
167
167
|
});
|
|
@@ -171,124 +171,128 @@ const bgColorClasses = computed(() => {
|
|
|
171
171
|
if (resolvedBg.value.color) return resolvedBg.value.color;
|
|
172
172
|
|
|
173
173
|
const variants = {
|
|
174
|
-
subtle:
|
|
175
|
-
bold:
|
|
176
|
-
gradient:
|
|
177
|
-
none:
|
|
174
|
+
subtle: "bg-neutral-100 dark:bg-neutral-900",
|
|
175
|
+
bold: "bg-primary-600 dark:bg-primary-700",
|
|
176
|
+
gradient: "bg-gradient-to-br from-primary-600 to-primary-800",
|
|
177
|
+
none: "",
|
|
178
178
|
};
|
|
179
179
|
return variants[resolvedBg.value.variant] || variants.subtle;
|
|
180
180
|
});
|
|
181
181
|
|
|
182
182
|
const bgImageOverlayClass = computed(() => {
|
|
183
183
|
const overlays = {
|
|
184
|
-
light:
|
|
185
|
-
heavy:
|
|
186
|
-
gradient:
|
|
184
|
+
light: "bg-black/30",
|
|
185
|
+
heavy: "bg-black/60",
|
|
186
|
+
gradient: "bg-gradient-to-t from-black/80 via-black/40 to-transparent",
|
|
187
187
|
};
|
|
188
|
-
return overlays[resolvedBg.value.imageOverlay] ||
|
|
188
|
+
return overlays[resolvedBg.value.imageOverlay] || "";
|
|
189
189
|
});
|
|
190
190
|
|
|
191
191
|
const containerClasses = computed(() => {
|
|
192
192
|
const layouts = {
|
|
193
|
-
default:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
default:
|
|
194
|
+
"flex flex-col md:flex-row md:items-center md:justify-between gap-6",
|
|
195
|
+
centered: "text-center max-w-2xl mx-auto",
|
|
196
|
+
inline:
|
|
197
|
+
"flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4",
|
|
198
|
+
stacked: "flex flex-col items-center text-center gap-6",
|
|
197
199
|
};
|
|
198
200
|
return layouts[props.variant];
|
|
199
201
|
});
|
|
200
202
|
|
|
201
203
|
const iconContainerClasses = computed(() => {
|
|
202
204
|
return [
|
|
203
|
-
|
|
204
|
-
props.size ===
|
|
205
|
-
isBoldBg.value ?
|
|
205
|
+
"flex items-center justify-center rounded-xl",
|
|
206
|
+
props.size === "lg" ? "w-16 h-16" : "w-12 h-12",
|
|
207
|
+
isBoldBg.value ? "bg-white/20" : "bg-primary-100 dark:bg-primary-900/50",
|
|
206
208
|
];
|
|
207
209
|
});
|
|
208
210
|
|
|
209
211
|
const iconClasses = computed(() => {
|
|
210
212
|
return [
|
|
211
|
-
props.size ===
|
|
212
|
-
isBoldBg.value ?
|
|
213
|
+
props.size === "lg" ? "w-8 h-8" : "w-6 h-6",
|
|
214
|
+
isBoldBg.value ? "text-white" : "text-primary-500",
|
|
213
215
|
];
|
|
214
216
|
});
|
|
215
217
|
|
|
216
218
|
const contentClasses = computed(() => {
|
|
217
|
-
if (props.variant ===
|
|
218
|
-
return
|
|
219
|
+
if (props.variant === "default") {
|
|
220
|
+
return "flex-1";
|
|
219
221
|
}
|
|
220
|
-
return
|
|
222
|
+
return "";
|
|
221
223
|
});
|
|
222
224
|
|
|
223
225
|
const eyebrowClasses = computed(() => {
|
|
224
226
|
return [
|
|
225
|
-
|
|
226
|
-
isBoldBg.value ?
|
|
227
|
+
"xText-eyebrow mb-2",
|
|
228
|
+
isBoldBg.value ? "text-white/70" : "text-primary-600 dark:text-primary-400",
|
|
227
229
|
];
|
|
228
230
|
});
|
|
229
231
|
|
|
230
232
|
const titleClasses = computed(() => {
|
|
231
233
|
const sizeClasses = {
|
|
232
|
-
sm:
|
|
233
|
-
md:
|
|
234
|
-
lg:
|
|
234
|
+
sm: "text-lg font-semibold",
|
|
235
|
+
md: "text-xl font-semibold",
|
|
236
|
+
lg: "xText-headline",
|
|
235
237
|
};
|
|
236
238
|
return [
|
|
237
239
|
sizeClasses[props.size],
|
|
238
|
-
isBoldBg.value ?
|
|
240
|
+
isBoldBg.value ? "text-white" : "text-neutral-900 dark:text-white",
|
|
239
241
|
];
|
|
240
242
|
});
|
|
241
243
|
|
|
242
244
|
const descriptionClasses = computed(() => {
|
|
243
245
|
return [
|
|
244
|
-
|
|
245
|
-
props.size ===
|
|
246
|
-
isBoldBg.value ?
|
|
246
|
+
"mt-2",
|
|
247
|
+
props.size === "sm" ? "text-sm" : "text-base",
|
|
248
|
+
isBoldBg.value ? "text-white/80" : "text-neutral-600 dark:text-neutral-400",
|
|
247
249
|
];
|
|
248
250
|
});
|
|
249
251
|
|
|
250
252
|
const buttonsClasses = computed(() => {
|
|
251
253
|
const layouts = {
|
|
252
|
-
default:
|
|
253
|
-
centered:
|
|
254
|
-
inline:
|
|
255
|
-
stacked:
|
|
254
|
+
default: "flex flex-wrap gap-3",
|
|
255
|
+
centered: "flex flex-wrap justify-center gap-3 mt-6",
|
|
256
|
+
inline: "flex flex-wrap gap-3",
|
|
257
|
+
stacked: "flex flex-wrap justify-center gap-3",
|
|
256
258
|
};
|
|
257
259
|
return layouts[props.variant];
|
|
258
260
|
});
|
|
259
261
|
|
|
260
262
|
const getButtonColor = (button, index) => {
|
|
261
263
|
// Allow explicit color override
|
|
262
|
-
if (button.color && button.color !==
|
|
264
|
+
if (button.color && button.color !== "white") return button.color;
|
|
263
265
|
if (index === 0) {
|
|
264
266
|
// Use neutral instead of white (not a valid Nuxt UI v4 color)
|
|
265
267
|
// Custom classes handle the actual styling
|
|
266
|
-
return isBoldBg.value ?
|
|
268
|
+
return isBoldBg.value ? "neutral" : "primary";
|
|
267
269
|
}
|
|
268
|
-
return
|
|
270
|
+
return "neutral";
|
|
269
271
|
};
|
|
270
272
|
|
|
271
273
|
const getButtonClass = (button, index) => {
|
|
272
|
-
const isOutline =
|
|
273
|
-
|
|
274
|
-
const
|
|
274
|
+
const isOutline =
|
|
275
|
+
button.variant === "outline" || (index > 0 && !button.variant);
|
|
276
|
+
const isGhost = button.variant === "ghost";
|
|
277
|
+
const isSolid =
|
|
278
|
+
button.variant === "solid" || (index === 0 && !button.variant);
|
|
275
279
|
|
|
276
280
|
// Solid buttons on bold/gradient backgrounds - white bg with dark text
|
|
277
281
|
if (isBoldBg.value && isSolid) {
|
|
278
|
-
return
|
|
282
|
+
return "bg-white text-neutral-900 hover:bg-white/90";
|
|
279
283
|
}
|
|
280
284
|
// Outline buttons on bold/gradient backgrounds - transparent with white border
|
|
281
285
|
if (isBoldBg.value && isOutline) {
|
|
282
|
-
return
|
|
286
|
+
return "bg-white/10 backdrop-blur-sm border-white/40 hover:bg-white/20 hover:border-white/60 text-white";
|
|
283
287
|
}
|
|
284
288
|
// Ghost buttons on bold/gradient backgrounds - transparent with white text
|
|
285
289
|
if (isBoldBg.value && isGhost) {
|
|
286
|
-
return
|
|
290
|
+
return "text-white hover:bg-white/10";
|
|
287
291
|
}
|
|
288
292
|
// Outline buttons on subtle backgrounds
|
|
289
293
|
if (!isBoldBg.value && isOutline) {
|
|
290
|
-
return
|
|
294
|
+
return "border-neutral-400 dark:border-neutral-600 hover:border-neutral-500 dark:hover:border-neutral-500";
|
|
291
295
|
}
|
|
292
|
-
return
|
|
296
|
+
return "";
|
|
293
297
|
};
|
|
294
298
|
</script>
|
|
@@ -1,164 +1,174 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
<component
|
|
3
|
+
:is="to ? 'NuxtLink' : 'article'"
|
|
4
|
+
:to="to"
|
|
5
|
+
:class="[
|
|
6
|
+
'group relative aspect-[9/16] rounded-2xl overflow-hidden cursor-pointer block',
|
|
7
|
+
{ 'xHover-lift': hoverable },
|
|
8
|
+
]"
|
|
9
|
+
@click="!to && $emit('click')"
|
|
10
|
+
>
|
|
11
|
+
<!-- Background Image -->
|
|
12
|
+
<img
|
|
13
|
+
:src="img.src"
|
|
14
|
+
:alt="img.alt || ''"
|
|
15
|
+
class="absolute inset-0 w-full h-full object-cover transition-transform duration-700 ease-out group-hover:scale-[1.08]"
|
|
16
|
+
/>
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class="absolute inset-0"
|
|
21
|
-
:class="overlayClasses"
|
|
22
|
-
/>
|
|
18
|
+
<!-- Overlay -->
|
|
19
|
+
<div class="absolute inset-0" :class="overlayClasses" />
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
v-if="badge"
|
|
34
|
-
class="inline-flex self-start px-3 py-1 rounded-full text-xs font-medium bg-white/20 text-white backdrop-blur-sm"
|
|
35
|
-
>
|
|
36
|
-
{{ badge }}
|
|
37
|
-
</span>
|
|
21
|
+
<!-- Content -->
|
|
22
|
+
<div :class="['absolute inset-0 p-6 flex flex-col', alignmentClasses]">
|
|
23
|
+
<!-- Badge -->
|
|
24
|
+
<span
|
|
25
|
+
v-if="badge"
|
|
26
|
+
class="inline-flex self-start px-3 py-1 rounded-full text-xs font-medium bg-white/20 text-white backdrop-blur-sm"
|
|
27
|
+
>
|
|
28
|
+
{{ badge }}
|
|
29
|
+
</span>
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
<!-- Spacer -->
|
|
32
|
+
<div class="flex-1" />
|
|
41
33
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
<!-- Text Content -->
|
|
35
|
+
<div class="space-y-3">
|
|
36
|
+
<h3 :class="['font-semibold text-white', titleSizeClass]">
|
|
37
|
+
{{ title }}
|
|
38
|
+
</h3>
|
|
47
39
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
<div v-if="$slots.default" class="text-white/80 text-sm">
|
|
41
|
+
<slot />
|
|
42
|
+
</div>
|
|
51
43
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
44
|
+
<div
|
|
45
|
+
v-if="buttonLabel && to"
|
|
46
|
+
class="mt-4 inline-flex items-center justify-center px-4 py-2 rounded-md text-sm font-medium bg-white text-neutral-900 transition-all duration-300 group-hover:scale-110 group-hover:shadow-lg group-hover:shadow-white/50"
|
|
47
|
+
>
|
|
48
|
+
{{ buttonLabel }}
|
|
49
|
+
</div>
|
|
58
50
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
51
|
+
<button
|
|
52
|
+
v-if="buttonLabel && !to"
|
|
53
|
+
class="mt-4 inline-flex items-center justify-center px-4 py-2 rounded-md text-sm font-medium bg-white text-neutral-900 transition-all duration-300 group-hover:scale-110 group-hover:shadow-lg group-hover:shadow-white/50"
|
|
54
|
+
>
|
|
55
|
+
{{ buttonLabel }}
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</component>
|
|
68
60
|
</template>
|
|
69
61
|
|
|
70
62
|
<script setup>
|
|
71
|
-
defineEmits([
|
|
63
|
+
defineEmits(["click"]);
|
|
72
64
|
|
|
73
65
|
const props = defineProps({
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
66
|
+
/** Navigation URL (uses NuxtLink) */
|
|
67
|
+
to: {
|
|
68
|
+
type: String,
|
|
69
|
+
default: "",
|
|
70
|
+
},
|
|
71
|
+
/** Image object { src: string, alt?: string } */
|
|
72
|
+
img: {
|
|
73
|
+
type: Object,
|
|
74
|
+
default: () => ({
|
|
75
|
+
src: "",
|
|
76
|
+
alt: "Promo image",
|
|
77
|
+
}),
|
|
78
|
+
validator: (v) => !v || typeof v.src === "string",
|
|
79
|
+
},
|
|
80
|
+
/** Card title */
|
|
81
|
+
title: {
|
|
82
|
+
type: String,
|
|
83
|
+
default: "Promo Card Title",
|
|
84
|
+
},
|
|
85
|
+
/** Optional badge text */
|
|
86
|
+
badge: {
|
|
87
|
+
type: String,
|
|
88
|
+
default: "",
|
|
89
|
+
},
|
|
90
|
+
/** Button object { label: string, color?: string, variant?: string } */
|
|
91
|
+
button: {
|
|
92
|
+
type: Object,
|
|
93
|
+
default: null,
|
|
94
|
+
validator: (v) => !v || v.label !== undefined,
|
|
95
|
+
},
|
|
96
|
+
/** Content alignment */
|
|
97
|
+
align: {
|
|
98
|
+
type: String,
|
|
99
|
+
default: "bottomLeft",
|
|
100
|
+
validator: (v) =>
|
|
101
|
+
["bottomLeft", "bottomRight", "bottomCenter", "topLeft"].includes(v),
|
|
102
|
+
},
|
|
103
|
+
/** Overlay style */
|
|
104
|
+
overlay: {
|
|
105
|
+
type: String,
|
|
106
|
+
default: "gradient",
|
|
107
|
+
validator: (v) => ["light", "heavy", "gradient"].includes(v),
|
|
108
|
+
},
|
|
109
|
+
/** Enable lift on hover */
|
|
110
|
+
hoverable: {
|
|
111
|
+
type: Boolean,
|
|
112
|
+
default: true,
|
|
113
|
+
},
|
|
114
|
+
/** Title text size (Tailwind sizes) */
|
|
115
|
+
titleSize: {
|
|
116
|
+
type: String,
|
|
117
|
+
default: "xl",
|
|
118
|
+
validator: (v) =>
|
|
119
|
+
[
|
|
120
|
+
"xs",
|
|
121
|
+
"sm",
|
|
122
|
+
"base",
|
|
123
|
+
"lg",
|
|
124
|
+
"xl",
|
|
125
|
+
"2xl",
|
|
126
|
+
"3xl",
|
|
127
|
+
"4xl",
|
|
128
|
+
"5xl",
|
|
129
|
+
"6xl",
|
|
130
|
+
"7xl",
|
|
131
|
+
"8xl",
|
|
132
|
+
].includes(v),
|
|
133
|
+
},
|
|
124
134
|
});
|
|
125
135
|
|
|
126
|
-
const buttonLabel = computed(() => props.button?.label ||
|
|
136
|
+
const buttonLabel = computed(() => props.button?.label || "");
|
|
127
137
|
|
|
128
138
|
const overlayClasses = computed(() => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
const overlays = {
|
|
140
|
+
light: "bg-black/30",
|
|
141
|
+
heavy: "bg-black/60",
|
|
142
|
+
gradient: "bg-gradient-to-t from-black/80 via-black/30 to-transparent",
|
|
143
|
+
};
|
|
144
|
+
return overlays[props.overlay];
|
|
135
145
|
});
|
|
136
146
|
|
|
137
147
|
const alignmentClasses = computed(() => {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
const alignments = {
|
|
149
|
+
bottomLeft: "justify-end items-start text-left",
|
|
150
|
+
bottomRight: "justify-end items-end text-right",
|
|
151
|
+
bottomCenter: "justify-end items-center text-center",
|
|
152
|
+
topLeft: "justify-start items-start text-left",
|
|
153
|
+
};
|
|
154
|
+
return alignments[props.align];
|
|
145
155
|
});
|
|
146
156
|
|
|
147
157
|
const titleSizeClass = computed(() => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
const sizes = {
|
|
159
|
+
xs: "text-xs",
|
|
160
|
+
sm: "text-sm",
|
|
161
|
+
base: "text-base",
|
|
162
|
+
lg: "text-lg",
|
|
163
|
+
xl: "text-xl",
|
|
164
|
+
"2xl": "text-2xl",
|
|
165
|
+
"3xl": "text-3xl",
|
|
166
|
+
"4xl": "text-4xl",
|
|
167
|
+
"5xl": "text-5xl",
|
|
168
|
+
"6xl": "text-6xl",
|
|
169
|
+
"7xl": "text-7xl",
|
|
170
|
+
"8xl": "text-8xl",
|
|
171
|
+
};
|
|
172
|
+
return sizes[props.titleSize] || "text-xl";
|
|
163
173
|
});
|
|
164
174
|
</script>
|