@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.
Files changed (45) hide show
  1. package/.claude/settings.local.json +23 -0
  2. package/.playground/app.vue +12 -6
  3. package/.playground/package-lock.json +6 -0
  4. package/.playground/pages/index.vue +2 -2
  5. package/.playground/pages/layout/x-legal.vue +12 -12
  6. package/LICENSE +60 -0
  7. package/README.md +210 -64
  8. package/app/app.config.ts +2 -72
  9. package/app/components/X/Footer/index.vue +3 -4
  10. package/app/components/X/Mark/Affiliate/ComparisonTable.vue +382 -0
  11. package/app/components/X/Mark/Affiliate/Disclosure.vue +138 -0
  12. package/app/components/X/Mark/Affiliate/ProductCard.vue +431 -0
  13. package/app/components/X/Mark/Affiliate/ProductDetail.vue +410 -0
  14. package/app/components/X/Mark/Affiliate/ProductGrid.vue +101 -0
  15. package/app/components/X/Mark/Blog/Author.vue +26 -20
  16. package/app/components/X/Mark/Blog/Card.vue +28 -15
  17. package/app/components/X/Mark/Blog/Detail.vue +177 -169
  18. package/app/components/X/Mark/Blog/List.vue +17 -16
  19. package/app/components/X/Mark/CTA/index.vue +74 -70
  20. package/app/components/X/Mark/Card/Promo.vue +149 -139
  21. package/app/components/X/Mark/Directory/CategoryHero.vue +124 -0
  22. package/app/components/X/Mark/Directory/ListingCard.vue +336 -0
  23. package/app/components/X/Mark/Directory/ListingDetail.vue +306 -0
  24. package/app/components/X/Mark/Directory/ListingGrid.vue +190 -0
  25. package/app/components/X/Mark/Directory/SubmitCTA.vue +201 -0
  26. package/app/components/X/Mark/FAQ/index.vue +191 -0
  27. package/app/components/X/Mark/Features/index.vue +87 -35
  28. package/app/components/X/Mark/Hero/index.vue +41 -38
  29. package/app/components/X/Mark/Layout/AnnouncementBar.vue +20 -28
  30. package/app/components/X/Mark/Modal/Chat.vue +124 -124
  31. package/app/components/X/Mark/Modal/Video.vue +46 -36
  32. package/app/components/X/Mark/Pricing/Comparison.vue +76 -21
  33. package/app/components/X/Mark/Pricing/Plans.vue +75 -25
  34. package/app/components/X/Mark/SocialProof/Testimonials.vue +89 -21
  35. package/app/components/X/Mark/SocialProof/Toast.vue +146 -138
  36. package/app/components/X/X/Footer/index.vue +101 -0
  37. package/app/components/X/X/Header/Nav/index.vue +62 -0
  38. package/app/components/X/X/Header/index.vue +72 -0
  39. package/app/components/X/X/Legal/index.vue +94 -0
  40. package/app/composables/useParallax.ts +32 -19
  41. package/app/composables/useScrollReveal.ts +20 -13
  42. package/app/pages/index.vue +230 -28
  43. package/componentList.json +1856 -238
  44. package/package.json +2 -1
  45. package/screenshots/playground-home.png +0 -0
@@ -1,151 +1,159 @@
1
1
  <template>
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>
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
- /** List of notifications to cycle through */
57
- notifications: {
58
- type: Array,
59
- required: true,
60
- validator: (arr) => arr.every((n) => n.name && n.action && n.time),
61
- },
62
- /** Time between notifications (ms) */
63
- interval: {
64
- type: Number,
65
- default: 4000,
66
- },
67
- /** Delay before first notification (ms) */
68
- initialDelay: {
69
- type: Number,
70
- default: 5000,
71
- },
72
- /** How long each notification shows (ms) */
73
- displayDuration: {
74
- type: Number,
75
- default: 3000,
76
- },
77
- /** Toast placement */
78
- placement: {
79
- type: String,
80
- default: "topRight",
81
- validator: (v) =>
82
- ["topLeft", "topRight", "bottomLeft", "bottomRight"].includes(v),
83
- },
84
- /** Toast type - determines icon and color */
85
- type: {
86
- type: String,
87
- default: "success",
88
- validator: (v) => ["success", "error", "warning", "info"].includes(v),
89
- },
90
- /** Whether to show the status icon */
91
- showIcon: {
92
- type: Boolean,
93
- default: true,
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
- const placements = {
99
- topLeft: "top-6 left-6",
100
- topRight: "top-6 right-6",
101
- bottomLeft: "bottom-6 left-6",
102
- bottomRight: "bottom-6 right-6",
103
- };
104
- return placements[props.placement];
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
- success: {
109
- name: "i-lucide-badge-check",
110
- color: "text-green-500",
111
- },
112
- error: {
113
- name: "i-lucide-x-circle",
114
- color: "text-red-500",
115
- },
116
- warning: {
117
- name: "i-lucide-alert-triangle",
118
- color: "text-yellow-500",
119
- },
120
- info: {
121
- name: "i-lucide-info",
122
- color: "text-blue-500",
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
- if (!props.showIcon) return null;
128
- return iconConfig[props.type]?.name || null;
135
+ if (!props.showIcon) return null;
136
+ return iconConfig[props.type]?.name || null;
129
137
  });
130
138
 
131
139
  const iconClasses = computed(() => {
132
- const baseClasses = "w-5 h-5 flex-shrink-0";
133
- const colorClass = iconConfig[props.type]?.color || "text-gray-500";
134
- return `${baseClasses} ${colorClass}`;
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
- if (props.placement.includes("Left")) {
139
- return "-translate-x-full opacity-0";
140
- }
141
- return "translate-x-full opacity-0";
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
- if (props.placement.includes("Left")) {
146
- return "-translate-x-full opacity-0";
147
- }
148
- return "translate-x-full opacity-0";
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
- if (props.notifications.length === 0) return;
166
+ if (props.notifications.length === 0) return;
159
167
 
160
- currentNotification.value = props.notifications[currentIndex.value];
161
- currentIndex.value = (currentIndex.value + 1) % props.notifications.length;
168
+ currentNotification.value = props.notifications[currentIndex.value];
169
+ currentIndex.value = (currentIndex.value + 1) % props.notifications.length;
162
170
 
163
- hideTimeout = setTimeout(() => {
164
- currentNotification.value = null;
171
+ hideTimeout = setTimeout(() => {
172
+ currentNotification.value = null;
165
173
 
166
- cycleTimeout = setTimeout(showNext, props.interval - props.displayDuration);
167
- }, props.displayDuration);
174
+ cycleTimeout = setTimeout(showNext, props.interval - props.displayDuration);
175
+ }, props.displayDuration);
168
176
  };
169
177
 
170
178
  onMounted(() => {
171
- if (process.client && props.notifications.length > 0) {
172
- showTimeout = setTimeout(showNext, props.initialDelay);
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
- if (showTimeout) clearTimeout(showTimeout);
178
- if (hideTimeout) clearTimeout(hideTimeout);
179
- if (cycleTimeout) clearTimeout(cycleTimeout);
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>