@xenterprises/nuxt-x-marketing 0.0.23 → 1.0.0

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 (115) hide show
  1. package/.claude/settings.local.json +22 -0
  2. package/.playground/app.config.ts +44 -45
  3. package/.playground/app.vue +263 -0
  4. package/.playground/components/AppFooter.vue +103 -0
  5. package/.playground/components/AppHeader.vue +41 -0
  6. package/.playground/components/CodeExample.vue +70 -0
  7. package/.playground/components/ComponentPlayground.vue +117 -0
  8. package/.playground/components/DocsHeader.vue +19 -0
  9. package/.playground/layouts/default.vue +13 -3
  10. package/.playground/layouts/docs-layout.vue +161 -0
  11. package/.playground/layouts/docs.vue +11 -0
  12. package/.playground/package-lock.json +6 -0
  13. package/.playground/pages/blog/author.vue +145 -0
  14. package/.playground/pages/blog/card.vue +149 -0
  15. package/.playground/pages/blog/cta.vue +131 -0
  16. package/.playground/pages/blog/detail.vue +379 -0
  17. package/.playground/pages/blog/list.vue +261 -0
  18. package/.playground/pages/blog/navigation.vue +228 -0
  19. package/.playground/pages/blog/sidebar.vue +279 -0
  20. package/.playground/pages/config.vue +289 -0
  21. package/.playground/pages/content/comparison.vue +253 -0
  22. package/.playground/pages/content/cta.vue +245 -0
  23. package/.playground/pages/content/features.vue +217 -0
  24. package/.playground/pages/content/newsletter-section.vue +260 -0
  25. package/.playground/pages/content/newsletter.vue +200 -0
  26. package/.playground/pages/content/pricing.vue +335 -0
  27. package/.playground/pages/content/testimonials.vue +188 -0
  28. package/.playground/pages/hero.vue +257 -0
  29. package/.playground/pages/index.vue +379 -2
  30. package/.playground/pages/layout/footer.vue +210 -0
  31. package/.playground/pages/layout/legal-footer.vue +121 -0
  32. package/.playground/pages/layout/navbar.vue +285 -0
  33. package/.playground/pages/layout/section.vue +266 -0
  34. package/.playground/pages/layout/x-legal.vue +199 -0
  35. package/.playground/pages/modals/feature-modal.vue +320 -0
  36. package/.playground/pages/modals/image-lightbox.vue +277 -0
  37. package/.playground/pages/modals/video-modal.vue +222 -0
  38. package/.playground/pages/notifications/announcement-bar.vue +164 -0
  39. package/.playground/pages/notifications/cookie-banner.vue +210 -0
  40. package/.playground/pages/notifications/cookie-toast.vue +143 -0
  41. package/.playground/pages/notifications/gdpr.vue +276 -0
  42. package/.playground/pages/notifications/social-proof-toast.vue +255 -0
  43. package/.playground/pages/ui/badge.vue +161 -0
  44. package/.playground/pages/ui/bg-pattern.vue +177 -0
  45. package/.playground/pages/ui/glass-card.vue +179 -0
  46. package/.playground/pages/ui/glow-divider.vue +179 -0
  47. package/.playground/pages/ui/promo-card.vue +259 -0
  48. package/.playground/pages/ui/section-stitch.vue +411 -0
  49. package/.playground/pages/utilities/back-to-top.vue +153 -0
  50. package/.playground/pages/utilities/popup-chat.vue +224 -0
  51. package/README.md +799 -164
  52. package/app/app.config.ts +88 -2
  53. package/app/app.vue +106 -11
  54. package/app/assets/css/x-marketing.css +668 -0
  55. package/app/components/X/Footer/XLegal/index.vue +63 -26
  56. package/app/components/X/Footer/index.vue +89 -89
  57. package/app/components/X/Header/App.vue +63 -0
  58. package/app/components/X/Mark/Blog/Author.vue +170 -0
  59. package/app/components/X/Mark/Blog/CTANewsletter.vue +197 -0
  60. package/app/components/X/Mark/Blog/Card.vue +117 -0
  61. package/app/components/X/Mark/Blog/Detail.vue +240 -0
  62. package/app/components/X/Mark/Blog/List.vue +156 -0
  63. package/app/components/X/Mark/Blog/Navigation.vue +100 -0
  64. package/app/components/X/Mark/Blog/Sidebar.vue +160 -0
  65. package/app/components/X/Mark/Button/BackToTop.vue +99 -0
  66. package/app/components/X/Mark/CTA/index.vue +294 -0
  67. package/app/components/X/Mark/Card/Glass.vue +64 -0
  68. package/app/components/X/Mark/Card/Promo.vue +164 -0
  69. package/app/components/X/Mark/Divider/Glow.vue +113 -0
  70. package/app/components/X/Mark/Features/index.vue +194 -0
  71. package/app/components/X/Mark/Hero/index.vue +217 -0
  72. package/app/components/X/Mark/Layout/AnnouncementBar.vue +139 -0
  73. package/app/components/X/Mark/Layout/Footer.vue +160 -0
  74. package/app/components/X/Mark/Layout/FooterLegal.vue +42 -0
  75. package/app/components/X/Mark/Layout/Navbar.vue +267 -0
  76. package/app/components/X/Mark/Modal/Chat.vue +151 -0
  77. package/app/components/X/Mark/Modal/Feature.vue +197 -0
  78. package/app/components/X/Mark/Modal/Image.vue +226 -0
  79. package/app/components/X/Mark/Modal/Video.vue +203 -0
  80. package/app/components/X/Mark/Modal/index.vue +133 -0
  81. package/app/components/X/Mark/Newsletter/Form.vue +174 -0
  82. package/app/components/X/Mark/Pattern/Bg.vue +86 -0
  83. package/app/components/X/Mark/Pricing/Comparison.vue +126 -0
  84. package/app/components/X/Mark/Pricing/Plans.vue +181 -0
  85. package/app/components/X/Mark/Privacy/CookieBanner.vue +112 -0
  86. package/app/components/X/Mark/Privacy/CookieToast.vue +43 -0
  87. package/app/components/X/Mark/Privacy/GDPR.vue +245 -0
  88. package/app/components/X/Mark/Section/Newsletter.vue +183 -0
  89. package/app/components/X/Mark/Section/Stitch.vue +156 -0
  90. package/app/components/X/Mark/Section/index.vue +151 -0
  91. package/app/components/X/Mark/SocialProof/Badge.vue +102 -0
  92. package/app/components/X/Mark/SocialProof/Testimonials.vue +326 -0
  93. package/app/components/X/Mark/SocialProof/Toast.vue +136 -0
  94. package/app/components/X/Mark/_archive/Pattern/Background.vue +83 -0
  95. package/app/components/X/Mark/_archive/Pattern/CheckBg.vue +35 -0
  96. package/app/components/X/Mark/_archive/Pattern/CircuitBg.vue +41 -0
  97. package/app/components/X/Mark/_archive/Pattern/DotsBg.vue +35 -0
  98. package/app/components/X/Mark/_archive/Pattern/GlowDivider.vue +86 -0
  99. package/app/components/X/Mark/_archive/Pattern/HexBg.vue +35 -0
  100. package/app/components/X/Mark/_archive/Pattern/PlaidBg.vue +47 -0
  101. package/app/components/X/Mark/_archive/Pattern/SectionStitch.vue +168 -0
  102. package/app/components/X/Mark/_archive/Pattern/WaveBg.vue +41 -0
  103. package/app/components/X/_archive/Newsletter/Form.vue +123 -0
  104. package/app/components/X/_archive/Newsletter/Section.vue +131 -0
  105. package/app/composables/useParallax.ts +124 -0
  106. package/app/composables/useScrollReveal.ts +92 -0
  107. package/app/pages/index.vue +210 -0
  108. package/app/plugins/marketing.client.ts +13 -0
  109. package/app/types/marketing.d.ts +268 -0
  110. package/componentList.json +511 -0
  111. package/nuxt.config.ts +6 -13
  112. package/package.json +21 -9
  113. package/.editorconfig +0 -12
  114. package/app/assets/css/main.css +0 -2
  115. package/app/components/X/Marketing/Sections/EmailSubscription/index.vue +0 -129
