@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
@@ -2,45 +2,82 @@
2
2
  <footer
3
3
  class="bg-gray-900 text-gray-400 text-xs relative overflow-visible pt-2"
4
4
  >
5
- <a href="https://x.enterprises" target="_blank">
5
+ <a v-if="resolvedLogo.src" :href="resolvedLogo.href" target="_blank">
6
6
  <img
7
- src="https://cdn.x.enterprises/logo-x.png"
8
- alt="X Enterprises"
7
+ :src="resolvedLogo.src"
8
+ :alt="resolvedLogo.alt"
9
9
  class="h-6 w-6 mx-auto -mt-4 relative z-10"
10
10
  />
11
11
  </a>
12
12
  <main
13
- class="flex flex-row justify-between items-center px-6 pt-3 pb-5 max-w-screen-2xl mx-auto"
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
14
  >
15
- <div class="pb-4 md:pb-0 text-center md:text-left">
16
- Copyright &copy;2016-{{ new Date().getFullYear() }} X Enterprises, LLC.
17
- All Rights Reserved.
15
+ <div class="text-center md:text-left">
16
+ {{ resolvedCopyright }}
18
17
  </div>
19
18
 
20
- <div class="text-center md:text-right flex gap-4">
19
+ <nav v-if="resolvedLinks.length" class="text-center md:text-right flex flex-wrap justify-center gap-4">
21
20
  <a
22
- href="https://x.enterprises/legal/terms"
21
+ v-for="link in resolvedLinks"
22
+ :key="link.label"
23
+ :href="link.href"
23
24
  class="hover:underline"
24
25
  target="_blank"
25
26
  >
26
- Terms of Service
27
+ {{ link.label }}
27
28
  </a>
28
- <a
29
- href="https://x.enterprises/legal/privacy-policy"
30
- class="ml-4 hover:underline"
31
- target="_blank"
32
- >
33
- Privacy Policy
34
- </a>
35
- <a
36
- href="https://x.enterprises/legal/cookie-policy"
37
- class="ml-4 hover:underline"
38
- target="_blank"
39
- >
40
- Cookie Policy
41
- </a>
42
- </div>
29
+ </nav>
43
30
  </main>
44
31
  </footer>
45
32
  </template>
46
- <script setup></script>
33
+
34
+ <script setup>
35
+ const props = defineProps({
36
+ /** Copyright text (defaults to X Enterprises) */
37
+ copyright: {
38
+ type: String,
39
+ default: '',
40
+ },
41
+ /** Legal links array [{ label, href }] (defaults to X Enterprises legal pages) */
42
+ links: {
43
+ type: Array,
44
+ default: null,
45
+ },
46
+ /** Logo object { src, alt, href } (defaults to X Enterprises logo) */
47
+ logo: {
48
+ type: Object,
49
+ default: null,
50
+ },
51
+ });
52
+
53
+ // Default X Enterprises values
54
+ const defaultLogo = {
55
+ src: 'https://cdn.x.enterprises/logo-x.png',
56
+ alt: 'X Enterprises',
57
+ href: 'https://x.enterprises',
58
+ };
59
+
60
+ const defaultLinks = [
61
+ { label: 'Terms of Service', href: 'https://x.enterprises/legal/terms' },
62
+ { label: 'Privacy Policy', href: 'https://x.enterprises/legal/privacy-policy' },
63
+ { label: 'Cookie Policy', href: 'https://x.enterprises/legal/cookie-policy' },
64
+ ];
65
+
66
+ const defaultCopyright = `Copyright ©2016-${new Date().getFullYear()} X Enterprises, LLC. All Rights Reserved.`;
67
+
68
+ // Resolved values with defaults
69
+ const resolvedLogo = computed(() => {
70
+ if (props.logo === null) return defaultLogo;
71
+ if (props.logo === false) return { src: '', alt: '', href: '' };
72
+ return { ...defaultLogo, ...props.logo };
73
+ });
74
+
75
+ const resolvedLinks = computed(() => {
76
+ if (props.links === null) return defaultLinks;
77
+ return props.links;
78
+ });
79
+
80
+ const resolvedCopyright = computed(() => {
81
+ return props.copyright || defaultCopyright;
82
+ });
83
+ </script>
@@ -1,93 +1,93 @@
1
1
  <template>
