@commercejs/ui 0.1.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 (80) hide show
  1. package/dist/module.cjs +5 -0
  2. package/dist/module.d.mts +15 -0
  3. package/dist/module.d.ts +15 -0
  4. package/dist/module.json +12 -0
  5. package/dist/module.mjs +30 -0
  6. package/dist/runtime/app.config.d.ts +0 -0
  7. package/dist/runtime/app.config.js +341 -0
  8. package/dist/runtime/components/auction/CAuctionCard.vue +213 -0
  9. package/dist/runtime/components/auction/CBidPanel.vue +176 -0
  10. package/dist/runtime/components/cart/CCartDrawer.vue +223 -0
  11. package/dist/runtime/components/cart/CCartItem.vue +136 -0
  12. package/dist/runtime/components/cart/CCartSummary.vue +127 -0
  13. package/dist/runtime/components/cart/CQuantitySelector.vue +110 -0
  14. package/dist/runtime/components/category/CCategoryFilter.vue +123 -0
  15. package/dist/runtime/components/checkout/CAddressForm.vue +186 -0
  16. package/dist/runtime/components/checkout/CCheckoutStepper.vue +84 -0
  17. package/dist/runtime/components/common/CEmptyState.vue +81 -0
  18. package/dist/runtime/components/common/CProductTypeBadge.vue +37 -0
  19. package/dist/runtime/components/event/CEventCard.vue +129 -0
  20. package/dist/runtime/components/gift-card/CGiftCardBalance.vue +119 -0
  21. package/dist/runtime/components/gift-card/CGiftCardForm.vue +157 -0
  22. package/dist/runtime/components/gift-card/CGiftCardForm.vue.backup +138 -0
  23. package/dist/runtime/components/marketing/CHeroBanner.vue +142 -0
  24. package/dist/runtime/components/navigation/CSearchBar.vue +127 -0
  25. package/dist/runtime/components/order/COrderCard.vue +117 -0
  26. package/dist/runtime/components/order/COrderTimeline.vue +99 -0
  27. package/dist/runtime/components/product/CProductCard.vue +206 -0
  28. package/dist/runtime/components/product/CProductGallery.vue +110 -0
  29. package/dist/runtime/components/product/CProductGrid.vue +82 -0
  30. package/dist/runtime/components/product/CProductOptions.vue +101 -0
  31. package/dist/runtime/components/product/CProductPrice.vue +87 -0
  32. package/dist/runtime/components/promotion/CCouponInput.vue +104 -0
  33. package/dist/runtime/components/promotion/CPromoBanner.vue +153 -0
  34. package/dist/runtime/components/rental/CRentalBookingForm.vue +214 -0
  35. package/dist/runtime/components/rental/CRentalCard.vue +146 -0
  36. package/dist/runtime/components/review/CReviewCard.vue +96 -0
  37. package/dist/runtime/components/review/CReviewStars.vue +106 -0
  38. package/dist/runtime/components/subscription/CSubscriptionCard.vue +137 -0
  39. package/dist/runtime/components/wholesale/CPriceTierTable.vue +88 -0
  40. package/dist/runtime/components/wholesale/CQuoteRequestForm.vue +148 -0
  41. package/dist/runtime/components/wishlist/CWishlistGrid.vue +96 -0
  42. package/dist/types.d.mts +7 -0
  43. package/dist/types.d.ts +7 -0
  44. package/package.json +41 -0
  45. package/src/module.ts +52 -0
  46. package/src/runtime/app.config.ts +392 -0
  47. package/src/runtime/components/auction/CAuctionCard.vue +213 -0
  48. package/src/runtime/components/auction/CBidPanel.vue +176 -0
  49. package/src/runtime/components/cart/CCartDrawer.vue +223 -0
  50. package/src/runtime/components/cart/CCartItem.vue +136 -0
  51. package/src/runtime/components/cart/CCartSummary.vue +127 -0
  52. package/src/runtime/components/cart/CQuantitySelector.vue +110 -0
  53. package/src/runtime/components/category/CCategoryFilter.vue +123 -0
  54. package/src/runtime/components/checkout/CAddressForm.vue +186 -0
  55. package/src/runtime/components/checkout/CCheckoutStepper.vue +84 -0
  56. package/src/runtime/components/common/CEmptyState.vue +81 -0
  57. package/src/runtime/components/common/CProductTypeBadge.vue +37 -0
  58. package/src/runtime/components/event/CEventCard.vue +129 -0
  59. package/src/runtime/components/gift-card/CGiftCardBalance.vue +119 -0
  60. package/src/runtime/components/gift-card/CGiftCardForm.vue +157 -0
  61. package/src/runtime/components/gift-card/CGiftCardForm.vue.backup +138 -0
  62. package/src/runtime/components/marketing/CHeroBanner.vue +142 -0
  63. package/src/runtime/components/navigation/CSearchBar.vue +127 -0
  64. package/src/runtime/components/order/COrderCard.vue +117 -0
  65. package/src/runtime/components/order/COrderTimeline.vue +99 -0
  66. package/src/runtime/components/product/CProductCard.vue +206 -0
  67. package/src/runtime/components/product/CProductGallery.vue +110 -0
  68. package/src/runtime/components/product/CProductGrid.vue +82 -0
  69. package/src/runtime/components/product/CProductOptions.vue +101 -0
  70. package/src/runtime/components/product/CProductPrice.vue +87 -0
  71. package/src/runtime/components/promotion/CCouponInput.vue +104 -0
  72. package/src/runtime/components/promotion/CPromoBanner.vue +153 -0
  73. package/src/runtime/components/rental/CRentalBookingForm.vue +214 -0
  74. package/src/runtime/components/rental/CRentalCard.vue +146 -0
  75. package/src/runtime/components/review/CReviewCard.vue +96 -0
  76. package/src/runtime/components/review/CReviewStars.vue +106 -0
  77. package/src/runtime/components/subscription/CSubscriptionCard.vue +137 -0
  78. package/src/runtime/components/wholesale/CPriceTierTable.vue +88 -0
  79. package/src/runtime/components/wholesale/CQuoteRequestForm.vue +148 -0
  80. package/src/runtime/components/wishlist/CWishlistGrid.vue +96 -0
