@xenterprises/nuxt-x-marketing 1.0.1 → 1.0.3

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 (46) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.playground/app.vue +12 -6
  3. package/.playground/nuxt.config.ts +12 -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/nuxt.config.ts +1 -11
  45. package/package.json +2 -1
  46. package/screenshots/playground-home.png +0 -0
@@ -0,0 +1,191 @@
1
+ <template>
2
+ <div class="faq-container">
3
+ <!-- Accordion Layout -->
4
+ <div
5
+ v-if="layout === 'accordion'"
6
+ class="border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden"
7
+ >
8
+ <div
9
+ v-for="(item, index) in items"
10
+ :key="index"
11
+ class="faq-item border-b border-neutral-200 dark:border-neutral-800 last:border-b-0"
12
+ >
13
+ <!-- Question -->
14
+ <button
15
+ :aria-expanded="openIndexes.includes(index)"
16
+ class="faq-question w-full flex items-center justify-between p-5 text-left hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors duration-200"
17
+ @click="toggleItem(index)"
18
+ >
19
+ <span class="font-medium text-neutral-900 dark:text-white pr-4">
20
+ {{ item.question }}
21
+ </span>
22
+ <UIcon
23
+ name="i-lucide-chevron-down"
24
+ class="w-5 h-5 text-neutral-500 dark:text-neutral-400 flex-shrink-0 transition-transform duration-300"
25
+ :class="{ 'rotate-180': openIndexes.includes(index) }"
26
+ />
27
+ </button>
28
+
29
+ <!-- Answer -->
30
+ <div
31
+ ref="answerRefs"
32
+ class="faq-answer overflow-hidden transition-all duration-300 ease-in-out"
33
+ :style="{
34
+ maxHeight: openIndexes.includes(index)
35
+ ? answerHeights[index] + 'px'
36
+ : '0',
37
+ opacity: openIndexes.includes(index) ? 1 : 0,
38
+ }"
39
+ >
40
+ <div
41
+ class="p-5 pt-0 text-neutral-600 dark:text-neutral-400 prose prose-neutral dark:prose-invert max-w-none"
42
+ >
43
+ {{ item.answer }}
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+
49
+ <!-- Two-Column Layout -->
50
+ <div
51
+ v-else-if="layout === 'two-column'"
52
+ class="grid md:grid-cols-2 gap-x-12 gap-y-8"
53
+ >
54
+ <!-- Questions Column -->
55
+ <div class="questions-column space-y-4">
56
+ <div
57
+ v-for="(item, index) in items"
58
+ :key="index"
59
+ class="font-medium text-neutral-900 dark:text-white"
60
+ >
61
+ {{ item.question }}
62
+ </div>
63
+ </div>
64
+
65
+ <!-- Answers Column -->
66
+ <div class="answers-column space-y-4">
67
+ <div
68
+ v-for="(item, index) in items"
69
+ :key="index"
70
+ class="text-neutral-600 dark:text-neutral-400"
71
+ >
72
+ {{ item.answer }}
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </template>
78
+
79
+ <script setup>
80
+ const props = defineProps({
81
+ /** Array of FAQ items [{ question, answer, open? }] */
82
+ items: {
83
+ type: Array,
84
+ default: () => [
85
+ {
86
+ question: "What is this?",
87
+ answer: "This is a frequently asked question.",
88
+ },
89
+ ],
90
+ },
91
+ /** Layout type: 'accordion' or 'two-column' */
92
+ layout: {
93
+ type: String,
94
+ default: "accordion",
95
+ validator: (value) => ["accordion", "two-column"].includes(value),
96
+ },
97
+ /** Whether the first item should start open */
98
+ openFirst: {
99
+ type: Boolean,
100
+ default: false,
101
+ },
102
+ /** Whether multiple items can be open at once */
103
+ allowMultiple: {
104
+ type: Boolean,
105
+ default: false,
106
+ },
107
+ });
108
+
109
+ // Track which items are open
110
+ const openIndexes = ref([]);
111
+
112
+ // Track answer heights for smooth transitions
113
+ const answerHeights = ref({});
114
+
115
+ // Initialize open state based on props
116
+ onMounted(() => {
117
+ // Set initial open indexes
118
+ const initialOpen = [];
119
+ props.items.forEach((item, index) => {
120
+ if (item.open || (props.openFirst && index === 0)) {
121
+ initialOpen.push(index);
122
+ }
123
+ });
124
+ openIndexes.value = initialOpen;
125
+
126
+ // Calculate initial heights after DOM update
127
+ nextTick(() => {
128
+ calculateHeights();
129
+ });
130
+ });
131
+
132
+ // Recalculate heights on window resize
133
+ onMounted(() => {
134
+ if (typeof window !== "undefined") {
135
+ window.addEventListener("resize", calculateHeights);
136
+ }
137
+ });
138
+
139
+ onUnmounted(() => {
140
+ if (typeof window !== "undefined") {
141
+ window.removeEventListener("resize", calculateHeights);
142
+ }
143
+ });
144
+
145
+ // Watch for items changes to recalculate heights
146
+ watch(
147
+ () => props.items,
148
+ () => {
149
+ nextTick(() => {
150
+ calculateHeights();
151
+ });
152
+ },
153
+ { deep: true }
154
+ );
155
+
156
+ function calculateHeights() {
157
+ // Need to access DOM elements - we'll estimate based on content
158
+ // This is a simplified approach - in production, you might measure actual heights
159
+ props.items.forEach((item, index) => {
160
+ // Estimate height based on answer length (rough approximation)
161
+ const answerLength = item.answer?.length || 0;
162
+ // Average characters per line ~50, line height ~1.6, padding ~20px
163
+ const lines = Math.ceil(answerLength / 50);
164
+ const estimatedHeight = lines * 28 + 40; // ~28px per line + padding
165
+ answerHeights.value[index] = Math.max(estimatedHeight, 100);
166
+ });
167
+ }
168
+
169
+ function toggleItem(index) {
170
+ const isOpen = openIndexes.value.includes(index);
171
+
172
+ if (isOpen) {
173
+ // Close the item
174
+ openIndexes.value = openIndexes.value.filter((i) => i !== index);
175
+ } else {
176
+ // Open the item
177
+ if (props.allowMultiple) {
178
+ // Allow multiple - just add to open indexes
179
+ openIndexes.value = [...openIndexes.value, index];
180
+ } else {
181
+ // Only one at a time - close others and open this one
182
+ openIndexes.value = [index];
183
+ }
184
+ }
185
+
186
+ // Recalculate heights after toggle
187
+ nextTick(() => {
188
+ calculateHeights();
189
+ });
190
+ }
191
+ </script>
@@ -14,10 +14,7 @@
14
14
  v-if="feature.icon"
