@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
|
@@ -1,106 +1,159 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
2
|
+
<Transition
|
|
3
|
+
enter-active-class="transition-all duration-500 ease-out"
|
|
4
|
+
:enter-from-class="enterFromClass"
|
|
5
|
+
leave-active-class="transition-all duration-300 ease-in"
|
|
6
|
+
:leave-to-class="leaveToClass"
|
|
7
|
+
>
|
|
8
|
+
<aside
|
|
9
|
+
v-if="currentNotification"
|
|
10
|
+
class="fixed z-40"
|
|
11
|
+
:class="placementClasses"
|
|
12
|
+
>
|
|
13
|
+
<div
|
|
14
|
+
class="bg-white dark:bg-gray-900 rounded-xl shadow-lg border border-gray-200 dark:border-gray-800 p-4 flex items-center gap-3"
|
|
15
|
+
>
|
|
16
|
+
<!-- Avatar -->
|
|
17
|
+
<img
|
|
18
|
+
v-if="currentNotification.avatar"
|
|
19
|
+
:src="currentNotification.avatar"
|
|
20
|
+
:alt="currentNotification.name"
|
|
21
|
+
class="w-10 h-10 rounded-full object-cover"
|
|
22
|
+
/>
|
|
23
|
+
<div
|
|
24
|
+
v-else
|
|
25
|
+
class="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center"
|
|
26
|
+
>
|
|
27
|
+
<span class="text-primary-600 dark:text-primary-400 font-medium">
|
|
28
|
+
{{ currentNotification.name.charAt(0) }}
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Content -->
|
|
33
|
+
<div>
|
|
34
|
+
<p class="text-sm text-gray-900 dark:text-white">
|
|
35
|
+
<span class="font-medium">{{ currentNotification.name }}</span>
|
|
36
|
+
{{ currentNotification.action }}
|
|
37
|
+
</p>
|
|
38
|
+
<p class="text-xs text-gray-500 mt-0.5">
|
|
39
|
+
{{ currentNotification.time }}
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Status icon -->
|
|
44
|
+
<UIcon
|
|
45
|
+
v-if="showIcon && iconName"
|
|
46
|
+
:name="iconName"
|
|
47
|
+
:class="iconClasses"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
</aside>
|
|
51
|
+
</Transition>
|
|
49
52
|
</template>
|
|
50
53
|
|
|
51
54
|
<script setup>
|
|
52
55
|
const props = defineProps({
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
56
|
+
/** List of notifications to cycle through [{ name, action, time, avatar? }] */
|
|
57
|
+
notifications: {
|
|
58
|
+
type: Array,
|
|
59
|
+
default: () => [
|
|
60
|
+
{ name: "Alex M.", action: "just signed up", time: "2 minutes ago" },
|
|
61
|
+
{ name: "Sarah K.", action: "upgraded to Pro", time: "5 minutes ago" },
|
|
62
|
+
{
|
|
63
|
+
name: "James T.",
|
|
64
|
+
action: "started a free trial",
|
|
65
|
+
time: "8 minutes ago",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
validator: (arr) => arr.every((n) => n.name && n.action && n.time),
|
|
69
|
+
},
|
|
70
|
+
/** Time between notifications (ms) */
|
|
71
|
+
interval: {
|
|
72
|
+
type: Number,
|
|
73
|
+
default: 4000,
|
|
74
|
+
},
|
|
75
|
+
/** Delay before first notification (ms) */
|
|
76
|
+
initialDelay: {
|
|
77
|
+
type: Number,
|
|
78
|
+
default: 5000,
|
|
79
|
+
},
|
|
80
|
+
/** How long each notification shows (ms) */
|
|
81
|
+
displayDuration: {
|
|
82
|
+
type: Number,
|
|
83
|
+
default: 3000,
|
|
84
|
+
},
|
|
85
|
+
/** Toast placement */
|
|
86
|
+
placement: {
|
|
87
|
+
type: String,
|
|
88
|
+
default: "topRight",
|
|
89
|
+
validator: (v) =>
|
|
90
|
+
["topLeft", "topRight", "bottomLeft", "bottomRight"].includes(v),
|
|
91
|
+
},
|
|
92
|
+
/** Toast type - determines icon and color */
|
|
93
|
+
type: {
|
|
94
|
+
type: String,
|
|
95
|
+
default: "success",
|
|
96
|
+
validator: (v) => ["success", "error", "warning", "info"].includes(v),
|
|
97
|
+
},
|
|
98
|
+
/** Whether to show the status icon */
|
|
99
|
+
showIcon: {
|
|
100
|
+
type: Boolean,
|
|
101
|
+
default: true,
|
|
102
|
+
},
|
|
80
103
|
});
|
|
81
104
|
|
|
82
105
|
const placementClasses = computed(() => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
const placements = {
|
|
107
|
+
topLeft: "top-6 left-6",
|
|
108
|
+
topRight: "top-6 right-6",
|
|
109
|
+
bottomLeft: "bottom-6 left-6",
|
|
110
|
+
bottomRight: "bottom-6 right-6",
|
|
111
|
+
};
|
|
112
|
+
return placements[props.placement];
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const iconConfig = {
|
|
116
|
+
success: {
|
|
117
|
+
name: "i-lucide-badge-check",
|
|
118
|
+
color: "text-green-500",
|
|
119
|
+
},
|
|
120
|
+
error: {
|
|
121
|
+
name: "i-lucide-x-circle",
|
|
122
|
+
color: "text-red-500",
|
|
123
|
+
},
|
|
124
|
+
warning: {
|
|
125
|
+
name: "i-lucide-alert-triangle",
|
|
126
|
+
color: "text-yellow-500",
|
|
127
|
+
},
|
|
128
|
+
info: {
|
|
129
|
+
name: "i-lucide-info",
|
|
130
|
+
color: "text-blue-500",
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const iconName = computed(() => {
|
|
135
|
+
if (!props.showIcon) return null;
|
|
136
|
+
return iconConfig[props.type]?.name || null;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const iconClasses = computed(() => {
|
|
140
|
+
const baseClasses = "w-5 h-5 flex-shrink-0";
|
|
141
|
+
const colorClass = iconConfig[props.type]?.color || "text-gray-500";
|
|
142
|
+
return `${baseClasses} ${colorClass}`;
|
|
90
143
|
});
|
|
91
144
|
|
|
92
145
|
const enterFromClass = computed(() => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
146
|
+
if (props.placement.includes("Left")) {
|
|
147
|
+
return "-translate-x-full opacity-0";
|
|
148
|
+
}
|
|
149
|
+
return "translate-x-full opacity-0";
|
|
97
150
|
});
|
|
98
151
|
|
|
99
152
|
const leaveToClass = computed(() => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
153
|
+
if (props.placement.includes("Left")) {
|
|
154
|
+
return "-translate-x-full opacity-0";
|
|
155
|
+
}
|
|
156
|
+
return "translate-x-full opacity-0";
|
|
104
157
|
});
|
|
105
158
|
|
|
106
159
|
const currentNotification = ref(null);
|
|
@@ -110,27 +163,27 @@ let hideTimeout = null;
|
|
|
110
163
|
let cycleTimeout = null;
|
|
111
164
|
|
|
112
165
|
const showNext = () => {
|
|
113
|
-
|
|
166
|
+
if (props.notifications.length === 0) return;
|
|
114
167
|
|
|
115
|
-
|
|
116
|
-
|
|
168
|
+
currentNotification.value = props.notifications[currentIndex.value];
|
|
169
|
+
currentIndex.value = (currentIndex.value + 1) % props.notifications.length;
|
|
117
170
|
|
|
118
|
-
|
|
119
|
-
|
|
171
|
+
hideTimeout = setTimeout(() => {
|
|
172
|
+
currentNotification.value = null;
|
|
120
173
|
|
|
121
|
-
|
|
122
|
-
|
|
174
|
+
cycleTimeout = setTimeout(showNext, props.interval - props.displayDuration);
|
|
175
|
+
}, props.displayDuration);
|
|
123
176
|
};
|
|
124
177
|
|
|
125
178
|
onMounted(() => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
179
|
+
if (import.meta.client && props.notifications.length > 0) {
|
|
180
|
+
showTimeout = setTimeout(showNext, props.initialDelay);
|
|
181
|
+
}
|
|
129
182
|
});
|
|
130
183
|
|
|
131
184
|
onUnmounted(() => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
185
|
+
if (showTimeout) clearTimeout(showTimeout);
|
|
186
|
+
if (hideTimeout) clearTimeout(hideTimeout);
|
|
187
|
+
if (cycleTimeout) clearTimeout(cycleTimeout);
|
|
135
188
|
});
|
|
136
189
|
</script>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<XMarkSectionNewsletter
|
|
4
|
+
v-if="
|
|
5
|
+
appConfig?.xMarketing?.config?.emailMarketingNewsletters?.organizationId
|
|
6
|
+
"
|
|
7
|
+
title="Subscribe to our newsletter"
|
|
8
|
+
description="Get the latest news and updates."
|
|
9
|
+
@submit="(email) => emit('newsletter-submit', email)"
|
|
10
|
+
/>
|
|
11
|
+
<section
|
|
12
|
+
class="relative pt-24 overflow-hidden"
|
|
13
|
+
:class="footer?.bg?.color ? footer?.bg?.color : 'bg-gray-900'"
|
|
14
|
+
>
|
|
15
|
+
<img
|
|
16
|
+
v-if="footer?.bg?.img?.src"
|
|
17
|
+
class="absolute left-1/2 bottom-0 h-full transform -translate-x-1/2"
|
|
18
|
+
:src="footer?.bg?.img?.src"
|
|
19
|
+
:alt="footer?.bg?.img?.alt"
|
|
20
|
+
/>
|
|
21
|
+
<div
|
|
22
|
+
class="relative z-10 container mx-auto max-w-screen-2xl px-4 md:px-8"
|
|
23
|
+
>
|
|
24
|
+
<div class="pb-24">
|
|
25
|
+
<div
|
|
26
|
+
class="flex flex-col lg:flex-row lg:justify-between gap-8 lg:gap-12"
|
|
27
|
+
>
|
|
28
|
+
<div class="shrink-0 lg:max-w-xl">
|
|
29
|
+
<a class="mb-8 inline-block" href="/">
|
|
30
|
+
<img
|
|
31
|
+
:src="footer?.logo?.src"
|
|
32
|
+
:alt="footer?.logo?.alt"
|
|
33
|
+
class="h-6"
|
|
34
|
+
/>
|
|
35
|
+
</a>
|
|
36
|
+
<p
|
|
37
|
+
class="mb-8 text-gray-400 font-medium leading-relaxed max-w-lg mx-auto lg:mx-0"
|
|
38
|
+
>
|
|
39
|
+
{{ footer?.body }}
|
|
40
|
+
</p>
|
|
41
|
+
<div class="flex flex-row flex-wrap gap-2 justify-start">
|
|
42
|
+
<div
|
|
43
|
+
v-for="(social, index) in footer?.socials"
|
|
44
|
+
:key="index"
|
|
45
|
+
class="w-auto"
|
|
46
|
+
>
|
|
47
|
+
<UButton
|
|
48
|
+
:to="social.url"
|
|
49
|
+
:icon="social?.icon"
|
|
50
|
+
:aria-label="social?.name"
|
|
51
|
+
color="neutral"
|
|
52
|
+
square
|
|
53
|
+
target="_blank"
|
|
54
|
+
variant="ghost"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
<UColorModeButton />
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div
|
|
61
|
+
class="flex flex-col sm:flex-row gap-8 lg:gap-12 lg:justify-end"
|
|
62
|
+
>
|
|
63
|
+
<div
|
|
64
|
+
v-for="(column, index) in footer?.columns"
|
|
65
|
+
:key="index"
|
|
66
|
+
class="text-left sm:text-right"
|
|
67
|
+
>
|
|
68
|
+
<h3
|
|
69
|
+
class="mb-6 text-sm text-gray-500 uppercase font-semibold leading-normal tracking-wide"
|
|
70
|
+
>
|
|
71
|
+
{{ column?.headerLabel }}
|
|
72
|
+
</h3>
|
|
73
|
+
<ul class="space-y-4">
|
|
74
|
+
<li v-for="link in column?.links" :key="link?.label">
|
|
75
|
+
<NuxtLink
|
|
76
|
+
:to="link?.to"
|
|
77
|
+
:target="link?.target ? link?.target : '_self'"
|
|
78
|
+
class="text-white hover:text-gray-300 font-medium leading-relaxed transition-colors"
|
|
79
|
+
>{{ link?.label }}</NuxtLink
|
|
80
|
+
>
|
|
81
|
+
</li>
|
|
82
|
+
</ul>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</section>
|
|
89
|
+
</div>
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<script setup>
|
|
93
|
+
/**
|
|
94
|
+
* XXFooter — X Enterprises branded footer
|
|
95
|
+
* Reads all configuration from appConfig.xMarketing.footer
|
|
96
|
+
* For a generic white-label footer, use XMarkLayoutFooter instead.
|
|
97
|
+
*/
|
|
98
|
+
const emit = defineEmits(["newsletter-submit"]);
|
|
99
|
+
const appConfig = useAppConfig();
|
|
100
|
+
const footer = appConfig?.xMarketing?.footer;
|
|
101
|
+
</script>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<UHeader>
|
|
3
|
+
<template #title>
|
|
4
|
+
<NuxtLink v-if="header?.logo?.src" to="/">
|
|
5
|
+
<UColorModeImage
|
|
6
|
+
:light="header?.logo?.src"
|
|
7
|
+
:dark="
|
|
8
|
+
header?.logo?.srcDark ? header?.logo?.srcDark : header?.logo?.src
|
|
9
|
+
"
|
|
10
|
+
:alt="header?.logo?.alt"
|
|
11
|
+
class="h-8"
|
|
12
|
+
/>
|
|
13
|
+
</NuxtLink>
|
|
14
|
+
<span v-else>{{ header?.logo?.alt }}</span>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<UNavigationMenu :items="links" />
|
|
18
|
+
|
|
19
|
+
<template #right>
|
|
20
|
+
<span v-for="(button, index) in header?.nav?.buttons" :key="index">
|
|
21
|
+
<UButton
|
|
22
|
+
class="hidden md:flex md:items-center md:justify-center py-2"
|
|
23
|
+
:to="button?.to"
|
|
24
|
+
:icon="button?.icon ? button?.icon : null"
|
|
25
|
+
:target="button?.target ? button?.target : '_self'"
|
|
26
|
+
:color="button?.color ? button?.color : 'primary'"
|
|
27
|
+
:variant="button?.variant ? button?.variant : 'solid'"
|
|
28
|
+
>{{ button?.label }}</UButton
|
|
29
|
+
>
|
|
30
|
+
</span>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<template #body>
|
|
34
|
+
<UNavigationMenu orientation="vertical" :items="links" class="-mx-2.5" />
|
|
35
|
+
<div class="mt-5 space-y-2">
|
|
36
|
+
<UButton
|
|
37
|
+
v-for="(button, index) in header?.nav?.buttons"
|
|
38
|
+
:key="index"
|
|
39
|
+
class="w-full justify-center"
|
|
40
|
+
:to="button?.to"
|
|
41
|
+
:icon="button?.icon ? button?.icon : null"
|
|
42
|
+
:target="button?.target ? button?.target : '_self'"
|
|
43
|
+
:color="button?.color ? button?.color : 'primary'"
|
|
44
|
+
:variant="button?.variant ? button?.variant : 'solid'"
|
|
45
|
+
>
|
|
46
|
+
{{ button?.label }}
|
|
47
|
+
</UButton>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
</UHeader>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script setup>
|
|
54
|
+
/**
|
|
55
|
+
* XXHeaderNav — X Enterprises branded navigation header
|
|
56
|
+
* Reads all configuration from appConfig.xMarketing.header
|
|
57
|
+
* For a generic white-label navbar, use XMarkNavbar instead.
|
|
58
|
+
*/
|
|
59
|
+
const appConfig = useAppConfig();
|
|
60
|
+
const header = appConfig?.xMarketing?.header;
|
|
61
|
+
const links = header?.nav?.links || [];
|
|
62
|
+
</script>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<UHeader
|
|
3
|
+
:ui="{
|
|
4
|
+
root: headerClasses,
|
|
5
|
+
}"
|
|
6
|
+
>
|
|
7
|
+
<template #left>
|
|
8
|
+
<UNavigationMenu :items="navLinks" />
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<template #center>
|
|
12
|
+
<NuxtLink to="/" class="flex items-center">
|
|
13
|
+
<slot name="logo">
|
|
14
|
+
<span class="text-xl font-bold">LOGO</span>
|
|
15
|
+
</slot>
|
|
16
|
+
</NuxtLink>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<template #right>
|
|
20
|
+
<UButton
|
|
21
|
+
v-if="secondaryCta"
|
|
22
|
+
:to="secondaryCta.to"
|
|
23
|
+
variant="ghost"
|
|
24
|
+
color="neutral"
|
|
25
|
+
:label="secondaryCta.label"
|
|
26
|
+
/>
|
|
27
|
+
<UButton
|
|
28
|
+
v-if="primaryCta"
|
|
29
|
+
:to="primaryCta.to"
|
|
30
|
+
variant="solid"
|
|
31
|
+
color="primary"
|
|
32
|
+
:label="primaryCta.label"
|
|
33
|
+
/>
|
|
34
|
+
</template>
|
|
35
|
+
</UHeader>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script setup>
|
|
39
|
+
/**
|
|
40
|
+
* XXHeader — X Enterprises branded app header (props-based variant)
|
|
41
|
+
* For the appConfig-driven version, use XXHeaderNav instead.
|
|
42
|
+
* For a generic white-label navbar, use XMarkNavbar instead.
|
|
43
|
+
*/
|
|
44
|
+
const props = defineProps({
|
|
45
|
+
/** Navigation links array [{ label, to, children? }] */
|
|
46
|
+
navLinks: {
|
|
47
|
+
type: Array,
|
|
48
|
+
default: () => [],
|
|
49
|
+
},
|
|
50
|
+
/** Primary CTA button { label, to } */
|
|
51
|
+
primaryCta: {
|
|
52
|
+
type: Object,
|
|
53
|
+
default: null,
|
|
54
|
+
},
|
|
55
|
+
/** Secondary CTA button { label, to } */
|
|
56
|
+
secondaryCta: {
|
|
57
|
+
type: Object,
|
|
58
|
+
default: null,
|
|
59
|
+
},
|
|
60
|
+
/** Transparent background (for use over hero sections) */
|
|
61
|
+
transparent: {
|
|
62
|
+
type: Boolean,
|
|
63
|
+
default: false,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const headerClasses = computed(() => {
|
|
68
|
+
return props.transparent
|
|
69
|
+
? "bg-transparent text-white"
|
|
70
|
+
: "bg-white dark:bg-deep-950";
|
|
71
|
+
});
|
|
72
|
+
</script>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<footer
|
|
3
|
+
class="bg-gray-900 text-gray-400 text-xs relative overflow-visible pt-2"
|
|
4
|
+
>
|
|
5
|
+
<a v-if="resolvedLogo.src" :href="resolvedLogo.href" target="_blank">
|
|
6
|
+
<img
|
|
7
|
+
:src="resolvedLogo.src"
|
|
8
|
+
:alt="resolvedLogo.alt"
|
|
9
|
+
class="h-6 w-6 mx-auto -mt-4 relative z-10"
|
|
10
|
+
/>
|
|
11
|
+
</a>
|
|
12
|
+
<main
|
|
13
|
+
class="flex flex-col md:flex-row justify-between items-center px-6 pt-3 pb-5 max-w-screen-2xl mx-auto gap-4"
|
|
14
|
+
>
|
|
15
|
+
<div class="text-center md:text-left">
|
|
16
|
+
{{ resolvedCopyright }}
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<nav
|
|
20
|
+
v-if="resolvedLinks.length"
|
|
21
|
+
class="text-center md:text-right flex flex-wrap justify-center gap-4"
|
|
22
|
+
>
|
|
23
|
+
<a
|
|
24
|
+
v-for="link in resolvedLinks"
|
|
25
|
+
:key="link.label"
|
|
26
|
+
:href="link.href"
|
|
27
|
+
class="hover:underline"
|
|
28
|
+
target="_blank"
|
|
29
|
+
>
|
|
30
|
+
{{ link.label }}
|
|
31
|
+
</a>
|
|
32
|
+
</nav>
|
|
33
|
+
</main>
|
|
34
|
+
</footer>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup>
|
|
38
|
+
/**
|
|
39
|
+
* XXLegal — X Enterprises branded legal footer bar
|
|
40
|
+
* Defaults to X Enterprises copyright, logo, and legal links.
|
|
41
|
+
* Pass props to override any defaults for white-label use.
|
|
42
|
+
*/
|
|
43
|
+
const props = defineProps({
|
|
44
|
+
/** Copyright text (defaults to X Enterprises copyright) */
|
|
45
|
+
copyright: {
|
|
46
|
+
type: String,
|
|
47
|
+
default: "",
|
|
48
|
+
},
|
|
49
|
+
/** Legal links array [{ label, href }] (defaults to X Enterprises legal pages) */
|
|
50
|
+
links: {
|
|
51
|
+
type: Array,
|
|
52
|
+
default: null,
|
|
53
|
+
},
|
|
54
|
+
/** Logo object { src, alt, href } (defaults to X Enterprises logo). Pass false to hide. */
|
|
55
|
+
logo: {
|
|
56
|
+
type: [Object, Boolean],
|
|
57
|
+
default: null,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Default X Enterprises values
|
|
62
|
+
const defaultLogo = {
|
|
63
|
+
src: "https://cdn.x.enterprises/logo-x.png",
|
|
64
|
+
alt: "X Enterprises",
|
|
65
|
+
href: "https://x.enterprises",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const defaultLinks = [
|
|
69
|
+
{ label: "Terms of Service", href: "https://x.enterprises/legal/terms" },
|
|
70
|
+
{
|
|
71
|
+
label: "Privacy Policy",
|
|
72
|
+
href: "https://x.enterprises/legal/privacy-policy",
|
|
73
|
+
},
|
|
74
|
+
{ label: "Cookie Policy", href: "https://x.enterprises/legal/cookie-policy" },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const defaultCopyright = `Copyright ©2016-${new Date().getFullYear()} X Enterprises, LLC. All Rights Reserved.`;
|
|
78
|
+
|
|
79
|
+
// Resolved values with defaults
|
|
80
|
+
const resolvedLogo = computed(() => {
|
|
81
|
+
if (props.logo === null) return defaultLogo;
|
|
82
|
+
if (props.logo === false) return { src: "", alt: "", href: "" };
|
|
83
|
+
return { ...defaultLogo, ...props.logo };
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const resolvedLinks = computed(() => {
|
|
87
|
+
if (props.links === null) return defaultLinks;
|
|
88
|
+
return props.links;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const resolvedCopyright = computed(() => {
|
|
92
|
+
return props.copyright || defaultCopyright;
|
|
93
|
+
});
|
|
94
|
+
</script>
|