@@ -0,0 +1,5 @@
1
+ module.exports = function(...args) {
2
+ return import('./module.mjs').then(m => m.default.call(this, ...args))
3
+ }
4
+ const _meta = module.exports.meta = require('./module.json')
5
+ module.exports.getMeta = () => Promise.resolve(_meta)
@@ -0,0 +1,15 @@
1
+ import { NuxtModule } from '@nuxt/schema';
2
+
3
+ interface CommerceUIModuleOptions {
4
+ /**
5
+ * Prefix for all commerce UI components.
6
+ * @default 'C'
7
+ * @example 'Commerce' → <CommerceProductCard />
8
+ * @example 'U' → <UProductCard /> (for upstream Nuxt UI)
9
+ */
10
+ prefix?: string;
11
+ }
12
+ declare const commerceUIModule: NuxtModule<CommerceUIModuleOptions>;
13
+
14
+ export { commerceUIModule as default };
15
+ export type { CommerceUIModuleOptions };
@@ -0,0 +1,15 @@
1
+ import { NuxtModule } from '@nuxt/schema';
2
+
3
+ interface CommerceUIModuleOptions {
4
+ /**
5
+ * Prefix for all commerce UI components.
6
+ * @default 'C'
7
+ * @example 'Commerce' → <CommerceProductCard />
8
+ * @example 'U' → <UProductCard /> (for upstream Nuxt UI)
9
+ */
10
+ prefix?: string;
11
+ }
12
+ declare const commerceUIModule: NuxtModule<CommerceUIModuleOptions>;
13
+
14
+ export { commerceUIModule as default };
15
+ export type { CommerceUIModuleOptions };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@commercejs/ui",
3
+ "configKey": "commerceUI",
4
+ "compatibility": {
5
+ "nuxt": ">=3.0.0"
6
+ },
7
+ "version": "0.1.0",
8
+ "builder": {
9
+ "@nuxt/module-builder": "0.8.4",
10
+ "unbuild": "unknown"
11
+ }
12
+ }
@@ -0,0 +1,30 @@
1
+ import { defineNuxtModule, createResolver, installModule, addComponentsDir } from '@nuxt/kit';
2
+
3
+ const commerceUIModule = defineNuxtModule({
4
+ meta: {
5
+ name: "@commercejs/ui",
6
+ configKey: "commerceUI",
7
+ compatibility: {
8
+ nuxt: ">=3.0.0"
9
+ }
10
+ },
11
+ defaults: {
12
+ prefix: "C"
13
+ },
14
+ async setup(options, nuxt) {
15
+ const { resolve } = createResolver(import.meta.url);
16
+ const prefix = options.prefix || "C";
17
+ await installModule("@nuxt/ui");
18
+ addComponentsDir({
19
+ path: resolve("./runtime/components"),
20
+ prefix,
21
+ // Scan subdirectories (product/, cart/, etc.)
22
+ pathPrefix: false
23
+ });
24
+ nuxt.hook("app:resolve", (app) => {
25
+ app.configs.push(resolve("./runtime/app.config"));
26
+ });
27
+ }
28
+ });
29
+
30
+ export { commerceUIModule as default };
File without changes
@@ -0,0 +1,341 @@
1
+ export default defineAppConfig({
2
+ ui: {
3
+ // ---- Product Components ----
4
+ productCard: {
5
+ slots: {
6
+ root: "group relative rounded-lg overflow-hidden ring ring-default transition-all duration-200",
7
+ imageWrapper: "relative aspect-square overflow-hidden bg-elevated",
8
+ image: "size-full object-cover transition-transform duration-300 group-hover:scale-105",
9
+ badge: "absolute top-3 start-3 z-10",
10
+ overlay: "absolute inset-x-0 bottom-0 p-3 flex justify-end opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-10",
11
+ body: "p-4 space-y-1.5",
12
+ title: "font-medium text-sm text-highlighted line-clamp-2 transition-colors",
13
+ price: "font-bold text-highlighted",
14
+ originalPrice: "text-xs text-muted line-through ms-2",
15
+ priceWrapper: "flex items-baseline gap-1",
16
+ rating: "flex items-center gap-1 text-xs text-muted"
17
+ },
18
+ variants: {
19
+ variant: {
20
+ outline: { root: "bg-default hover:shadow-lg hover:shadow-default/10" },
21
+ soft: { root: "bg-elevated/50 ring-0 hover:bg-elevated" },
22
+ ghost: { root: "ring-0 hover:bg-elevated/50" }
23
+ },
24
+ size: {
25
+ sm: { body: "p-3 space-y-1", title: "text-xs", price: "text-sm" },
26
+ md: { body: "p-4 space-y-1.5", title: "text-sm", price: "text-base" },
27
+ lg: { body: "p-5 space-y-2", title: "text-base", price: "text-lg" }
28
+ }
29
+ },
30
+ defaultVariants: { variant: "outline", size: "md" }
31
+ },
32
+ productPrice: {
33
+ slots: {
34
+ root: "inline-flex items-baseline gap-1.5",
35
+ current: "font-bold text-highlighted",
36
+ original: "line-through text-muted",
37
+ discount: "font-medium text-error"
38
+ },
39
+ variants: {
40
+ size: {
41
+ xs: { current: "text-xs", original: "text-xs", discount: "text-xs" },
42
+ sm: { current: "text-sm", original: "text-xs", discount: "text-xs" },
43
+ md: { current: "text-base", original: "text-sm", discount: "text-sm" },
44
+ lg: { current: "text-lg", original: "text-sm", discount: "text-sm" },
45
+ xl: { current: "text-2xl", original: "text-base", discount: "text-base" }
46
+ }
47
+ },
48
+ defaultVariants: { size: "md" }
49
+ },
50
+ productGallery: {
51
+ slots: {
52
+ root: "flex flex-col gap-3",
53
+ main: "relative rounded-lg overflow-hidden bg-elevated aspect-square",
54
+ mainImage: "size-full object-contain",
55
+ thumbnails: "flex gap-2 overflow-x-auto",
56
+ thumbnail: "size-16 rounded-md overflow-hidden ring ring-transparent cursor-pointer shrink-0 transition-all",
57
+ thumbnailActive: "ring-primary"
58
+ },
59
+ variants: {
60
+ thumbnailPosition: {
61
+ bottom: { root: "flex-col" },
62
+ start: { root: "flex-row-reverse", thumbnails: "flex-col overflow-y-auto max-h-[400px]" }
63
+ }
64
+ }
65
+ },
66
+ productOptions: {
67
+ slots: {
68
+ root: "space-y-4",
69
+ group: "space-y-2",
70
+ label: "text-sm font-medium text-highlighted",
71
+ values: "flex flex-wrap gap-2"
72
+ }
73
+ },
74
+ productGrid: {
75
+ slots: {
76
+ root: "",
77
+ empty: ""
78
+ }
79
+ },
80
+ // ---- Cart Components ----
81
+ quantitySelector: {
82
+ slots: {
83
+ root: "inline-flex items-center gap-0.5",
84
+ button: "",
85
+ input: "w-10 text-center text-sm font-medium bg-transparent border-0 focus:ring-0 text-highlighted"
86
+ },
87
+ variants: {
88
+ size: {
89
+ sm: { input: "w-8 text-xs" },
90
+ md: { input: "w-10 text-sm" },
91
+ lg: { input: "w-12 text-base" }
92
+ }
93
+ },
94
+ defaultVariants: { size: "md" }
95
+ },
96
+ cartItem: {
97
+ slots: {
98
+ root: "flex gap-4",
99
+ imageWrapper: "shrink-0 rounded-lg overflow-hidden bg-elevated",
100
+ image: "size-full object-cover",
101
+ body: "flex-1 min-w-0",
102
+ title: "font-medium text-sm text-highlighted line-clamp-1",
103
+ variant: "text-xs text-muted",
104
+ priceWrapper: "mt-1",
105
+ actions: "flex items-center justify-between mt-2"
106
+ },
107
+ variants: {
108
+ size: {
109
+ sm: { imageWrapper: "size-16", title: "text-xs" },
110
+ md: { imageWrapper: "size-20", title: "text-sm" },
111
+ lg: { imageWrapper: "size-24", title: "text-base" }
112
+ }
113
+ },
114
+ defaultVariants: { size: "md" }
115
+ },
116
+ cartSummary: {
117
+ slots: {
118
+ root: "space-y-4",
119
+ title: "text-lg font-semibold text-highlighted",
120
+ lineItem: "flex justify-between text-sm",
121
+ lineLabel: "text-muted",
122
+ lineValue: "font-medium text-highlighted",
123
+ separator: "",
124
+ total: "flex justify-between text-base font-bold",
125
+ totalLabel: "text-highlighted",
126
+ totalValue: "text-highlighted",
127
+ actions: "pt-4"
128
+ }
129
+ },
130
+ // ---- Checkout Components ----
131
+ checkoutStepper: {
132
+ slots: {
133
+ root: ""
134
+ }
135
+ },
136
+ addressForm: {
137
+ slots: {
138
+ root: "space-y-4",
139
+ row: "grid grid-cols-1 sm:grid-cols-2 gap-4",
140
+ field: ""
141
+ }
142
+ },
143
+ // ---- Review Components ----
144
+ reviewStars: {
145
+ slots: {
146
+ root: "",
147
+ star: "",
148
+ starFilled: "",
149
+ starEmpty: "",
150
+ count: "text-xs text-muted ms-1"
151
+ }
152
+ },
153
+ reviewCard: {
154
+ slots: {
155
+ root: "space-y-2",
156
+ header: "space-y-1",
157
+ author: "font-medium text-sm text-highlighted",
158
+ date: "text-xs text-muted",
159
+ title: "font-medium text-highlighted",
160
+ body: "text-sm text-muted leading-relaxed",
161
+ verified: ""
162
+ }
163
+ },
164
+ // ---- Navigation Components ----
165
+ searchBar: {
166
+ slots: {
167
+ root: ""
168
+ }
169
+ },
170
+ // ---- Category Components ----
171
+ categoryFilter: {
172
+ slots: {
173
+ root: "space-y-4",
174
+ group: "space-y-2",
175
+ groupTitle: "text-sm font-semibold text-highlighted",
176
+ values: "space-y-1.5",
177
+ value: "flex items-center gap-2 text-sm cursor-pointer",
178
+ count: "text-xs text-muted ms-auto",
179
+ showMore: "text-xs text-primary font-medium cursor-pointer hover:underline"
180
+ }
181
+ },
182
+ // ---- Marketing Components ----
183
+ heroBanner: {
184
+ slots: {
185
+ root: "",
186
+ background: "",
187
+ overlay: "",
188
+ content: "",
189
+ title: "",
190
+ subtitle: "",
191
+ actions: ""
192
+ }
193
+ },
194
+ // ---- Common Components ----
195
+ emptyState: {
196
+ slots: {
197
+ root: "",
198
+ icon: "",
199
+ title: "",
200
+ description: ""
201
+ }
202
+ },
203
+ // ---- Auction Components ----
204
+ auctionCard: {
205
+ slots: {
206
+ root: "",
207
+ imageWrapper: "",
208
+ image: "",
209
+ statusBadge: "",
210
+ body: "p-4 space-y-2",
211
+ title: "font-medium text-sm text-highlighted line-clamp-2",
212
+ bidInfo: "space-y-1",
213
+ currentBid: "font-bold text-lg text-highlighted",
214
+ bidCount: "text-xs text-muted",
215
+ timer: "",
216
+ actions: "flex gap-2 pt-1"
217
+ }
218
+ },
219
+ bidPanel: {
220
+ slots: {
221
+ root: "space-y-6",
222
+ currentBid: "text-center p-6 rounded-xl bg-elevated ring ring-default",
223
+ form: "space-y-4",
224
+ history: "space-y-2"
225
+ }
226
+ },
227
+ // ---- Rental Components ----
228
+ rentalCard: {
229
+ slots: {
230
+ root: "",
231
+ imageWrapper: "",
232
+ image: "",
233
+ body: "p-4 space-y-2",
234
+ title: "font-medium text-sm text-highlighted line-clamp-2",
235
+ pricing: "flex items-baseline gap-1",
236
+ meta: "flex flex-wrap gap-2 text-xs",
237
+ actions: ""
238
+ }
239
+ },
240
+ rentalBookingForm: {
241
+ slots: {
242
+ root: "space-y-5",
243
+ dates: "",
244
+ summary: "rounded-xl bg-elevated p-4 space-y-2"
245
+ }
246
+ },
247
+ // ---- Subscription Components ----
248
+ subscriptionCard: {
249
+ slots: {
250
+ root: "",
251
+ header: "text-center",
252
+ pricing: "text-center py-4",
253
+ features: "",
254
+ actions: ""
255
+ }
256
+ },
257
+ // ---- Wholesale Components ----
258
+ priceTierTable: {
259
+ slots: {
260
+ root: "overflow-hidden rounded-lg ring ring-default",
261
+ row: "",
262
+ activeRow: "bg-primary/5 font-medium"
263
+ }
264
+ },
265
+ quoteRequestForm: {
266
+ slots: {
267
+ root: "space-y-6",
268
+ items: "space-y-4",
269
+ contact: "grid grid-cols-1 sm:grid-cols-2 gap-4"
270
+ }
271
+ },
272
+ // ---- Gift Card Components ----
273
+ giftCardForm: {
274
+ slots: {
275
+ root: "space-y-6",
276
+ amounts: "space-y-3",
277
+ recipient: "space-y-3"
278
+ }
279
+ },
280
+ giftCardBalance: {
281
+ slots: {
282
+ root: "space-y-4",
283
+ card: "rounded-xl bg-elevated ring ring-default p-5 space-y-3",
284
+ form: "flex gap-2"
285
+ }
286
+ },
287
+ // ---- Order Components ----
288
+ orderCard: {
289
+ slots: {
290
+ root: "rounded-xl ring ring-default bg-default overflow-hidden",
291
+ header: "flex items-center justify-between px-5 py-3 bg-elevated",
292
+ items: "px-5 py-4",
293
+ footer: "flex items-center justify-between px-5 py-3 bg-elevated"
294
+ }
295
+ },
296
+ orderTimeline: {
297
+ slots: {
298
+ root: "relative",
299
+ entry: "flex gap-4 pb-6 last:pb-0",
300
+ dot: "size-8 rounded-full flex items-center justify-center ring-4 ring-default bg-default z-10",
301
+ line: "w-0.5 flex-1 bg-default mt-1"
302
+ }
303
+ },
304
+ // ---- Promotion Components ----
305
+ promoBanner: {
306
+ slots: {
307
+ root: "relative overflow-hidden rounded-xl",
308
+ content: "",
309
+ timer: "flex gap-2",
310
+ cta: ""
311
+ }
312
+ },
313
+ couponInput: {
314
+ slots: {
315
+ root: "space-y-2",
316
+ input: "flex-1 font-mono uppercase",
317
+ applied: "flex items-center justify-between p-3 rounded-lg bg-success/10 ring ring-success/30"
318
+ }
319
+ },
320
+ // ---- Event Components ----
321
+ eventCard: {
322
+ slots: {
323
+ root: "",
324
+ imageWrapper: "",
325
+ dateOverlay: "",
326
+ body: "p-4 space-y-2",
327
+ title: "font-medium text-sm text-highlighted line-clamp-2",
328
+ meta: "space-y-1 text-xs text-muted",
329
+ actions: ""
330
+ }
331
+ },
332
+ // ---- Wishlist Components ----
333
+ wishlistGrid: {
334
+ slots: {
335
+ root: "",
336
+ item: "relative group",
337
+ actions: ""
338
+ }
339
+ }
340
+ }
341
+ });
@@ -0,0 +1,213 @@
1
+ <script setup lang="ts">
2
+ import type { Product, AuctionProductMeta } from '@commercejs/types'
3
+
4
+ /**
5
+ * CAuctionCard — Product card for auction items.
6
+ * Shows current bid, bid count, time remaining, and auction status.
7
+ */
8
+
9
+ export interface AuctionCardProps {
10
+ product: Product
11
+ /** Override auction meta (optional, reads from product.auction by default) */
12
+ auction?: AuctionProductMeta
13
+ /** Show buy-now button */
14
+ showBuyNow?: boolean
15
+ /** Per-instance theme overrides */
16
+ ui?: Partial<{
17
+ root: any
18
+ imageWrapper: any
19
+ image: any
20
+ statusBadge: any
21
+ body: any
22
+ title: any
23
+ bidInfo: any
24
+ currentBid: any
25
+ bidCount: any
26
+ timer: any
27
+ actions: any
28
+ }>
29
+ }
30
+
31
+ const props = withDefaults(defineProps<AuctionCardProps>(), {
32
+ showBuyNow: true,
33
+ })
34
+
35
+ const emit = defineEmits<{
36
+ 'bid': [product: Product]
37
+ 'buy-now': [product: Product]
38
+ }>()
39
+
40
+ const auctionMeta = computed(() => props.auction || props.product.auction)
41
+
42
+ function t(value: any): string {
43
+ if (!value) return ''
44
+ if (typeof value === 'string') return value
45
+ return value.en || value.ar || Object.values(value)[0] || ''
46
+ }
47
+
48
+ const productName = computed(() => t(props.product.name))
49
+ const mainImage = computed(() => props.product.primaryImage || props.product.gallery?.[0])
50
+
51
+ // Time remaining
52
+ const timeRemaining = ref('')
53
+ let timer: ReturnType<typeof setInterval>
54
+
55
+ function updateTimer() {
56
+ if (!auctionMeta.value) return
57
+ const end = new Date(auctionMeta.value.endsAt).getTime()
58
+ const now = Date.now()
59
+ const diff = end - now
60
+
61
+ if (diff <= 0) {
62
+ timeRemaining.value = 'Ended'
63
+ clearInterval(timer)
64
+ return
65
+ }
66
+
67
+ const days = Math.floor(diff / 86400000)
68
+ const hours = Math.floor((diff % 86400000) / 3600000)
69
+ const mins = Math.floor((diff % 3600000) / 60000)
70
+ const secs = Math.floor((diff % 60000) / 1000)
71
+
72
+ if (days > 0) {
73
+ timeRemaining.value = `${days}d ${hours}h`
74
+ } else if (hours > 0) {
75
+ timeRemaining.value = `${hours}h ${mins}m`
76
+ } else {
77
+ timeRemaining.value = `${mins}m ${secs}s`
78
+ }
79
+ }
80
+
81
+ onMounted(() => {
82
+ updateTimer()
83
+ timer = setInterval(updateTimer, 1000)
84
+ })
85
+
86
+ onUnmounted(() => clearInterval(timer))
87
+
88
+ const statusColor = computed(() => {
89
+ const map: Record<string, string> = {
90
+ upcoming: 'info',
91
+ active: 'success',
92
+ ended: 'neutral',
93
+ sold: 'primary',
94
+ cancelled: 'error',
95
+ reserve_not_met: 'warning',
96
+ }
97
+ return map[auctionMeta.value?.status || ''] || 'neutral'
98
+ })
99
+
100
+ const statusLabel = computed(() => {
101
+ const map: Record<string, string> = {
102
+ upcoming: 'Upcoming',
103
+ active: 'Live',
104
+ ended: 'Ended',
105
+ sold: 'Sold',
106
+ cancelled: 'Cancelled',
107
+ reserve_not_met: 'Reserve Not Met',
108
+ }
109
+ return map[auctionMeta.value?.status || ''] || auctionMeta.value?.status
110
+ })
111
+
112
+ // Resolve theme from app.config
113
+ const appConfig = useAppConfig()
114
+ const theme = computed(() => (appConfig.ui as any)?.auctionCard ?? {})
115
+
116
+ const slotClasses = computed(() => {
117
+ const base = theme.value?.slots ?? {}
118
+ const merge = (slot: string) => [base[slot], props.ui?.[slot as keyof typeof props.ui]]
119
+ return {
120
+ root: merge('root'),
121
+ imageWrapper: merge('imageWrapper'),
122
+ image: merge('image'),
123
+ statusBadge: merge('statusBadge'),
124
+ body: merge('body'),
125
+ title: merge('title'),
126
+ bidInfo: merge('bidInfo'),
127
+ currentBid: merge('currentBid'),
128
+ bidCount: merge('bidCount'),
129
+ timer: merge('timer'),
130
+ actions: merge('actions'),
131
+ }
132
+ })
133
+ </script>
134
+
135
+ <template>
136
+ <div :class="['group relative rounded-lg overflow-hidden ring ring-default bg-default hover:shadow-lg transition-all duration-200', slotClasses.root]">
137
+ <!-- Image -->
138
+ <div :class="['relative aspect-square overflow-hidden bg-elevated', slotClasses.imageWrapper]">
139
+ <img
140
+ v-if="mainImage"
141
+ :src="mainImage.url"
142
+ :alt="mainImage.alt || productName"
143
+ :class="['size-full object-cover transition-transform duration-300 group-hover:scale-105', slotClasses.image]"
144
+ />
145
+
146
+ <!-- Status badge -->
147
+ <slot name="status" :status="auctionMeta?.status" :label="statusLabel">
148
+ <UBadge
149
+ v-if="auctionMeta"
150
+ :color="statusColor as any"
151
+ size="sm"
152
+ :class="['absolute top-3 start-3', slotClasses.statusBadge]"
153
+ >
154
+ <UIcon v-if="auctionMeta.status === 'active'" name="i-heroicons-signal" class="me-0.5 animate-pulse" />
155
+ {{ statusLabel }}
156
+ </UBadge>
157
+ </slot>
158
+
159
+ <!-- Timer -->
160
+ <slot name="timer" :remaining="timeRemaining">
161
+ <div v-if="auctionMeta?.status === 'active'" :class="['absolute bottom-3 end-3 bg-black/70 backdrop-blur-sm text-white text-xs font-mono px-2 py-1 rounded', slotClasses.timer]">
162
+ <UIcon name="i-heroicons-clock" class="me-1" />
163
+ {{ timeRemaining }}
164
+ </div>
165
+ </slot>
166
+ </div>
167
+
168
+ <!-- Body -->
169
+ <div :class="['p-4 space-y-2', slotClasses.body]">
170
+ <slot name="title" :name="productName">
171
+ <h3 :class="['font-medium text-sm text-highlighted line-clamp-2', slotClasses.title]">{{ productName }}</h3>
172
+ </slot>
173
+
174
+ <!-- Bid info -->
175
+ <slot name="bid-info" :auction="auctionMeta">
176
+ <div v-if="auctionMeta" :class="['space-y-1', slotClasses.bidInfo]">
177
+ <div class="flex items-baseline justify-between">
178
+ <span class="text-xs text-muted">{{ auctionMeta.bidCount > 0 ? 'Current Bid' : 'Starting Price' }}</span>
179
+ <span :class="['font-bold text-lg text-highlighted', slotClasses.currentBid]">
180
+ {{ auctionMeta.currentBid?.formatted || auctionMeta.startingPrice.formatted }}
181
+ </span>
182
+ </div>
183
+ <span :class="['text-xs text-muted', slotClasses.bidCount]">
184
+ {{ auctionMeta.bidCount }} bid{{ auctionMeta.bidCount !== 1 ? 's' : '' }}
185
+ </span>
186
+ </div>
187
+ </slot>
188
+
189
+ <!-- Actions -->
190
+ <slot name="actions" :auction="auctionMeta">
191
+ <div v-if="auctionMeta?.status === 'active'" :class="['flex gap-2 pt-1', slotClasses.actions]">
192
+ <UButton
193
+ size="sm"
194
+ color="primary"
195
+ class="flex-1"
196
+ @click="emit('bid', product)"
197
+ >
198
+ Place Bid
199
+ </UButton>
200
+ <UButton
201
+ v-if="showBuyNow && auctionMeta.buyNowPrice"
202
+ size="sm"
203
+ variant="outline"
204
+ color="neutral"
205
+ @click="emit('buy-now', product)"
206
+ >
207
+ Buy Now {{ auctionMeta.buyNowPrice.formatted }}
208
+ </UButton>
209
+ </div>
210
+ </slot>
211
+ </div>
212
+ </div>
213
+ </template>