15
15
  class="w-12 h-12 rounded-xl bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center mb-4 transition-transform duration-300 group-hover:scale-110"
16
16
  >
17
- <UIcon
18
- :name="feature.icon"
19
- class="w-6 h-6 text-primary-500"
20
- />
17
+ <UIcon :name="feature.icon" class="w-6 h-6 text-primary-500" />
21
18
  </div>
22
19
 
23
20
  <!-- Image -->
@@ -45,12 +42,23 @@
45
42
  <!-- Link -->
46
43
  <NuxtLink
47
44
  v-if="feature.link"
48
- :to="typeof feature.link === 'string' ? feature.link : feature.link.to"
49
- :target="typeof feature.link === 'object' ? feature.link.target : undefined"
45
+ :to="
46
+ typeof feature.link === 'string' ? feature.link : feature.link.to
47
+ "
48
+ :target="
49
+ typeof feature.link === 'object' ? feature.link.target : undefined
50
+ "
50
51
  class="inline-flex items-center gap-1 mt-4 text-primary-500 hover:text-primary-600 font-medium text-sm transition-colors group/link"
51
52
  >
52
- {{ typeof feature.link === 'object' && feature.link.label ? feature.link.label : 'Learn more' }}
53
- <UIcon name="i-lucide-arrow-right" class="w-4 h-4 transition-transform duration-300 group-hover/link:translate-x-1" />
53
+ {{
54
+ typeof feature.link === "object" && feature.link.label
55
+ ? feature.link.label
56
+ : "Learn more"
57
+ }}
58
+ <UIcon
59
+ name="i-lucide-arrow-right"
60
+ class="w-4 h-4 transition-transform duration-300 group-hover/link:translate-x-1"
61
+ />
54
62
  </NuxtLink>