2
- <div>
3
- <XMarketingSectionsEmailSubscription
4
- :organizationId="
5
- appConfig?.xMarketing?.config?.emailMarketingNewsletters?.organizationId
6
- "
7
- v-if="
8
- appConfig?.xMarketing?.config?.emailMarketingNewsletters?.organizationId
9
- "
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
- class="absolute left-1/2 bottom-0 h-full transform -translate-x-1/2"
17
- :src="footer?.bg?.img?.src"
18
- :alt="footer?.bg?.img?.alt"
19
- v-if="footer?.bg?.img?.src"
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
- class="w-auto"
44
- v-for="(social, index) in footer?.socials"
45
- :key="index"
46
- >
47
- <UButton
48
- :to="social.url"
49
- :icon="social?.icon"
50
- :alt="social?.name"
51
- color="gray"
52
- square
53
- target="_blank"
54
- variant="ghost"
55
- >
56
- </UButton>
57
- </div>
58
- <UColorModeButton />
59
- </div>
60
- </div>
61
- <div
62
- class="flex flex-col sm:flex-row gap-8 lg:gap-12 lg:justify-end"
63
- >
64
- <div
65
- class="text-left sm:text-right"
66
- v-for="(column, index) in footer.columns"
67
- :key="index"
68
- >
69
- <h3
70
- class="mb-6 text-sm text-gray-500 uppercase font-semibold leading-normal tracking-wide"
71
- >
72
- {{ column?.headerLabel }}
73
- </h3>
74
- <ul class="space-y-4">
75
- <li v-for="link in column?.links" :key="link?.label">
76
- <nuxt-link
77
- :to="link?.to"
78
- :target="link?.target ? link?.target : '_self'"
79
- class="text-white hover:text-gray-300 font-medium leading-relaxed transition-colors"
80
- >{{ link?.label }}</nuxt-link
81
- >
82
- </li>
83
- </ul>
84
- </div>
85
- </div>
86
- </div>
87
- </div>
88
- </div>
89
- </section>
90
- </div>
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) => console.log('Newsletter signup:', 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
+ class="absolute left-1/2 bottom-0 h-full transform -translate-x-1/2"
17
+ :src="footer?.bg?.img?.src"
18
+ :alt="footer?.bg?.img?.alt"
19
+ v-if="footer?.bg?.img?.src"
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
+ class="w-auto"
44
+ v-for="(social, index) in footer?.socials"
45
+ :key="index"
46
+ >
47
+ <UButton
48
+ :to="social.url"
49
+ :icon="social?.icon"
50
+ :alt="social?.name"
51
+ color="gray"
52
+ square
53
+ target="_blank"
54
+ variant="ghost"
55
+ >
56
+ </UButton>
57
+ </div>
58
+ <UColorModeButton />
59
+ </div>
60
+ </div>
61
+ <div
62
+ class="flex flex-col sm:flex-row gap-8 lg:gap-12 lg:justify-end"
63
+ >
64
+ <div
65
+ class="text-left sm:text-right"
66
+ v-for="(column, index) in footer.columns"
67
+ :key="index"
68
+ >
69
+ <h3
70
+ class="mb-6 text-sm text-gray-500 uppercase font-semibold leading-normal tracking-wide"
71
+ >
72
+ {{ column?.headerLabel }}
73
+ </h3>
74
+ <ul class="space-y-4">
75
+ <li v-for="link in column?.links" :key="link?.label">
76
+ <nuxt-link
77
+ :to="link?.to"
78
+ :target="link?.target ? link?.target : '_self'"
79
+ class="text-white hover:text-gray-300 font-medium leading-relaxed transition-colors"
80
+ >{{ link?.label }}</nuxt-link
81
+ >
82
+ </li>
83
+ </ul>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </section>
90
+ </div>
91
91
  </template>
92
92
  <script setup>
93
93
  const appConfig = useAppConfig();
@@ -0,0 +1,63 @@
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
+ const props = defineProps({
40
+ navLinks: {
41
+ type: Array,
42
+ default: () => [],
43
+ },
44
+ primaryCta: {
45
+ type: Object,
46
+ default: null,
47
+ },
48
+ secondaryCta: {
49
+ type: Object,
50
+ default: null,
51
+ },
52
+ transparent: {
53
+ type: Boolean,
54
+ default: false,
55
+ },
56
+ });
57
+
58
+ const headerClasses = computed(() => {
59
+ return props.transparent
60
+ ? "bg-transparent text-white"
61
+ : "bg-white dark:bg-deep-950";
62
+ });
63
+ </script>
@@ -0,0 +1,170 @@
1
+ <template>
2
+ <!-- Inline / Small variant -->
3
+ <div
4
+ v-if="variant === 'inline' || variant === 'sm'"
5
+ class="flex items-center gap-3"
6
+ >
7
+ <figure>
8
+ <img
9
+ v-if="author.avatar"
10
+ :src="author.avatar"
11
+ :alt="author.name"
12
+ :class="[
13
+ 'rounded-full object-cover',
14
+ variant === 'sm' ? 'w-8 h-8' : 'w-10 h-10'
15
+ ]"
16
+ />
17
+ <div
18
+ v-else
19
+ :class="[
20
+ 'rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center',
21
+ variant === 'sm' ? 'w-8 h-8' : 'w-10 h-10'
22
+ ]"
23
+ >
24
+ <span class="text-primary-600 dark:text-primary-400 font-medium">
25
+ {{ author.name?.charAt(0) }}
26
+ </span>
27
+ </div>
28
+ </figure>
29
+ <div>
30
+ <p class="font-medium text-neutral-900 dark:text-white">
31
+ {{ author.name }}
32
+ </p>
33
+ <p v-if="author.title" class="text-sm text-neutral-500">
34
+ {{ author.title }}
35
+ </p>
36
+ </div>
37
+ </div>
38
+
39
+ <!-- Full Bio variant -->
40
+ <div
41
+ v-else-if="variant === 'full'"
42
+ class="flex flex-col sm:flex-row gap-6 p-6 rounded-2xl bg-neutral-100 dark:bg-neutral-900"
43
+ >
44
+ <figure class="flex-shrink-0">
45
+ <img
46
+ v-if="author.avatar"
47
+ :src="author.avatar"
48
+ :alt="author.name"
49
+ class="w-20 h-20 rounded-full object-cover"
50
+ />
51
+ <div
52
+ v-else
53
+ class="w-20 h-20 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center"
54
+ >
55
+ <span class="text-primary-600 dark:text-primary-400 font-medium text-2xl">
56
+ {{ author.name?.charAt(0) }}
57
+ </span>
58
+ </div>
59
+ </figure>
60
+ <div class="flex-1">
61
+ <p class="font-semibold text-lg text-neutral-900 dark:text-white">
62
+ {{ author.name }}
63
+ </p>
64
+ <p v-if="author.title" class="text-sm text-neutral-500 mb-3">
65
+ {{ author.title }}
66
+ </p>
67
+ <p v-if="author.bio" class="text-neutral-600 dark:text-neutral-400">
68
+ {{ author.bio }}
69
+ </p>
70
+
71
+ <!-- Social Links -->
72
+ <div v-if="hasSocialLinks" class="flex gap-2 mt-4">
73
+ <UButton
74
+ v-for="(url, platform) in author.social"
75
+ :key="platform"
76
+ :to="url"
77
+ :icon="getSocialIcon(platform)"
78
+ variant="ghost"
79
+ color="neutral"
80
+ size="sm"
81
+ target="_blank"
82
+ :aria-label="platform"
83
+ />
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <!-- Card variant -->
89
+ <div
90
+ v-else-if="variant === 'card'"
91
+ class="text-center p-6 rounded-2xl bg-neutral-100 dark:bg-neutral-900"
92
+ >
93
+ <figure class="mx-auto mb-4">
94
+ <img
95
+ v-if="author.avatar"
96
+ :src="author.avatar"
97
+ :alt="author.name"
98
+ class="w-24 h-24 rounded-full object-cover mx-auto ring-4 ring-white dark:ring-neutral-800"
99
+ />
100
+ <div
101
+ v-else
102
+ class="w-24 h-24 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center mx-auto"
103
+ >
104
+ <span class="text-primary-600 dark:text-primary-400 font-medium text-3xl">
105
+ {{ author.name?.charAt(0) }}
106
+ </span>
107
+ </div>
108
+ </figure>
109
+ <p class="font-semibold text-lg text-neutral-900 dark:text-white">
110
+ {{ author.name }}
111
+ </p>
112
+ <p v-if="author.title" class="text-sm text-neutral-500 mt-1">
113
+ {{ author.title }}
114
+ </p>
115
+ <p v-if="author.bio" class="text-sm text-neutral-600 dark:text-neutral-400 mt-3">
116
+ {{ author.bio }}
117
+ </p>
118
+
119
+ <!-- Social Links -->
120
+ <div v-if="hasSocialLinks" class="flex justify-center gap-2 mt-4">
121
+ <UButton
122
+ v-for="(url, platform) in author.social"
123
+ :key="platform"
124
+ :to="url"
125
+ :icon="getSocialIcon(platform)"
126
+ variant="soft"
127
+ color="neutral"
128
+ size="sm"
129
+ target="_blank"
130
+ :aria-label="platform"
131
+ />
132
+ </div>
133
+ </div>
134
+ </template>
135
+
136
+ <script setup>
137
+ const props = defineProps({
138
+ /** Author object */
139
+ author: {
140
+ type: Object,
141
+ required: true,
142
+ // { name, avatar?, title?, bio?, social?: { twitter?, linkedin?, github?, website? } }
143
+ },
144
+ /** Display variant */
145
+ variant: {
146
+ type: String,
147
+ default: 'inline',
148
+ validator: (v) => ['inline', 'sm', 'full', 'card'].includes(v),
149
+ },
150
+ });
151
+
152
+ const hasSocialLinks = computed(() => {
153
+ return props.author.social && Object.keys(props.author.social).length > 0;
154
+ });
155
+
156
+ const getSocialIcon = (platform) => {
157
+ const icons = {
158
+ twitter: 'i-lucide-twitter',
159
+ x: 'i-lucide-twitter',
160
+ linkedin: 'i-lucide-linkedin',
161
+ github: 'i-lucide-github',
162
+ website: 'i-lucide-globe',
163
+ instagram: 'i-lucide-instagram',
164
+ facebook: 'i-lucide-facebook',
165
+ youtube: 'i-lucide-youtube',
166
+ email: 'i-lucide-mail',
167
+ };
168
+ return icons[platform.toLowerCase()] || 'i-lucide-link';
169
+ };
170
+ </script>