@xenterprises/nuxt-x-marketing 1.0.1 → 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 +23 -0
- package/.playground/app.vue +12 -6
- package/.playground/package-lock.json +6 -0
- package/.playground/pages/index.vue +2 -2
- package/.playground/pages/layout/x-legal.vue +12 -12
- package/LICENSE +60 -0
- package/README.md +210 -64
- package/app/app.config.ts +2 -72
- 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 +177 -169
- 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/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 +146 -138
- 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/package.json +2 -1
- package/screenshots/playground-home.png +0 -0
|
@@ -1,151 +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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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>
|
|
52
52
|
</template>
|
|
53
53
|
|
|
54
54
|
<script setup>
|
|
55
55
|
const props = defineProps({
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
},
|
|
95
103
|
});
|
|
96
104
|
|
|
97
105
|
const placementClasses = computed(() => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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];
|
|
105
113
|
});
|
|
106
114
|
|
|
107
115
|
const iconConfig = {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
},
|
|
124
132
|
};
|
|
125
133
|
|
|
126
134
|
const iconName = computed(() => {
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
if (!props.showIcon) return null;
|
|
136
|
+
return iconConfig[props.type]?.name || null;
|
|
129
137
|
});
|
|
130
138
|
|
|
131
139
|
const iconClasses = computed(() => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
const baseClasses = "w-5 h-5 flex-shrink-0";
|
|
141
|
+
const colorClass = iconConfig[props.type]?.color || "text-gray-500";
|
|
142
|
+
return `${baseClasses} ${colorClass}`;
|
|
135
143
|
});
|
|
136
144
|
|
|
137
145
|
const enterFromClass = computed(() => {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
if (props.placement.includes("Left")) {
|
|
147
|
+
return "-translate-x-full opacity-0";
|
|
148
|
+
}
|
|
149
|
+
return "translate-x-full opacity-0";
|
|
142
150
|
});
|
|
143
151
|
|
|
144
152
|
const leaveToClass = computed(() => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
if (props.placement.includes("Left")) {
|
|
154
|
+
return "-translate-x-full opacity-0";
|
|
155
|
+
}
|
|
156
|
+
return "translate-x-full opacity-0";
|
|
149
157
|
});
|
|
150
158
|
|
|
151
159
|
const currentNotification = ref(null);
|
|
@@ -155,27 +163,27 @@ let hideTimeout = null;
|
|
|
155
163
|
let cycleTimeout = null;
|
|
156
164
|
|
|
157
165
|
const showNext = () => {
|
|
158
|
-
|
|
166
|
+
if (props.notifications.length === 0) return;
|
|
159
167
|
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
currentNotification.value = props.notifications[currentIndex.value];
|
|
169
|
+
currentIndex.value = (currentIndex.value + 1) % props.notifications.length;
|
|
162
170
|
|
|
163
|
-
|
|
164
|
-
|
|
171
|
+
hideTimeout = setTimeout(() => {
|
|
172
|
+
currentNotification.value = null;
|
|
165
173
|
|
|
166
|
-
|
|
167
|
-
|
|
174
|
+
cycleTimeout = setTimeout(showNext, props.interval - props.displayDuration);
|
|
175
|
+
}, props.displayDuration);
|
|
168
176
|
};
|
|
169
177
|
|
|
170
178
|
onMounted(() => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
if (import.meta.client && props.notifications.length > 0) {
|
|
180
|
+
showTimeout = setTimeout(showNext, props.initialDelay);
|
|
181
|
+
}
|
|
174
182
|
});
|
|
175
183
|
|
|
176
184
|
onUnmounted(() => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
if (showTimeout) clearTimeout(showTimeout);
|
|
186
|
+
if (hideTimeout) clearTimeout(hideTimeout);
|
|
187
|
+
if (cycleTimeout) clearTimeout(cycleTimeout);
|
|
180
188
|
});
|
|
181
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>
|