55
63
  </article>
56
64
  </div>
@@ -70,10 +78,7 @@
70
78
  v-if="feature.icon"
71
79
  class="flex-shrink-0 w-12 h-12 rounded-xl bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center transition-transform duration-300 group-hover:scale-110"
72
80
  >
73
- <UIcon
74
- :name="feature.icon"
75
- class="w-6 h-6 text-primary-500"
76
- />
81
+ <UIcon :name="feature.icon" class="w-6 h-6 text-primary-500" />
77
82
  </div>
78
83
 
79
84
  <!-- Content -->
@@ -86,12 +91,27 @@
86
91
  </p>
87
92
  <NuxtLink
88
93
  v-if="feature.link"
89
- :to="typeof feature.link === 'string' ? feature.link : feature.link.to"
90
- :target="typeof feature.link === 'object' ? feature.link.target : undefined"
94
+ :to="
95
+ typeof feature.link === 'string'
96
+ ? feature.link
97
+ : feature.link.to
98
+ "
99
+ :target="
100
+ typeof feature.link === 'object'
101
+ ? feature.link.target
102
+ : undefined
103
+ "
91
104
  class="inline-flex items-center gap-1 mt-4 text-primary-500 hover:text-primary-600 font-medium text-sm transition-colors group/link"
92
105
  >
93
- {{ typeof feature.link === 'object' && feature.link.label ? feature.link.label : 'Learn more' }}
94
- <UIcon name="i-lucide-arrow-right" class="w-4 h-4 transition-transform duration-300 group-hover/link:translate-x-1" />
106
+ {{
107
+ typeof feature.link === "object" && feature.link.label
108
+ ? feature.link.label
109
+ : "Learn more"
110
+ }}
111
+ <UIcon
112
+ name="i-lucide-arrow-right"
113
+ class="w-4 h-4 transition-transform duration-300 group-hover/link:translate-x-1"
114
+ />
95
115
  </NuxtLink>
96
116
  </div>
97
117
  </article>
@@ -127,26 +147,42 @@
127
147
  v-if="feature.icon"
128
148
  class="w-14 h-14 rounded-xl bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center mb-6 transition-transform duration-300 group-hover:scale-110"
129
149
  >
130
- <UIcon
131
- :name="feature.icon"
132
- class="w-7 h-7 text-primary-500"
133
- />
150
+ <UIcon :name="feature.icon" class="w-7 h-7 text-primary-500" />
134
151
  </div>
135
152
 
136
- <h3 class="xText-headline text-neutral-900 dark:text-white xText-balance">
153
+ <h3
154
+ class="xText-headline text-neutral-900 dark:text-white xText-balance"
155
+ >
137
156
  {{ feature.title }}
138
157
  </h3>
139
- <p class="mt-4 text-lg text-neutral-600 dark:text-neutral-400 leading-relaxed">
158
+ <p
159
+ class="mt-4 text-lg text-neutral-600 dark:text-neutral-400 leading-relaxed"
160
+ >
140
161
  {{ feature.description }}
141
162
  </p>
142
163
  <NuxtLink
143
164
  v-if="feature.link"
144
- :to="typeof feature.link === 'string' ? feature.link : feature.link.to"
145
- :target="typeof feature.link === 'object' ? feature.link.target : undefined"
165
+ :to="
166
+ typeof feature.link === 'string'
167
+ ? feature.link
168
+ : feature.link.to
169
+ "
170
+ :target="
171
+ typeof feature.link === 'object'
172
+ ? feature.link.target
173
+ : undefined
174
+ "
146
175
  class="inline-flex items-center gap-2 mt-6 text-primary-500 hover:text-primary-600 font-medium transition-colors group/link"
147
176
  >
148
- {{ typeof feature.link === 'object' && feature.link.label ? feature.link.label : 'Learn more' }}
149
- <UIcon name="i-lucide-arrow-right" class="w-5 h-5 transition-transform duration-300 group-hover/link:translate-x-1" />
177
+ {{
178
+ typeof feature.link === "object" && feature.link.label
179
+ ? feature.link.label
180
+ : "Learn more"
181
+ }}
182
+ <UIcon
183
+ name="i-lucide-arrow-right"
184
+ class="w-5 h-5 transition-transform duration-300 group-hover/link:translate-x-1"
185
+ />
150
186
  </NuxtLink>
