@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
@@ -0,0 +1,306 @@
1
+ <template>
2
+ <article class="listing-detail">
3
+ <!-- Header -->
4
+ <header class="mb-10">
5
+ <div class="flex flex-col sm:flex-row sm:items-start gap-5">
6
+ <!-- Logo -->
7
+ <figure
8
+ v-if="listing.logo"
9
+ class="w-20 h-20 rounded-2xl overflow-hidden flex-shrink-0 border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900 shadow-sm"
10
+ >
11
+ <img
12
+ :src="listing.logo"
13
+ :alt="listing.name"
14
+ class="w-full h-full object-contain"
15
+ />
16
+ </figure>
17
+ <div
18
+ v-else
19
+ class="w-20 h-20 rounded-2xl flex-shrink-0 bg-primary-100 dark:bg-primary-900/40 flex items-center justify-center shadow-sm"
20
+ >
21
+ <span
22
+ class="text-primary-600 dark:text-primary-400 font-bold text-3xl"
23
+ >
24
+ {{ listing.name.charAt(0) }}
25
+ </span>
26
+ </div>
27
+
28
+ <!-- Name + Meta -->
29
+ <div class="flex-1 min-w-0">
30
+ <div class="flex flex-wrap items-center gap-2 mb-1">
31
+ <h1
32
+ class="text-2xl sm:text-3xl font-bold text-neutral-900 dark:text-white"
33
+ >
34
+ {{ listing.name }}
35
+ </h1>
36
+ <UBadge
37
+ v-if="listing.badge"
38
+ :label="listing.badge"
39
+ color="primary"
40
+ variant="solid"
41
+ size="sm"
42
+ />
43
+ </div>
44
+
45
+ <!-- Category + Rating -->
46
+ <div class="flex flex-wrap items-center gap-3 mt-2">
47
+ <UBadge
48
+ v-if="listing.category"
49
+ :label="listing.category"
50
+ variant="subtle"
51
+ color="neutral"
52
+ />
53
+
54
+ <div v-if="listing.rating" class="flex items-center gap-1.5">
55
+ <div class="flex items-center gap-0.5">
56
+ <UIcon
57
+ v-for="i in 5"
58
+ :key="i"
59
+ name="i-lucide-star"
60
+ class="w-4 h-4"
61
+ :class="
62
+ i <= Math.round(listing.rating)
63
+ ? 'text-yellow-400'
64
+ : 'text-neutral-300 dark:text-neutral-600'
65
+ "
66
+ />
67
+ </div>
68
+ <span
69
+ class="text-sm font-semibold text-neutral-700 dark:text-neutral-300"
70
+ >
71
+ {{ listing.rating.toFixed(1) }}
72
+ </span>
73
+ <span
74
+ v-if="listing.reviewCount"
75
+ class="text-sm text-neutral-500 dark:text-neutral-400"
76
+ >
77
+ ({{ listing.reviewCount }} reviews)
78
+ </span>
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Tags -->
83
+ <div v-if="listing.tags?.length" class="flex flex-wrap gap-1.5 mt-3">
84
+ <UBadge
85
+ v-for="tag in listing.tags"
86
+ :key="tag"
87
+ :label="tag"
88
+ variant="subtle"
89
+ color="neutral"
90
+ size="sm"
91
+ />
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </header>
96
+
97
+ <!-- Content Grid -->
98
+ <div
99
+ :class="[
100
+ 'grid gap-10 lg:gap-14',
101
+ hasSidebar ? 'lg:grid-cols-[1fr_300px]' : '',
102
+ ]"
103
+ >
104
+ <!-- Main Content -->
105
+ <div class="min-w-0">
106
+ <!-- Description -->
107
+ <section v-if="listing.description" class="mb-8">
108
+ <div class="prose prose-neutral dark:prose-invert max-w-none">
109
+ <p
110
+ class="text-lg text-neutral-700 dark:text-neutral-300 leading-relaxed"
111
+ >
112
+ {{ listing.description }}
113
+ </p>
114
+ </div>
115
+ </section>
116
+
117
+ <!-- After Description Slot -->
118
+ <slot name="after-description" />
119
+
120
+ <!-- Screenshots Gallery -->
121
+ <section
122
+ v-if="hasScreenshots && listing.screenshots?.length"
123
+ class="mb-10"
124
+ >
125
+ <h2
126
+ class="text-lg font-semibold text-neutral-900 dark:text-white mb-4"
127
+ >
128
+ Screenshots
129
+ </h2>
130
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
131
+ <figure
132
+ v-for="(screenshot, index) in listing.screenshots"
133
+ :key="index"
134
+ class="aspect-video rounded-xl overflow-hidden border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-900 cursor-pointer group"
135
+ >
136
+ <img
137
+ :src="
138
+ typeof screenshot === 'string' ? screenshot : screenshot.src
139
+ "
140
+ :alt="
141
+ typeof screenshot === 'string'
142
+ ? `Screenshot ${index + 1}`
143
+ : screenshot.alt || `Screenshot ${index + 1}`
144
+ "
145
+ class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
146
+ />
147
+ </figure>
148
+ </div>
149
+ </section>
150
+
151
+ <!-- Features List -->
152
+ <section v-if="listing.features?.length" class="mb-10">
153
+ <h2
154
+ class="text-lg font-semibold text-neutral-900 dark:text-white mb-4"
155
+ >
156
+ Features
157
+ </h2>
158
+ <ul class="grid sm:grid-cols-2 gap-3">
159
+ <li
160
+ v-for="(feature, index) in listing.features"
161
+ :key="index"
162
+ class="flex items-start gap-3"
163
+ >
164
+ <span
165
+ class="flex-shrink-0 w-5 h-5 rounded-full bg-primary-100 dark:bg-primary-900/40 flex items-center justify-center mt-0.5"
166
+ >
167
+ <UIcon
168
+ name="i-lucide-check"
169
+ class="w-3 h-3 text-primary-600 dark:text-primary-400"
170
+ />
171
+ </span>
172
+ <span class="text-sm text-neutral-700 dark:text-neutral-300">
173
+ {{ feature }}
174
+ </span>
175
+ </li>
176
+ </ul>
177
+ </section>
178
+
179
+ <!-- Reviews Slot -->
180
+ <section v-if="hasReviews">
181
+ <slot name="reviews" />
182
+ </section>
183
+ </div>
184
+
185
+ <!-- Sidebar -->
186
+ <aside v-if="hasSidebar" class="hidden lg:block">
187
+ <div class="sticky top-24 space-y-6">
188
+ <!-- Default Sidebar Content -->
189
+ <slot name="sidebar">
190
+ <!-- Website CTA -->
191
+ <div
192
+ class="p-5 rounded-xl bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200/60 dark:border-neutral-800"
193
+ >
194
+ <UButton
195
+ v-if="listing.website"
196
+ :to="listing.website"
197
+ label="Visit Website"
198
+ icon="i-lucide-external-link"
199
+ color="primary"
200
+ variant="solid"
201
+ block
202
+ target="_blank"
203
+ rel="noopener noreferrer"
204
+ class="justify-center mb-4"
205
+ />
206
+
207
+ <!-- Category -->
208
+ <div
209
+ v-if="listing.category"
210
+ class="flex items-center justify-between py-2.5 border-b border-neutral-200 dark:border-neutral-800"
211
+ >
212
+ <span
213
+ class="text-xs font-semibold uppercase tracking-wider text-neutral-500"
214
+ >
215
+ Category
216
+ </span>
217
+ <UBadge
218
+ :label="listing.category"
219
+ variant="subtle"
220
+ color="neutral"
221
+ size="sm"
222
+ />
223
+ </div>
224
+
225
+ <!-- Rating Summary -->
226
+ <div
227
+ v-if="listing.rating"
228
+ class="flex items-center justify-between py-2.5 border-b border-neutral-200 dark:border-neutral-800"
229
+ >
230
+ <span
231
+ class="text-xs font-semibold uppercase tracking-wider text-neutral-500"
232
+ >
233
+ Rating
234
+ </span>
235
+ <div class="flex items-center gap-1.5">
236
+ <UIcon name="i-lucide-star" class="w-4 h-4 text-yellow-400" />
237
+ <span
238
+ class="text-sm font-semibold text-neutral-900 dark:text-white"
239
+ >
240
+ {{ listing.rating.toFixed(1) }}
241
+ </span>
242
+ <span
243
+ v-if="listing.reviewCount"
244
+ class="text-xs text-neutral-500"
245
+ >
246
+ ({{ listing.reviewCount }})
247
+ </span>
248
+ </div>
249
+ </div>
250
+
251
+ <!-- Tags -->
252
+ <div v-if="listing.tags?.length" class="pt-3">
253
+ <span
254
+ class="text-xs font-semibold uppercase tracking-wider text-neutral-500 block mb-2"
255
+ >
256
+ Tags
257
+ </span>
258
+ <div class="flex flex-wrap gap-1.5">
259
+ <UBadge
260
+ v-for="tag in listing.tags"
261
+ :key="tag"
262
+ :label="tag"
263
+ variant="subtle"
264
+ color="neutral"
265
+ size="xs"
266
+ />
267
+ </div>
268
+ </div>
269
+ </div>
270
+ </slot>
271
+ </div>
272
+ </aside>
273
+ </div>
274
+ </article>
275
+ </template>
276
+
277
+ <script setup>
278
+ const props = defineProps({
279
+ /** Listing object { name, description?, logo?, website?, screenshots?, tags?, category?, rating?, reviewCount?, features? } */
280
+ listing: {
281
+ type: Object,
282
+ default: () => ({
283
+ name: "Listing Name",
284
+ description: "Full description of this listing.",
285
+ category: "General",
286
+ rating: 4.5,
287
+ reviewCount: 42,
288
+ }),
289
+ },
290
+ /** Show screenshots gallery */
291
+ hasScreenshots: {
292
+ type: Boolean,
293
+ default: true,
294
+ },
295
+ /** Show sidebar */
296
+ hasSidebar: {
297
+ type: Boolean,
298
+ default: true,
299
+ },
300
+ /** Show reviews section */
301
+ hasReviews: {
302
+ type: Boolean,
303
+ default: false,
304
+ },
305
+ });
306
+ </script>
@@ -0,0 +1,190 @@
1
+ <template>
2
+ <div>
3
+ <!-- Search + Filters -->
4
+ <div v-if="hasSearch || hasFilters" class="flex flex-wrap gap-4 mb-8">
5
+ <UInput
6
+ v-if="hasSearch"
7
+ v-model="search"
8
+ placeholder="Search listings..."
9
+ icon="i-lucide-search"
10
+ class="w-full sm:w-72"
11
+ />
12
+ <USelect
13
+ v-if="hasFilters && categories.length"
14
+ v-model="selectedCategory"
15
+ :options="categoryOptions"
16
+ placeholder="All Categories"
17
+ class="w-48"
18
+ />
19
+ </div>
20
+
21
+ <!-- Grid / List -->
22
+ <div :class="containerClasses">
23
+ <XMarkDirectoryListingCard
24
+ v-for="(listing, index) in paginatedListings"
25
+ :key="listing.name + index"
26
+ :listing="listing"
27
+ :layout="layout === 'list' ? 'row' : 'card'"
28
+ class="xFadeUp"
29
+ :style="{ transitionDelay: `${index * 50}ms` }"
30
+ />
31
+ </div>
32
+
33
+ <!-- Empty State -->
34
+ <div v-if="!paginatedListings.length" class="text-center py-20">
35
+ <UIcon
36
+ name="i-lucide-search-x"
37
+ class="w-12 h-12 text-neutral-300 dark:text-neutral-700 mx-auto mb-4"
38
+ />
39
+ <p
40
+ class="text-lg font-medium text-neutral-700 dark:text-neutral-300 mb-1"
41
+ >
42
+ No listings found
43
+ </p>
44
+ <p class="text-sm text-neutral-500 dark:text-neutral-400">
45
+ Try adjusting your search or filter criteria.
46
+ </p>
47
+ <UButton
48
+ v-if="search || selectedCategory"
49
+ label="Clear filters"
50
+ variant="ghost"
51
+ color="primary"
52
+ size="sm"
53
+ class="mt-4"
54
+ @click="clearFilters"
55
+ />
56
+ </div>
57
+
58
+ <!-- Pagination -->
59
+ <div
60
+ v-if="hasPagination && totalPages > 1"
61
+ class="mt-12 flex justify-center"
62
+ >
63
+ <UPagination
64
+ v-model="currentPage"
65
+ :total="filteredListings.length"
66
+ :page-count="perPage"
67
+ />
68
+ </div>
69
+ </div>
70
+ </template>
71
+
72
+ <script setup>
73
+ // Initialize scroll reveal animations
74
+ useScrollReveal();
75
+
76
+ const props = defineProps({
77
+ /** Array of listing objects */
78
+ listings: {
79
+ type: Array,
80
+ default: () => [],
81
+ },
82
+ /** Available category strings for filter */
83
+ categories: {
84
+ type: Array,
85
+ default: () => [],
86
+ },
87
+ /** Display mode */
88
+ layout: {
89
+ type: String,
90
+ default: "grid",
91
+ validator: (v) => ["grid", "list"].includes(v),
92
+ },
93
+ /** Number of columns in grid mode */
94
+ columns: {
95
+ type: Number,
96
+ default: 3,
97
+ validator: (v) => [2, 3, 4].includes(v),
98
+ },
99
+ /** Show search input */
100
+ hasSearch: {
101
+ type: Boolean,
102
+ default: true,
103
+ },
104
+ /** Show category filter */
105
+ hasFilters: {
106
+ type: Boolean,
107
+ default: true,
108
+ },
109
+ /** Show pagination */
110
+ hasPagination: {
111
+ type: Boolean,
112
+ default: true,
113
+ },
114
+ /** Items per page */
115
+ perPage: {
116
+ type: Number,
117
+ default: 12,
118
+ },
119
+ /** Optional button config passed through to each ListingCard */
120
+ cardButton: {
121
+ type: Object,
122
+ default: null,
123
+ },
124
+ });
125
+
126
+ const search = ref("");
127
+ const selectedCategory = ref("");
128
+ const currentPage = ref(1);
129
+
130
+ const categoryOptions = computed(() => {
131
+ return [
132
+ { label: "All Categories", value: "" },
133
+ ...props.categories.map((cat) => ({
134
+ label: cat,
135
+ value: cat,
136
+ })),
137
+ ];
138
+ });
139
+
140
+ const filteredListings = computed(() => {
141
+ let result = props.listings;
142
+
143
+ if (search.value) {
144
+ const q = search.value.toLowerCase();
145
+ result = result.filter(
146
+ (item) =>
147
+ item.name?.toLowerCase().includes(q) ||
148
+ item.description?.toLowerCase().includes(q)
149
+ );
150
+ }
151
+
152
+ if (selectedCategory.value) {
153
+ result = result.filter((item) => item.category === selectedCategory.value);
154
+ }
155
+
156
+ return result;
157
+ });
158
+
159
+ const totalPages = computed(() => {
160
+ return Math.ceil(filteredListings.value.length / props.perPage);
161
+ });
162
+
163
+ const paginatedListings = computed(() => {
164
+ const start = (currentPage.value - 1) * props.perPage;
165
+ const end = start + props.perPage;
166
+ return filteredListings.value.slice(start, end);
167
+ });
168
+
169
+ const containerClasses = computed(() => {
170
+ if (props.layout === "list") {
171
+ return "flex flex-col gap-3";
172
+ }
173
+ const cols = {
174
+ 2: "grid sm:grid-cols-2 gap-6",
175
+ 3: "grid sm:grid-cols-2 lg:grid-cols-3 gap-6",
176
+ 4: "grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6",
177
+ };
178
+ return cols[props.columns] || cols[3];
179
+ });
180
+
181
+ const clearFilters = () => {
182
+ search.value = "";
183
+ selectedCategory.value = "";
184
+ };
185
+
186
+ // Reset pagination when filters change
187
+ watch([search, selectedCategory], () => {
188
+ currentPage.value = 1;
189
+ });
190
+ </script>
@@ -0,0 +1,201 @@
1
+ <template>
2
+ <section :class="['relative overflow-hidden rounded-2xl', bgClasses]">
3
+ <!-- Background Pattern -->
4
+ <XMarkPatternBg
5
+ v-if="bg !== 'bold' && bg !== 'gradient'"
6
+ pattern="dots"
7
+ :opacity="0.04"
8
+ class="absolute inset-0 pointer-events-none"
9
+ />
10
+
11
+ <div class="relative z-10 p-8 md:p-12 lg:p-16">
12
+ <div class="grid lg:grid-cols-2 gap-10 lg:gap-16 items-center">
13
+ <!-- Left: Text + Features -->
14
+ <div class="xFadeUp">
15
+ <!-- Title -->
16
+ <h2
17
+ :class="[
18
+ 'xText-headline',
19
+ isBold ? 'text-white' : 'text-neutral-900 dark:text-white',
20
+ ]"
21
+ >
22
+ {{ title }}
23
+ </h2>
24
+
25
+ <!-- Description -->
26
+ <p
27
+ v-if="description"
28
+ :class="[
29
+ 'mt-4 text-lg leading-relaxed',
30
+ isBold
31
+ ? 'text-white/80'
32
+ : 'text-neutral-600 dark:text-neutral-400',
33
+ ]"
34
+ >
35
+ {{ description }}
36
+ </p>
37
+
38
+ <!-- Features List -->
39
+ <ul v-if="features.length" class="mt-6 space-y-3">
40
+ <li
41
+ v-for="(feature, index) in features"
42
+ :key="index"
43
+ class="flex items-center gap-3"
44
+ >
45
+ <span
46
+ :class="[
47
+ 'flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center',
48
+ isBold
49
+ ? 'bg-white/20'
50
+ : 'bg-primary-100 dark:bg-primary-900/40',
51
+ ]"
52
+ >
53
+ <UIcon
54
+ name="i-lucide-check"
55
+ :class="[
56
+ 'w-3 h-3',
57
+ isBold
58
+ ? 'text-white'
59
+ : 'text-primary-600 dark:text-primary-400',
60
+ ]"
61
+ />
62
+ </span>
63
+ <span
64
+ :class="[
65
+ 'text-sm font-medium',
66
+ isBold
67
+ ? 'text-white/90'
68
+ : 'text-neutral-700 dark:text-neutral-300',
69
+ ]"
70
+ >
71
+ {{ feature }}
72
+ </span>
73
+ </li>
74
+ </ul>
75
+ </div>
76
+
77
+ <!-- Right: CTA Card -->
78
+ <div class="xFadeUp" style="transition-delay: 100ms">
79
+ <div
80
+ :class="[
81
+ 'rounded-2xl p-8 text-center',
82
+ isBold
83
+ ? 'bg-white/10 backdrop-blur-sm border border-white/20'
84
+ : 'bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 shadow-lg',
85
+ ]"
86
+ >
87
+ <!-- Icon -->
88
+ <div
89
+ :class="[
90
+ 'inline-flex items-center justify-center w-14 h-14 rounded-2xl mb-5',
91
+ isBold
92
+ ? 'bg-white/20'
93
+ : 'bg-primary-100 dark:bg-primary-900/40',
94
+ ]"
95
+ >
96
+ <UIcon
97
+ name="i-lucide-plus-circle"
98
+ :class="[
99
+ 'w-7 h-7',
100
+ isBold
101
+ ? 'text-white'
102
+ : 'text-primary-600 dark:text-primary-400',
103
+ ]"
104
+ />
105
+ </div>
106
+
107
+ <!-- Card Title -->
108
+ <h3
109
+ :class="[
110
+ 'text-xl font-semibold mb-2',
111
+ isBold ? 'text-white' : 'text-neutral-900 dark:text-white',
112
+ ]"
113
+ >
114
+ {{ button.label || "Submit a Listing" }}
115
+ </h3>
116
+
117
+ <p
118
+ :class="[
119
+ 'text-sm mb-6',
120
+ isBold
121
+ ? 'text-white/70'
122
+ : 'text-neutral-500 dark:text-neutral-400',
123
+ ]"
124
+ >
125
+ Get your product in front of thousands of buyers.
126
+ </p>
127
+
128
+ <!-- CTA Button -->
129
+ <UButton
130
+ :to="button.to || '/submit'"
131
+ :label="button.label || 'Submit a Listing'"
132
+ :color="isBold ? 'neutral' : 'primary'"
133
+ :class="
134
+ isBold
135
+ ? 'bg-white text-neutral-900 hover:bg-white/90 w-full justify-center'
136
+ : 'w-full justify-center'
137
+ "
138
+ size="lg"
139
+ block
140
+ />
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </section>
146
+ </template>
147
+
148
+ <script setup>
149
+ // Initialize scroll reveal animations
150
+ useScrollReveal();
151
+
152
+ const props = defineProps({
153
+ /** Section title */
154
+ title: {
155
+ type: String,
156
+ default: "List your product",
157
+ },
158
+ /** Section description */
159
+ description: {
160
+ type: String,
161
+ default:
162
+ "Reach thousands of buyers actively searching for solutions like yours.",
163
+ },
164
+ /** CTA button config { label, to } */
165
+ button: {
166
+ type: Object,
167
+ default: () => ({
168
+ label: "Submit a Listing",
169
+ to: "/submit",
170
+ }),
171
+ },
172
+ /** Feature bullet points */
173
+ features: {
174
+ type: Array,
175
+ default: () => [
176
+ "Free basic listing",
177
+ "Featured placement available",
178
+ "Analytics dashboard",
179
+ ],
180
+ },
181
+ /** Background style */
182
+ bg: {
183
+ type: String,
184
+ default: "subtle",
185
+ validator: (v) => ["subtle", "bold", "gradient"].includes(v),
186
+ },
187
+ });
188
+
189
+ const isBold = computed(() => {
190
+ return props.bg === "bold" || props.bg === "gradient";
191
+ });
192
+
193
+ const bgClasses = computed(() => {
194
+ const variants = {
195
+ subtle: "bg-neutral-100 dark:bg-neutral-900",
196
+ bold: "bg-primary-600 dark:bg-primary-700",
197
+ gradient: "bg-gradient-to-br from-primary-600 to-primary-800",
198
+ };
199
+ return variants[props.bg] || variants.subtle;
200
+ });
201
+ </script>