@@ -0,0 +1,217 @@
1
+ <template>
2
+ <section
3
+ class="relative min-h-svh flex overflow-hidden"
4
+ :class="[
5
+ alignmentClasses,
6
+ verticalAlignClasses,
7
+ { 'pt-20': includeNavPadding }
8
+ ]"
9
+ >
10
+ <!-- Background -->
11
+ <div class="absolute inset-0">
12
+ <!-- Custom Background Slot -->
13
+ <slot name="background">
14
+ <!-- Video Background -->
15
+ <video
16
+ v-if="video?.src"
17
+ :src="video.src"
18
+ :poster="video.poster || img?.src"
19
+ autoplay
20
+ loop
21
+ muted
22
+ playsinline
23
+ class="absolute inset-0 w-full h-full object-cover xParallax"
24
+ data-parallax-speed="0.3"
25
+ />
26
+
27
+ <!-- Image Background -->
28
+ <img
29
+ v-else-if="img?.src"
30
+ :src="img.src"
31
+ :alt="img.alt || ''"
32
+ class="absolute inset-0 w-full h-full object-cover xParallax"
33
+ data-parallax-speed="0.3"
34
+ />
35
+ </slot>
36
+
37
+ <!-- Overlay -->
38
+ <div
39
+ v-if="overlay !== 'none'"
40
+ class="absolute inset-0"
41
+ :class="overlayClass"
42
+ />
43
+ </div>
44
+
45
+ <!-- Content -->
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
+ >
51
+ <!-- Eyebrow -->
52
+ <p
53
+ v-if="eyebrow"
54
+ class="xText-eyebrow text-white/70 mb-4 xFadeUp"
55
+ >
56
+ {{ eyebrow }}
57
+ </p>
58
+
59
+ <!-- Title -->
60
+ <h1
61
+ class="xText-display text-white xText-balance xFadeUp"
62
+ style="transition-delay: 100ms"
63
+ >
64
+ <slot name="title">{{ title }}</slot>
65
+ </h1>
66
+
67
+ <!-- Subtitle -->
68
+ <p
69
+ v-if="subtitle || $slots.subtitle"
70
+ class="mt-6 text-xl text-white/80 max-w-2xl xFadeUp"
71
+ style="transition-delay: 200ms"
72
+ >
73
+ <slot name="subtitle">{{ subtitle }}</slot>
74
+ </p>
75
+
76
+ <!-- Actions -->
77
+ <div
78
+ v-if="buttons.length || $slots.actions"
79
+ class="mt-10 flex flex-wrap gap-4 xFadeUp"
80
+ :class="{ 'justify-center': align === 'center', 'justify-end': align === 'right' }"
81
+ style="transition-delay: 300ms"
82
+ >
83
+ <slot name="actions">
84
+ <UButton
85
+ v-for="(btn, index) in buttons"
86
+ :key="index"
87
+ size="xl"
88
+ :color="btn.color || (index === 0 ? 'primary' : 'neutral')"
89
+ :variant="btn.variant || 'solid'"
90
+ :to="btn.to"
91
+ :icon="btn.icon"
92
+ :class="[
93
+ btn.variant === 'outline' || (index > 0 && !btn.variant)
94
+ ? 'bg-white/10 backdrop-blur-sm border-white/40 text-white hover:bg-white/20 hover:border-white/60'
95
+ : ''
96
+ ]"
97
+ >
98
+ {{ btn.label }}
99
+ </UButton>
100
+ </slot>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Scroll Indicator -->
106
+ <div
107
+ v-if="hasScrollIndicator"
108
+ class="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2"
109
+ >
110
+ <span class="text-xs text-white/50 uppercase tracking-widest">Scroll</span>
111
+ <div class="w-6 h-10 border-2 border-white/30 rounded-full flex justify-center pt-2">
112
+ <div class="w-1.5 h-1.5 bg-white/60 rounded-full animate-[scroll-indicator_2s_ease-in-out_infinite]" />
113
+ </div>
114
+ </div>
115
+ </section>
116
+ </template>
117
+
118
+ <script setup>
119
+ const props = defineProps({
120
+ /** Background image object { src: string, alt?: string } */
121
+ img: {
122
+ type: Object,
123
+ default: null,
124
+ validator: (v) => !v || typeof v.src === 'string',
125
+ },
126
+ /** Background video object { src: string, poster?: string } */
127
+ video: {
128
+ type: Object,
129
+ default: null,
130
+ validator: (v) => !v || typeof v.src === 'string',
131
+ },
132
+ /** Overlay style */
133
+ overlay: {
134
+ type: String,
135
+ default: 'gradient',
136
+ validator: (v) => ['light', 'heavy', 'gradient', 'none'].includes(v),
137
+ },
138
+ /** Small text above title */
139
+ eyebrow: {
140
+ type: String,
141
+ default: '',
142
+ },
143
+ /** Main headline */
144
+ title: {
145
+ type: String,
146
+ required: true,
147
+ },
148
+ /** Supporting text */
149
+ subtitle: {
150
+ type: String,
151
+ default: '',
152
+ },
153
+ /** Action buttons array [{ label, color?, variant?, to?, icon? }] */
154
+ buttons: {
155
+ type: Array,
156
+ default: () => [],
157
+ validator: (v) => v.every(btn => btn.label),
158
+ },
159
+ /** Content horizontal alignment */
160
+ align: {
161
+ type: String,
162
+ default: 'left',
163
+ validator: (v) => ['left', 'center', 'right'].includes(v),
164
+ },
165
+ /** Content vertical position */
166
+ verticalAlign: {
167
+ type: String,
168
+ default: 'center',
169
+ validator: (v) => ['center', 'bottom'].includes(v),
170
+ },
171
+ /** Has scroll indicator arrow */
172
+ hasScrollIndicator: {
173
+ type: Boolean,
174
+ default: true,
175
+ },
176
+ /** Add top padding for non-transparent nav */
177
+ includeNavPadding: {
178
+ type: Boolean,
179
+ default: false,
180
+ },
181
+ });
182
+
183
+ const overlayClass = computed(() => {
184
+ const overlays = {
185
+ light: 'bg-black/30',
186
+ heavy: 'bg-black/60',
187
+ gradient: 'bg-gradient-to-t from-black/80 via-black/40 to-transparent',
188
+ };
189
+ return overlays[props.overlay] || '';
190
+ });
191
+
192
+ const alignmentClasses = computed(() => {
193
+ const aligns = {
194
+ left: 'text-left',
195
+ center: 'text-center',
196
+ right: 'text-right',
197
+ };
198
+ return aligns[props.align];
199
+ });
200
+
201
+ const verticalAlignClasses = computed(() => {
202
+ const aligns = {
203
+ center: 'items-center',
204
+ bottom: 'items-end pb-24',
205
+ };
206
+ return aligns[props.verticalAlign];
207
+ });
208
+
209
+ const contentAlignmentClasses = computed(() => {
210
+ const aligns = {
211
+ left: '',
212
+ center: 'mx-auto',
213
+ right: 'ml-auto',
214
+ };
215
+ return aligns[props.align];
216
+ });
217
+ </script>
@@ -0,0 +1,139 @@
1
+ <template>
2
+ <Transition name="slide-down">
3
+ <div
4
+ v-if="visible"
5
+ :class="[
6
+ 'relative z-50',
7
+ bgClasses
8
+ ]"
9
+ >
10
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2.5">
11
+ <div class="flex items-center justify-center gap-x-4 text-sm">
12
+ <!-- Icon -->
13
+ <UIcon
14
+ v-if="icon"
15
+ :name="icon"
16
+ class="w-5 h-5 flex-shrink-0"
17
+ />
18
+
19
+ <!-- Message -->
20
+ <p class="text-center">
21
+ <span v-if="badge" class="font-semibold mr-2">{{ badge }}</span>
22
+ <span>{{ message }}</span>
23
+ </p>
24
+
25
+ <!-- CTA Link -->
26
+ <NuxtLink
27
+ v-if="link"
28
+ :to="link"
29
+ class="inline-flex items-center gap-1 font-medium hover:underline underline-offset-2"
30
+ >
31
+ {{ linkText }}
32
+ <UIcon name="i-lucide-arrow-right" class="w-4 h-4" />
33
+ </NuxtLink>
34
+
35
+ <!-- Close Button -->
36
+ <button
37
+ v-if="dismissible"
38
+ class="absolute right-4 top-1/2 -translate-y-1/2 p-1 rounded-full hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
39
+ @click="dismiss"
40
+ >
41
+ <UIcon name="i-lucide-x" class="w-4 h-4" />
42
+ <span class="sr-only">Dismiss</span>
43
+ </button>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </Transition>
48
+ </template>
49
+
50
+ <script setup>
51
+ const props = defineProps({
52
+ /** Main message text */
53
+ message: {
54
+ type: String,
55
+ required: true,
56
+ },
57
+ /** Optional badge/label before message */
58
+ badge: {
59
+ type: String,
60
+ default: '',
61
+ },
62
+ /** Icon name to display */
63
+ icon: {
64
+ type: String,
65
+ default: '',
66
+ },
67
+ /** CTA link URL */
68
+ link: {
69
+ type: String,
70
+ default: '',
71
+ },
72
+ /** CTA link text */
73
+ linkText: {
74
+ type: String,
75
+ default: 'Learn more',
76
+ },
77
+ /** Color variant */
78
+ variant: {
79
+ type: String,
80
+ default: 'primary',
81
+ validator: (v) => ['primary', 'neutral', 'success', 'warning', 'error'].includes(v),
82
+ },
83
+ /** Allow dismissing the bar */
84
+ dismissible: {
85
+ type: Boolean,
86
+ default: true,
87
+ },
88
+ /** Storage key for dismissed state */
89
+ storageKey: {
90
+ type: String,
91
+ default: 'announcement-dismissed',
92
+ },
93
+ });
94
+
95
+ const emit = defineEmits(['dismiss']);
96
+
97
+ const visible = ref(true);
98
+
99
+ onMounted(() => {
100
+ if (props.dismissible && props.storageKey) {
101
+ const dismissed = localStorage.getItem(props.storageKey);
102
+ if (dismissed) {
103
+ visible.value = false;
104
+ }
105
+ }
106
+ });
107
+
108
+ const dismiss = () => {
109
+ if (props.storageKey) {
110
+ localStorage.setItem(props.storageKey, 'true');
111
+ }
112
+ visible.value = false;
113
+ emit('dismiss');
114
+ };
115
+
116
+ const bgClasses = computed(() => {
117
+ const variants = {
118
+ primary: 'bg-primary-500 text-white',
119
+ neutral: 'bg-neutral-900 dark:bg-neutral-100 text-white dark:text-neutral-900',
120
+ success: 'bg-green-500 text-white',
121
+ warning: 'bg-yellow-500 text-neutral-900',
122
+ error: 'bg-red-500 text-white',
123
+ };
124
+ return variants[props.variant];
125
+ });
126
+ </script>
127
+
128
+ <style scoped>
129
+ .slide-down-enter-active,
130
+ .slide-down-leave-active {
131
+ transition: transform 0.3s ease, opacity 0.3s ease;
132
+ }
133
+
134
+ .slide-down-enter-from,
135
+ .slide-down-leave-to {
136
+ transform: translateY(-100%);
137
+ opacity: 0;
138
+ }
139
+ </style>
@@ -0,0 +1,160 @@
1
+ <template>
2
+ <footer class="bg-neutral-950 text-white">
3
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 lg:py-20">
4
+ <!-- Main Footer Grid -->
5
+ <div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-8 lg:gap-12">
6
+ <!-- Brand Column -->
7
+ <div class="col-span-2">
8
+ <slot name="brand">
9
+ <NuxtLink v-if="logo?.src" to="/" class="inline-block">
10
+ <img
11
+ :src="logo.src"
12
+ :alt="logo.alt || 'Logo'"
13
+ class="h-8 w-auto"
14
+ />
15
+ </NuxtLink>
16
+ <p
17
+ v-if="description"
18
+ class="mt-4 text-neutral-400 max-w-xs"
19
+ >
20
+ {{ description }}
21
+ </p>
22
+ </slot>
23
+
24
+ <!-- Social Links -->
25
+ <div v-if="social.length" class="mt-6 flex gap-4">
26
+ <a
27
+ v-for="item in social"
28
+ :key="item.name"
29
+ :href="item.href"
30
+ target="_blank"
31
+ rel="noopener noreferrer"
32
+ class="text-neutral-400 hover:text-primary-400 transition-all duration-300 hover:scale-110"
33
+ >
34
+ <UIcon :name="item.icon" class="w-5 h-5" />
35
+ <span class="sr-only">{{ item.name }}</span>
36
+ </a>
37
+ </div>
38
+ </div>
39
+
40
+ <!-- Link Columns -->
41
+ <div
42
+ v-for="column in columns"
43
+ :key="column.title"
44
+ class="col-span-1"
45
+ >
46
+ <h4 class="font-semibold text-sm uppercase tracking-wider text-neutral-300 mb-4">
47
+ {{ column.title }}
48
+ </h4>
49
+ <ul class="space-y-3">
50
+ <li v-for="link in column.links" :key="link.label">
51
+ <NuxtLink
52
+ :to="link.to"
53
+ class="text-neutral-400 hover:text-white transition-colors text-sm inline-block relative group"
54
+ >
55
+ {{ link.label }}
56
+ <span class="absolute -bottom-0.5 left-0 w-0 h-px bg-primary-500 transition-all duration-300 group-hover:w-full" />
57
+ </NuxtLink>
58
+ </li>
59
+ </ul>
60
+ </div>
61
+ </div>
62
+
63
+ <!-- Newsletter Section -->
64
+ <div
65
+ v-if="hasNewsletter"
66
+ class="mt-12 pt-8 border-t border-neutral-800"
67
+ >
68
+ <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
69
+ <div>
70
+ <h4 class="font-semibold">{{ newsletterTitle }}</h4>
71
+ <p class="mt-1 text-sm text-neutral-400">{{ newsletterDescription }}</p>
72
+ </div>
73
+ <form class="flex gap-2 w-full md:w-auto" @submit.prevent="$emit('newsletter-submit', email)">
74
+ <UInput
75
+ v-model="email"
76
+ type="email"
77
+ placeholder="Enter your email"
78
+ class="flex-1 md:w-64"
79
+ />
80
+ <UButton type="submit" color="primary">
81
+ Subscribe
82
+ </UButton>
83
+ </form>
84
+ </div>
85
+ </div>
86
+
87
+ <!-- Bottom Bar -->
88
+ <div class="mt-12 pt-8 border-t border-neutral-800 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
89
+ <p class="text-sm text-neutral-500">
90
+ {{ copyright }}
91
+ </p>
92
+ <div v-if="legalLinks.length" class="flex flex-wrap gap-6">
93
+ <NuxtLink
94
+ v-for="link in legalLinks"
95
+ :key="link.label"
96
+ :to="link.to"
97
+ class="text-sm text-neutral-500 hover:text-neutral-300 transition-colors"
98
+ >
99
+ {{ link.label }}
100
+ </NuxtLink>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </footer>
105
+ </template>
106
+
107
+ <script setup>
108
+ const props = defineProps({
109
+ /** Logo object { src: string, alt?: string } */
110
+ logo: {
111
+ type: Object,
112
+ default: null,
113
+ validator: (v) => !v || typeof v.src === 'string',
114
+ },
115
+ /** Brand description */
116
+ description: {
117
+ type: String,
118
+ default: '',
119
+ },
120
+ /** Social media links */
121
+ social: {
122
+ type: Array,
123
+ default: () => [],
124
+ },
125
+ /** Footer link columns */
126
+ columns: {
127
+ type: Array,
128
+ default: () => [],
129
+ },
130
+ /** Legal/policy links */
131
+ legalLinks: {
132
+ type: Array,
133
+ default: () => [],
134
+ },
135
+ /** Copyright text */
136
+ copyright: {
137
+ type: String,
138
+ default: () => `© ${new Date().getFullYear()} All rights reserved.`,
139
+ },
140
+ /** Has newsletter section */
141
+ hasNewsletter: {
142
+ type: Boolean,
143
+ default: false,
144
+ },
145
+ /** Newsletter title */
146
+ newsletterTitle: {
147
+ type: String,
148
+ default: 'Subscribe to our newsletter',
149
+ },
150
+ /** Newsletter description */
151
+ newsletterDescription: {
152
+ type: String,
153
+ default: 'Get the latest updates and news.',
154
+ },
155
+ });
156
+
157
+ defineEmits(['newsletter-submit']);
158
+
159
+ const email = ref('');
160
+ </script>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <footer class="border-t border-neutral-200 dark:border-neutral-800">
3
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
4
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
5
+ <!-- Copyright -->
6
+ <p class="text-sm text-neutral-500 dark:text-neutral-400">
7
+ {{ copyright }}
8
+ </p>
9
+
10
+ <!-- Legal Links -->
11
+ <nav v-if="links.length" class="flex flex-wrap gap-6">
12
+ <NuxtLink
13
+ v-for="link in links"
14
+ :key="link.label"
15
+ :to="link.to"
16
+ class="text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors"
17
+ >
18
+ {{ link.label }}
19
+ </NuxtLink>
20
+ </nav>
21
+ </div>
22
+ </div>
23
+ </footer>
24
+ </template>
25
+
26
+ <script setup>
27
+ defineProps({
28
+ /** Copyright text */
29
+ copyright: {
30
+ type: String,
31
+ default: () => `© ${new Date().getFullYear()} All rights reserved.`,
32
+ },
33
+ /** Legal/policy links */
34
+ links: {
35
+ type: Array,
36
+ default: () => [
37
+ { label: 'Privacy Policy', to: '/privacy' },
38
+ { label: 'Terms of Service', to: '/terms' },
39
+ ],
40
+ },
41
+ });
42
+ </script>