151
187
  </div>
152
188
  </article>
@@ -157,19 +193,35 @@
157
193
 
158
194
  <script setup>
159
195
  // Initialize scroll reveal animations automatically
160
- useScrollReveal()
196
+ useScrollReveal();
161
197
 
162
198
  const props = defineProps({
163
- /** Array of features */
199
+ /** Array of features [{ icon?, title, description?, img?, link? }] */
164
200
  features: {
165
201
  type: Array,
166
- required: true,
202
+ default: () => [
203
+ {
204
+ icon: "i-lucide-zap",
205
+ title: "Lightning Fast",
206
+ description: "Built for performance from the ground up.",
207
+ },
208
+ {
209
+ icon: "i-lucide-shield",
210
+ title: "Secure by Default",
211
+ description: "Enterprise-grade security out of the box.",
212
+ },
213
+ {
214
+ icon: "i-lucide-layers",
215
+ title: "Fully Customizable",
216
+ description: "Adapt every component to match your brand.",
217
+ },
218
+ ],
167
219
  },
168
220
  /** Display layout */
169
221
  layout: {
170
222
  type: String,
171
- default: 'grid',
172
- validator: (v) => ['grid', 'list', 'alternating'].includes(v),
223
+ default: "grid",
224
+ validator: (v) => ["grid", "list", "alternating"].includes(v),
173
225
  },
174
226
  /** Grid columns (for grid layout) */
175
227
  columns: {
@@ -180,14 +232,14 @@ const props = defineProps({
180
232
  });
181
233
 
182
234
  const containerClasses = computed(() => {
183
- return props.layout === 'alternating' ? 'max-w-6xl mx-auto' : '';
235
+ return props.layout === "alternating" ? "max-w-6xl mx-auto" : "";
184
236
  });
185
237
 
186
238
  const gridClasses = computed(() => {
187
239
  const cols = {
188
- 2: 'grid md:grid-cols-2 gap-8',
189
- 3: 'grid md:grid-cols-2 lg:grid-cols-3 gap-8',
190
- 4: 'grid md:grid-cols-2 lg:grid-cols-4 gap-8',
240
+ 2: "grid md:grid-cols-2 gap-8",
241
+ 3: "grid md:grid-cols-2 lg:grid-cols-3 gap-8",
242
+ 4: "grid md:grid-cols-2 lg:grid-cols-4 gap-8",
191
243
  };
192
244
  return cols[props.columns];
193
245
  });
@@ -4,7 +4,7 @@
4
4
  :class="[
5
5
  alignmentClasses,
6
6
  verticalAlignClasses,
7
- { 'pt-20': includeNavPadding }
7
+ { 'pt-20': includeNavPadding },
8
8
  ]"
9
9
  >
10
10
  <!-- Background -->
@@ -44,15 +44,9 @@
44
44
 
45
45
  <!-- Content -->
46
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
- >
47
+ <div class="max-w-3xl" :class="contentAlignmentClasses">
51
48
  <!-- Eyebrow -->
52
- <p
53
- v-if="eyebrow"
54
- class="xText-eyebrow text-white/70 mb-4 xFadeUp"
55
- >
49
+ <p v-if="eyebrow" class="xText-eyebrow text-white/70 mb-4 xFadeUp">
56
50
  {{ eyebrow }}
57
51
  </p>
58
52
 
@@ -77,7 +71,10 @@
77
71
  <div
78
72
  v-if="buttons.length || $slots.actions"
79
73
  class="mt-10 flex flex-wrap gap-4 xFadeUp"
80
- :class="{ 'justify-center': align === 'center', 'justify-end': align === 'right' }"
74
+ :class="{
75
+ 'justify-center': align === 'center',
76
+ 'justify-end': align === 'right',
77
+ }"
81
78
  style="transition-delay: 300ms"
82
79
  >
83
80
  <slot name="actions">
@@ -92,7 +89,7 @@
92
89
  :class="[
93
90
  btn.variant === 'outline' || (index > 0 && !btn.variant)
94
91
  ? 'bg-white/10 backdrop-blur-sm border-white/40 text-white hover:bg-white/20 hover:border-white/60'
95
- : ''
92
+ : '',
96
93
  ]"
