@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,226 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <Transition
4
+ enter-active-class="transition-opacity duration-300"
5
+ enter-from-class="opacity-0"
6
+ leave-active-class="transition-opacity duration-200"
7
+ leave-to-class="opacity-0"
8
+ >
9
+ <div
10
+ v-if="isOpen"
11
+ class="fixed inset-0 z-50 flex items-center justify-center"
12
+ @click="close"
13
+ @keydown.escape="close"
14
+ >
15
+ <!-- Backdrop -->
16
+ <div class="absolute inset-0 bg-black/90" />
17
+
18
+ <!-- Close Button -->
19
+ <button
20
+ class="absolute top-4 right-4 w-10 h-10 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors z-10"
21
+ @click.stop="close"
22
+ >
23
+ <UIcon name="i-lucide-x" class="w-6 h-6" />
24
+ <span class="sr-only">Close</span>
25
+ </button>
26
+
27
+ <!-- Navigation (for galleries) -->
28
+ <template v-if="images.length > 1">
29
+ <button
30
+ class="absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors z-10"
31
+ @click.stop="prev"
32
+ >
33
+ <UIcon name="i-lucide-chevron-left" class="w-6 h-6" />
34
+ <span class="sr-only">Previous</span>
35
+ </button>
36
+ <button
37
+ class="absolute right-4 top-1/2 -translate-y-1/2 w-12 h-12 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors z-10"
38
+ @click.stop="next"
39
+ >
40
+ <UIcon name="i-lucide-chevron-right" class="w-6 h-6" />
41
+ <span class="sr-only">Next</span>
42
+ </button>
43
+ </template>
44
+
45
+ <!-- Image Container -->
46
+ <div
47
+ class="relative max-w-[90vw] max-h-[90vh]"
48
+ @click.stop
49
+ >
50
+ <Transition
51
+ :name="slideDirection"
52
+ mode="out-in"
53
+ >
54
+ <figure :key="currentIndex" class="flex flex-col items-center">
55
+ <img
56
+ :src="currentImage.src"
57
+ :alt="currentImage.alt"
58
+ class="max-w-full max-h-[80vh] object-contain rounded-lg"
59
+ @load="loading = false"
60
+ />
61
+ <figcaption
62
+ v-if="currentImage.caption"
63
+ class="mt-4 text-center text-white/80 text-sm max-w-lg"
64
+ >
65
+ {{ currentImage.caption }}
66
+ </figcaption>
67
+ </figure>
68
+ </Transition>
69
+
70
+ <!-- Loading indicator -->
71
+ <div
72
+ v-if="loading"
73
+ class="absolute inset-0 flex items-center justify-center"
74
+ >
75
+ <UIcon name="i-lucide-loader-2" class="w-8 h-8 text-white animate-spin" />
76
+ </div>
77
+ </div>
78
+
79
+ <!-- Counter -->
80
+ <div
81
+ v-if="images.length > 1 && hasCounter"
82
+ class="absolute bottom-4 left-1/2 -translate-x-1/2 px-3 py-1.5 rounded-full bg-white/10 text-white text-sm"
83
+ >
84
+ {{ currentIndex + 1 }} / {{ images.length }}
85
+ </div>
86
+
87
+ <!-- Thumbnails -->
88
+ <div
89
+ v-if="images.length > 1 && hasThumbnails"
90
+ class="absolute bottom-16 left-1/2 -translate-x-1/2 flex gap-2"
91
+ >
92
+ <button
93
+ v-for="(image, index) in images"
94
+ :key="index"
95
+ class="w-12 h-12 rounded-lg overflow-hidden border-2 transition-all"
96
+ :class="index === currentIndex ? 'border-white' : 'border-transparent opacity-50 hover:opacity-75'"
97
+ @click.stop="currentIndex = index"
98
+ >
99
+ <img
100
+ :src="image.src"
101
+ :alt="image.alt"
102
+ class="w-full h-full object-cover"
103
+ />
104
+ </button>
105
+ </div>
106
+ </div>
107
+ </Transition>
108
+ </Teleport>
109
+ </template>
110
+
111
+ <script setup>
112
+ const props = defineProps({
113
+ /** v-model for lightbox visibility */
114
+ modelValue: {
115
+ type: Boolean,
116
+ default: false,
117
+ },
118
+ /** Array of images or single image */
119
+ images: {
120
+ type: Array,
121
+ default: () => [],
122
+ // Each image: { src, alt, caption? }
123
+ },
124
+ /** Starting index for galleries */
125
+ startIndex: {
126
+ type: Number,
127
+ default: 0,
128
+ },
129
+ /** Show image counter */
130
+ hasCounter: {
131
+ type: Boolean,
132
+ default: true,
133
+ },
134
+ /** Show thumbnail navigation */
135
+ hasThumbnails: {
136
+ type: Boolean,
137
+ default: false,
138
+ },
139
+ });
140
+
141
+ const emit = defineEmits(['update:modelValue', 'change']);
142
+
143
+ const currentIndex = ref(props.startIndex);
144
+ const loading = ref(false);
145
+ const slideDirection = ref('slide-left');
146
+
147
+ const isOpen = computed({
148
+ get: () => props.modelValue,
149
+ set: (value) => emit('update:modelValue', value),
150
+ });
151
+
152
+ const currentImage = computed(() => {
153
+ return props.images[currentIndex.value] || { src: '', alt: '' };
154
+ });
155
+
156
+ const close = () => {
157
+ isOpen.value = false;
158
+ };
159
+
160
+ const next = () => {
161
+ loading.value = true;
162
+ slideDirection.value = 'slide-left';
163
+ currentIndex.value = (currentIndex.value + 1) % props.images.length;
164
+ emit('change', currentIndex.value);
165
+ };
166
+
167
+ const prev = () => {
168
+ loading.value = true;
169
+ slideDirection.value = 'slide-right';
170
+ currentIndex.value = (currentIndex.value - 1 + props.images.length) % props.images.length;
171
+ emit('change', currentIndex.value);
172
+ };
173
+
174
+ // Keyboard navigation
175
+ const handleKeydown = (e) => {
176
+ if (!isOpen.value) return;
177
+
178
+ if (e.key === 'ArrowRight') next();
179
+ if (e.key === 'ArrowLeft') prev();
180
+ if (e.key === 'Escape') close();
181
+ };
182
+
183
+ onMounted(() => {
184
+ window.addEventListener('keydown', handleKeydown);
185
+ });
186
+
187
+ onUnmounted(() => {
188
+ window.removeEventListener('keydown', handleKeydown);
189
+ });
190
+
191
+ // Reset to startIndex when opening
192
+ watch(isOpen, (open) => {
193
+ if (open) {
194
+ currentIndex.value = props.startIndex;
195
+ }
196
+ });
197
+ </script>
198
+
199
+ <style scoped>
200
+ .slide-left-enter-active,
201
+ .slide-left-leave-active,
202
+ .slide-right-enter-active,
203
+ .slide-right-leave-active {
204
+ transition: transform 0.3s ease, opacity 0.3s ease;
205
+ }
206
+
207
+ .slide-left-enter-from {
208
+ transform: translateX(20px);
209
+ opacity: 0;
210
+ }
211
+
212
+ .slide-left-leave-to {
213
+ transform: translateX(-20px);
214
+ opacity: 0;
215
+ }
216
+
217
+ .slide-right-enter-from {
218
+ transform: translateX(-20px);
219
+ opacity: 0;
220
+ }
221
+
222
+ .slide-right-leave-to {
223
+ transform: translateX(20px);
224
+ opacity: 0;
225
+ }
226
+ </style>
@@ -0,0 +1,203 @@
1
+ <template>
2
+ <XMarkModal
3
+ ref="modalRef"
4
+ :title="title"
5
+ :description="description"
6
+ :size="size"
7
+ dark
8
+ @open="emit('open')"
9
+ @close="handleClose"
10
+ >
11
+ <!-- Trigger Slot -->
12
+ <slot>
13
+ <!-- Thumbnail Trigger -->
14
+ <button
15
+ v-if="thumbnail"
16
+ class="relative group rounded-xl overflow-hidden cursor-pointer"
17
+ >
18
+ <img
19
+ :src="thumbnail"
20
+ :alt="title || 'Video thumbnail'"
21
+ class="w-full aspect-video object-cover transition-transform group-hover:scale-105"
22
+ />
23
+ <div class="absolute inset-0 bg-black/40 flex items-center justify-center group-hover:bg-black/50 transition-colors">
24
+ <div class="w-16 h-16 rounded-full bg-white/90 flex items-center justify-center shadow-lg">
25
+ <UIcon name="i-lucide-play" class="w-8 h-8 text-neutral-900 ml-1" />
26
+ </div>
27
+ </div>
28
+ </button>
29
+
30
+ <!-- Button Trigger -->
31
+ <UButton
32
+ v-else
33
+ :label="buttonProps.label"
34
+ :color="buttonProps.color"
35
+ :variant="buttonProps.variant"
36
+ :size="buttonProps.size"
37
+ :leading-icon="buttonProps.icon"
38
+ />
39
+ </slot>
40
+
41
+ <!-- Video Content -->
42
+ <template #content>
43
+ <div class="aspect-video bg-black overflow-hidden" :class="title || description ? 'rounded-t-lg' : 'rounded-lg'">
44
+ <!-- YouTube Embed -->
45
+ <iframe
46
+ v-if="modalRef?.isOpen && embedType === 'youtube'"
47
+ :src="embedUrl"
48
+ class="w-full h-full"
49
+ frameborder="0"
50
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
51
+ allowfullscreen
52
+ />
53
+
54
+ <!-- Vimeo Embed -->
55
+ <iframe
56
+ v-else-if="modalRef?.isOpen && embedType === 'vimeo'"
57
+ :src="embedUrl"
58
+ class="w-full h-full"
59
+ frameborder="0"
60
+ allow="autoplay; fullscreen; picture-in-picture"
61
+ allowfullscreen
62
+ />
63
+
64
+ <!-- Direct Video -->
65
+ <video
66
+ v-else-if="modalRef?.isOpen"
67
+ ref="videoRef"
68
+ :src="src"
69
+ :poster="poster"
70
+ class="w-full h-full object-contain"
71
+ controls
72
+ :autoplay="autoplay"
73
+ >
74
+ <track
75
+ v-if="captions"
76
+ kind="captions"
77
+ :src="captions"
78
+ :srclang="captionsLang"
79
+ :label="captionsLabel"
80
+ />
81
+ </video>
82
+ </div>
83
+ </template>
84
+ </XMarkModal>
85
+ </template>
86
+
87
+ <script setup>
88
+ const props = defineProps({
89
+ /** Video source (direct URL, YouTube, or Vimeo) */
90
+ src: {
91
+ type: String,
92
+ required: true,
93
+ },
94
+ /** Thumbnail image URL (shows image with play button) */
95
+ thumbnail: {
96
+ type: String,
97
+ default: '',
98
+ },
99
+ /** Poster image for direct videos */
100
+ poster: {
101
+ type: String,
102
+ default: '',
103
+ },
104
+ /** Video title (shown in modal footer) */
105
+ title: {
106
+ type: String,
107
+ default: '',
108
+ },
109
+ /** Video description (shown in modal footer) */
110
+ description: {
111
+ type: String,
112
+ default: '',
113
+ },
114
+ /** Modal size */
115
+ size: {
116
+ type: String,
117
+ default: '4xl',
118
+ validator: (v) => ['lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl', 'full'].includes(v),
119
+ },
120
+ /** Autoplay video when modal opens */
121
+ autoplay: {
122
+ type: Boolean,
123
+ default: true,
124
+ },
125
+ /** Button configuration object */
126
+ button: {
127
+ type: Object,
128
+ default: () => ({}),
129
+ },
130
+ /** Captions/subtitles file URL */
131
+ captions: {
132
+ type: String,
133
+ default: '',
134
+ },
135
+ /** Captions language code */
136
+ captionsLang: {
137
+ type: String,
138
+ default: 'en',
139
+ },
140
+ /** Captions label */
141
+ captionsLabel: {
142
+ type: String,
143
+ default: 'English',
144
+ },
145
+ });
146
+
147
+ const emit = defineEmits(['open', 'close']);
148
+
149
+ const modalRef = ref(null);
150
+ const videoRef = ref(null);
151
+
152
+ // Button props with defaults
153
+ const buttonProps = computed(() => ({
154
+ label: props.button.label || 'Watch Video',
155
+ color: props.button.color || 'primary',
156
+ variant: props.button.variant || 'solid',
157
+ size: props.button.size || 'md',
158
+ icon: props.button.icon || 'i-lucide-play',
159
+ }));
160
+
161
+ // Detect embed type from URL
162
+ const embedType = computed(() => {
163
+ if (props.src.includes('youtube.com') || props.src.includes('youtu.be')) {
164
+ return 'youtube';
165
+ }
166
+ if (props.src.includes('vimeo.com')) {
167
+ return 'vimeo';
168
+ }
169
+ return 'direct';
170
+ });
171
+
172
+ // Generate embed URL
173
+ const embedUrl = computed(() => {
174
+ const autoplayParam = props.autoplay ? '1' : '0';
175
+
176
+ if (embedType.value === 'youtube') {
177
+ let videoId = '';
178
+ if (props.src.includes('youtu.be/')) {
179
+ videoId = props.src.split('youtu.be/')[1]?.split('?')[0];
180
+ } else if (props.src.includes('v=')) {
181
+ videoId = props.src.split('v=')[1]?.split('&')[0];
182
+ } else if (props.src.includes('embed/')) {
183
+ videoId = props.src.split('embed/')[1]?.split('?')[0];
184
+ }
185
+ return `https://www.youtube.com/embed/${videoId}?autoplay=${autoplayParam}&rel=0`;
186
+ }
187
+
188
+ if (embedType.value === 'vimeo') {
189
+ const videoId = props.src.split('vimeo.com/')[1]?.split('?')[0];
190
+ return `https://player.vimeo.com/video/${videoId}?autoplay=${autoplayParam}`;
191
+ }
192
+
193
+ return props.src;
194
+ });
195
+
196
+ // Handle close - pause video
197
+ const handleClose = () => {
198
+ if (videoRef.value) {
199
+ videoRef.value.pause();
200
+ }
201
+ emit('close');
202
+ };
203
+ </script>
@@ -0,0 +1,133 @@
1
+ <template>
2
+ <UModal
3
+ v-model:open="isOpen"
4
+ :fullscreen="fullscreen"
5
+ :ui="modalUi"
6
+ >
7
+ <!-- Trigger Slot -->
8
+ <slot />
9
+
10
+ <!-- Modal Content -->
11
+ <template #body>
12
+ <div class="relative">
13
+ <!-- Close Button -->
14
+ <UButton
15
+ icon="i-lucide-x"
16
+ color="neutral"
17
+ variant="ghost"
18
+ :size="fullscreen ? 'lg' : 'md'"
19
+ class="absolute z-10 rounded-full"
20
+ :class="closeButtonClasses"
21
+ aria-label="Close"
22
+ @click="isOpen = false"
23
+ />
24
+
25
+ <!-- Content -->
26
+ <slot name="content" :close="close" />
27
+
28
+ <!-- Footer (optional) -->
29
+ <div v-if="$slots.footer || title || description" class="px-5 py-4 bg-neutral-900 rounded-b-lg">
30
+ <slot name="footer">
31
+ <h4 v-if="title" class="text-white font-medium">{{ title }}</h4>
32
+ <p v-if="description" class="mt-1 text-sm text-neutral-400">{{ description }}</p>
33
+ </slot>
34
+ </div>
35
+ </div>
36
+ </template>
37
+ </UModal>
38
+ </template>
39
+
40
+ <script setup>
41
+ const props = defineProps({
42
+ /** Modal title (shown in footer) */
43
+ title: {
44
+ type: String,
45
+ default: '',
46
+ },
47
+ /** Modal description (shown in footer) */
48
+ description: {
49
+ type: String,
50
+ default: '',
51
+ },
52
+ /** Modal size */
53
+ size: {
54
+ type: String,
55
+ default: '4xl',
56
+ validator: (v) => ['sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl', 'full'].includes(v),
57
+ },
58
+ /** Fullscreen mode */
59
+ fullscreen: {
60
+ type: Boolean,
61
+ default: false,
62
+ },
63
+ /** Dark mode for content (affects close button styling) */
64
+ dark: {
65
+ type: Boolean,
66
+ default: true,
67
+ },
68
+ /** Padding for body */
69
+ padding: {
70
+ type: String,
71
+ default: 'p-0',
72
+ },
73
+ });
74
+
75
+ const emit = defineEmits(['open', 'close']);
76
+
77
+ const isOpen = ref(false);
78
+
79
+ // Close function for slot
80
+ const close = () => {
81
+ isOpen.value = false;
82
+ };
83
+
84
+ // Size classes for content slot
85
+ const sizeClasses = computed(() => {
86
+ const sizes = {
87
+ sm: 'max-w-sm',
88
+ md: 'max-w-md',
89
+ lg: 'max-w-lg',
90
+ xl: 'max-w-xl',
91
+ '2xl': 'max-w-2xl',
92
+ '3xl': 'max-w-3xl',
93
+ '4xl': 'max-w-4xl',
94
+ '5xl': 'max-w-5xl',
95
+ '6xl': 'max-w-6xl',
96
+ '7xl': 'max-w-7xl',
97
+ full: 'max-w-[95vw]',
98
+ };
99
+ return sizes[props.size];
100
+ });
101
+
102
+ // Modal UI config
103
+ const modalUi = computed(() => ({
104
+ header: 'hidden',
105
+ body: `${props.padding} ${props.dark ? 'bg-black' : 'bg-white dark:bg-neutral-900'}`,
106
+ footer: 'hidden',
107
+ overlay: 'bg-black/80 backdrop-blur-sm',
108
+ content: `ring-neutral-200/50 dark:ring-neutral-800 ${props.fullscreen ? '' : sizeClasses.value}`,
109
+ }));
110
+
111
+ // Close button classes based on mode and fullscreen
112
+ const closeButtonClasses = computed(() => {
113
+ if (props.fullscreen) {
114
+ return 'fixed top-4 right-4 !bg-white/10 backdrop-blur-sm hover:!bg-white/20 !text-white';
115
+ }
116
+ if (props.dark) {
117
+ return 'top-3 right-3 !bg-black/50 hover:!bg-black/70 !text-white/80 hover:!text-white';
118
+ }
119
+ return 'top-3 right-3 !bg-neutral-100 hover:!bg-neutral-200 dark:!bg-neutral-800 dark:hover:!bg-neutral-700 !text-neutral-500 dark:!text-neutral-400';
120
+ });
121
+
122
+ // Emit events
123
+ watch(isOpen, (open) => {
124
+ if (open) {
125
+ emit('open');
126
+ } else {
127
+ emit('close');
128
+ }
129
+ });
130
+
131
+ // Expose close method
132
+ defineExpose({ close, isOpen });
133
+ </script>