97
94
  >
98
95
  {{ btn.label }}
@@ -107,9 +104,15 @@
107
104
  v-if="hasScrollIndicator"
108
105
  class="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2"
109
106
  >
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]" />
107
+ <span class="text-xs text-white/50 uppercase tracking-widest"
108
+ >Scroll</span
109
+ >
110
+ <div
111
+ class="w-6 h-10 border-2 border-white/30 rounded-full flex justify-center pt-2"
112
+ >
113
+ <div
114
+ class="w-1.5 h-1.5 bg-white/60 rounded-full animate-[scroll-indicator_2s_ease-in-out_infinite]"
115
+ />
113
116
  </div>
114
117
  </div>
115
118
  </section>
@@ -121,52 +124,52 @@ const props = defineProps({
121
124
  img: {
122
125
  type: Object,
123
126
  default: null,
124
- validator: (v) => !v || typeof v.src === 'string',
127
+ validator: (v) => !v || typeof v.src === "string",
125
128
  },
126
129
  /** Background video object { src: string, poster?: string } */
127
130
  video: {
128
131
  type: Object,
129
132
  default: null,
130
- validator: (v) => !v || typeof v.src === 'string',
133
+ validator: (v) => !v || typeof v.src === "string",
131
134
  },
132
135
  /** Overlay style */
133
136
  overlay: {
134
137
  type: String,
135
- default: 'gradient',
136
- validator: (v) => ['light', 'heavy', 'gradient', 'none'].includes(v),
138
+ default: "gradient",
139
+ validator: (v) => ["light", "heavy", "gradient", "none"].includes(v),
137
140
  },
138
141
  /** Small text above title */
139
142
  eyebrow: {
140
143
  type: String,
141
- default: '',
144
+ default: "",
142
145
  },
143
146
  /** Main headline */
144
147
  title: {
145
148
  type: String,
146
- required: true,
149
+ default: "Your Headline Here",
147
150
  },
148
151
  /** Supporting text */
149
152
  subtitle: {
150
153
  type: String,
151
- default: '',
154
+ default: "",
152
155
  },
153
156
  /** Action buttons array [{ label, color?, variant?, to?, icon? }] */
154
157
  buttons: {
155
158
  type: Array,
156
159
  default: () => [],
157
- validator: (v) => v.every(btn => btn.label),
160
+ validator: (v) => v.every((btn) => btn.label),
158
161
  },
159
162
  /** Content horizontal alignment */
160
163
  align: {
161
164
  type: String,
162
- default: 'left',
163
- validator: (v) => ['left', 'center', 'right'].includes(v),
165
+ default: "left",
166
+ validator: (v) => ["left", "center", "right"].includes(v),
164
167
  },
165
168
  /** Content vertical position */
166
169
  verticalAlign: {
167
170
  type: String,
168
- default: 'center',
169
- validator: (v) => ['center', 'bottom'].includes(v),
171
+ default: "center",
172
+ validator: (v) => ["center", "bottom"].includes(v),
170
173
  },
171
174
  /** Has scroll indicator arrow */
172
175
  hasScrollIndicator: {
@@ -182,35 +185,35 @@ const props = defineProps({
182
185
 
183
186
  const overlayClass = computed(() => {
184
187
  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
+ light: "bg-black/30",
189
+ heavy: "bg-black/60",
190
+ gradient: "bg-gradient-to-t from-black/80 via-black/40 to-transparent",
188
191
  };
189
- return overlays[props.overlay] || '';
192
+ return overlays[props.overlay] || "";
190
193
  });
191
194
 
192
195
  const alignmentClasses = computed(() => {
193
196
  const aligns = {
194
- left: 'text-left',
195
- center: 'text-center',
196
- right: 'text-right',
197
+ left: "text-left",
198
+ center: "text-center",
199
+ right: "text-right",
197
200
  };
198
201
  return aligns[props.align];
199
202
  });
200
203
 
201
204
  const verticalAlignClasses = computed(() => {
202
205
  const aligns = {
203
- center: 'items-center',
204
- bottom: 'items-end pb-24',
206
+ center: "items-center",
207
+ bottom: "items-end pb-24",
205
208
  };
206
209
  return aligns[props.verticalAlign];
207
210
  });
208
211
 
209
212
  const contentAlignmentClasses = computed(() => {
210
213
  const aligns = {
211
- left: '',
212
- center: 'mx-auto',
213
- right: 'ml-auto',
214
+ left: "",
215
+ center: "mx-auto",
216
+ right: "ml-auto",
214
217
  };
215
218
  return aligns[props.align];
216
219
  });
@@ -1,20 +1,10 @@
1
1
  <template>
2
2
  <Transition name="slide-down">
3
- <div
4
- v-if="visible"
5
- :class="[
6
- 'relative z-50',
7
- bgClasses
8
- ]"
9
- >
3
+ <div v-if="visible" :class="['relative z-50', bgClasses]">
10
4
  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2.5">
11
5
  <div class="flex items-center justify-center gap-x-4 text-sm">
12
6
  <!-- Icon -->
13
- <UIcon
14
- v-if="icon"
15
- :name="icon"
16
- class="w-5 h-5 flex-shrink-0"
17
- />
7
+ <UIcon v-if="icon" :name="icon" class="w-5 h-5 flex-shrink-0" />
18
8
 
19
9
  <!-- Message -->
20
10
  <p class="text-center">
@@ -52,33 +42,34 @@ const props = defineProps({
52
42
  /** Main message text */
53
43
  message: {
54
44
  type: String,
55
- required: true,
45
+ default: "🎉 New feature available — check it out!",
56
46
  },
57
47
  /** Optional badge/label before message */
58
48
  badge: {
59
49
  type: String,
60
- default: '',
50
+ default: "",
61
51
  },
62
52
  /** Icon name to display */
63
53
  icon: {
64
54
  type: String,
65
- default: '',
55
+ default: "",
66
56
  },
67
57
  /** CTA link URL */
68
58
  link: {
69
59
  type: String,
70
- default: '',
60
+ default: "",
71
61
  },
72
62
  /** CTA link text */
73
63
  linkText: {
74
64
  type: String,
75
- default: 'Learn more',
65
+ default: "Learn more",
76
66
  },
77
67
  /** Color variant */
78
68
  variant: {
79
69
  type: String,
80
- default: 'primary',
81
- validator: (v) => ['primary', 'neutral', 'success', 'warning', 'error'].includes(v),
70
+ default: "primary",
71
+ validator: (v) =>
72
+ ["primary", "neutral", "success", "warning", "error"].includes(v),
82
73
  },
83
74
  /** Allow dismissing the bar */
84
75
  dismissible: {
@@ -88,11 +79,11 @@ const props = defineProps({
88
79
  /** Storage key for dismissed state */
89
80
  storageKey: {
90
81
  type: String,
91
- default: 'announcement-dismissed',
82
+ default: "announcement-dismissed",
92
83
  },
93
84
  });
94
85
 
95
- const emit = defineEmits(['dismiss']);
86
+ const emit = defineEmits(["dismiss"]);
96
87
 
97
88
  const visible = ref(true);
98
89
 
@@ -107,19 +98,20 @@ onMounted(() => {
107
98
 
108
99
  const dismiss = () => {
109
100
  if (props.storageKey) {
110
- localStorage.setItem(props.storageKey, 'true');
101
+ localStorage.setItem(props.storageKey, "true");
111
102
  }
112
103
  visible.value = false;
113
- emit('dismiss');
104
+ emit("dismiss");
114
105
  };
115
106
 
116
107
  const bgClasses = computed(() => {
117
108
  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',
109
+ primary: "bg-primary-500 text-white",
110
+ neutral:
111
+ "bg-neutral-900 dark:bg-neutral-100 text-white dark:text-neutral-900",
112
+ success: "bg-green-500 text-white",
113
+ warning: "bg-yellow-500 text-neutral-900",
114
+ error: "bg-red-500 text-white",
123
115
  };
124
116
  return variants[props.variant];
125
117
  });