@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,176 @@
1
+ <script setup lang="ts">
2
+ import type { Bid, AuctionProductMeta, PlaceBidInput } from '@commercejs/types'
3
+
4
+ /**
5
+ * CBidPanel — Bid placement panel for auction products.
6
+ * Shows current bid, bid input, auto-bidding toggle, and bid history.
7
+ */
8
+
9
+ export interface BidPanelProps {
10
+ /** Auction metadata */
11
+ auction: AuctionProductMeta
12
+ /** Recent bids */
13
+ bids?: Bid[]
14
+ /** Whether a bid is being submitted */
15
+ loading?: boolean
16
+ /** Currency symbol for display */
17
+ currencySymbol?: string
18
+ /** Per-instance theme overrides */
19
+ ui?: Partial<{
20
+ root: any
21
+ currentBid: any
22
+ form: any
23
+ history: any
24
+ }>
25
+ }
26
+
27
+ const props = withDefaults(defineProps<BidPanelProps>(), {
28
+ bids: () => [],
29
+ loading: false,
30
+ currencySymbol: '',
31
+ })
32
+
33
+ const emit = defineEmits<{
34
+ 'place-bid': [input: PlaceBidInput]
35
+ }>()
36
+
37
+ const bidAmount = ref<number>(0)
38
+ const enableAutoBid = ref(false)
39
+ const maxAutoBidAmount = ref<number>(0)
40
+
41
+ // Set default bid amount to current bid + increment
42
+ watchEffect(() => {
43
+ const current = props.auction.currentBid?.amount ?? props.auction.startingPrice.amount
44
+ const increment = props.auction.bidIncrement.amount
45
+ if (typeof current === 'number' && typeof increment === 'number') {
46
+ bidAmount.value = current + increment
47
+ }
48
+ })
49
+
50
+ const minBid = computed(() => {
51
+ const current = props.auction.currentBid?.amount ?? props.auction.startingPrice.amount
52
+ const increment = props.auction.bidIncrement.amount
53
+ return typeof current === 'number' && typeof increment === 'number'
54
+ ? current + increment
55
+ : 0
56
+ })
57
+
58
+ function handleSubmit() {
59
+ emit('place-bid', {
60
+ productId: '', // Set by parent
61
+ amount: bidAmount.value,
62
+ maxAutoBid: enableAutoBid.value ? maxAutoBidAmount.value : undefined,
63
+ })
64
+ }
65
+
66
+ // Resolve theme from app.config
67
+ const appConfig = useAppConfig()
68
+ const theme = computed(() => (appConfig.ui as any)?.bidPanel ?? {})
69
+
70
+ const slotClasses = computed(() => {
71
+ const base = theme.value?.slots ?? {}
72
+ const merge = (slot: string) => [base[slot], props.ui?.[slot as keyof typeof props.ui]]
73
+ return {
74
+ root: merge('root'),
75
+ currentBid: merge('currentBid'),
76
+ form: merge('form'),
77
+ history: merge('history'),
78
+ }
79
+ })
80
+ </script>
81
+
82
+ <template>
83
+ <div :class="['space-y-6', slotClasses.root]">
84
+ <!-- Current bid display -->
85
+ <div :class="['text-center p-6 rounded-xl bg-elevated ring ring-default', slotClasses.currentBid]">
86
+ <p class="text-sm text-muted mb-1">{{ auction.bidCount > 0 ? 'Current Highest Bid' : 'Starting Price' }}</p>
87
+ <p class="text-4xl font-bold text-highlighted">
88
+ {{ auction.currentBid?.formatted || auction.startingPrice.formatted }}
89
+ </p>
90
+ <p class="text-xs text-muted mt-2">
91
+ {{ auction.bidCount }} bid{{ auction.bidCount !== 1 ? 's' : '' }}
92
+ · Min increment: {{ auction.bidIncrement.formatted }}
93
+ </p>
94
+ </div>
95
+
96
+ <!-- Bid form -->
97
+ <form v-if="auction.status === 'active'" :class="['space-y-4', slotClasses.form]" @submit.prevent="handleSubmit">
98
+ <UFormField label="Your Bid">
99
+ <UInput
100
+ v-model.number="bidAmount"
101
+ type="number"
102
+ :min="minBid"
103
+ :step="auction.bidIncrement.amount"
104
+ :placeholder="`Min: ${minBid}`"
105
+ size="lg"
106
+ required
107
+ />
108
+ </UFormField>
109
+
110
+ <!-- Auto-bidding toggle -->
111
+ <div v-if="auction.autoBiddingEnabled" class="space-y-2">
112
+ <UCheckbox v-model="enableAutoBid" label="Enable auto-bidding (proxy bid)" />
113
+ <UFormField v-if="enableAutoBid" label="Maximum Auto-Bid">
114
+ <UInput
115
+ v-model.number="maxAutoBidAmount"
116
+ type="number"
117
+ :min="bidAmount"
118
+ placeholder="Your maximum amount"
119
+ />
120
+ <template #hint>
121
+ <span class="text-xs text-muted">We'll bid on your behalf up to this amount</span>
122
+ </template>
123
+ </UFormField>
124
+ </div>
125
+
126
+ <UButton type="submit" block size="lg" color="primary" :loading="loading">
127
+ Place Bid — {{ currencySymbol }}{{ bidAmount }}
128
+ </UButton>
129
+
130
+ <!-- Buy it now -->
131
+ <UButton
132
+ v-if="auction.buyNowPrice"
133
+ block
134
+ variant="outline"
135
+ color="neutral"
136
+ size="lg"
137
+ >
138
+ Buy It Now — {{ auction.buyNowPrice.formatted }}
139
+ </UButton>
140
+ </form>
141
+
142
+ <!-- Bid status messages -->
143
+ <UAlert
144
+ v-else-if="auction.status === 'ended'"
145
+ icon="i-heroicons-clock"
146
+ title="Auction ended"
147
+ color="neutral"
148
+ />
149
+ <UAlert
150
+ v-else-if="auction.status === 'upcoming'"
151
+ icon="i-heroicons-calendar"
152
+ title="Auction hasn't started yet"
153
+ color="info"
154
+ />
155
+
156
+ <!-- Recent bid history -->
157
+ <div v-if="bids.length > 0" :class="['space-y-2', slotClasses.history]">
158
+ <h4 class="text-sm font-semibold text-highlighted">Recent Bids</h4>
159
+ <div class="space-y-1 max-h-48 overflow-y-auto">
160
+ <div
161
+ v-for="bid in bids"
162
+ :key="bid.id"
163
+ class="flex items-center justify-between text-sm py-1.5 px-2 rounded-md"
164
+ :class="bid.isWinning ? 'bg-success/10' : 'bg-elevated'"
165
+ >
166
+ <div class="flex items-center gap-2">
167
+ <UIcon v-if="bid.isWinning" name="i-heroicons-trophy-20-solid" class="text-success text-xs" />
168
+ <span class="text-highlighted">{{ bid.bidderName }}</span>
169
+ <UBadge v-if="bid.isAutoBid" size="xs" variant="subtle" color="info">Auto</UBadge>
170
+ </div>
171
+ <span class="font-medium text-highlighted">{{ bid.amount.formatted }}</span>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ </template>
@@ -0,0 +1,223 @@
1
+ <script setup lang="ts">
2
+ import type { Cart, CartItem, LocalizedString } from '@commercejs/types'
3
+
4
+ /**
5
+ * CCartDrawer — Slide-over panel showing cart contents.
6
+ * Opens from the right, displays compact cart items, subtotal, and action buttons.
7
+ * Designed to be triggered by `onItemAdded` from the `useCart` composable.
8
+ */
9
+
10
+ export interface CartDrawerProps {
11
+ /** Cart data */
12
+ cart: Cart | null
13
+ /** Cart line items (overrides cart.items if provided) */
14
+ items?: CartItem[]
15
+ /** Number of items in cart (overrides cart.itemCount if provided) */
16
+ itemCount?: number
17
+ /** Whether a cart operation is loading */
18
+ loading?: boolean
19
+ /** Title shown in the drawer header */
20
+ title?: string
21
+ /** Label for the "Checkout" button */
22
+ checkoutLabel?: string
23
+ /** Route for the "Checkout" button */
24
+ checkoutTo?: string
25
+ /** Label for the "View Cart" button */
26
+ viewCartLabel?: string
27
+ /** Route for the "View Cart" button */
28
+ viewCartTo?: string
29
+ /** Function to resolve the product URL from a cart item. Defaults to `/products/{productSlug || productId}` */
30
+ resolveProductUrl?: (item: CartItem) => string
31
+ }
32
+
33
+ const props = withDefaults(defineProps<CartDrawerProps>(), {
34
+ loading: false,
35
+ title: 'Your Cart',
36
+ checkoutLabel: 'Checkout',
37
+ checkoutTo: '/checkout',
38
+ viewCartLabel: 'View Full Cart',
39
+ viewCartTo: '/cart',
40
+ resolveProductUrl: (item: CartItem) => `/products/${item.productSlug || item.productId}`,
41
+ })
42
+
43
+ const emit = defineEmits<{
44
+ 'update:quantity': [itemId: string, quantity: number]
45
+ 'remove': [itemId: string]
46
+ }>()
47
+
48
+ const open = defineModel<boolean>('open', { default: false })
49
+
50
+ function t(value: LocalizedString | string | null | undefined): string {
51
+ if (!value) return ''
52
+ if (typeof value === 'string') return value
53
+ return value.en || value.ar || Object.values(value)[0] || ''
54
+ }
55
+
56
+ const cartItems = computed(() => {
57
+ if (props.items) return props.items
58
+ return props.cart?.items ?? []
59
+ })
60
+
61
+ const count = computed(() => {
62
+ if (props.itemCount != null) return props.itemCount
63
+ return props.cart?.itemCount ?? 0
64
+ })
65
+
66
+ const subtotalFormatted = computed(() => {
67
+ return props.cart?.totals?.subtotal?.formatted ?? ''
68
+ })
69
+
70
+ function handleClose() {
71
+ open.value = false
72
+ }
73
+ </script>
74
+
75
+ <template>
76
+ <USlideover
77
+ v-model:open="open"
78
+ :title="title"
79
+ side="right"
80
+ :ui="{
81
+ content: 'max-w-sm overflow-hidden',
82
+ body: 'flex flex-col p-0 overflow-hidden',
83
+ footer: 'p-4',
84
+ }"
85
+ >
86
+ <!-- Hidden default slot (controlled externally via v-model:open) -->
87
+ <template #default>
88
+ <slot name="trigger" />
89
+ </template>
90
+
91
+ <!-- Body: cart items list -->
92
+ <template #body>
93
+ <!-- Empty state -->
94
+ <div
95
+ v-if="cartItems.length === 0"
96
+ class="flex-1 flex flex-col items-center justify-center text-center py-16 px-6 gap-3"
97
+ >
98
+ <div class="w-16 h-16 rounded-full bg-elevated flex items-center justify-center mb-2">
99
+ <UIcon name="i-heroicons-shopping-cart" class="text-2xl text-muted" />
100
+ </div>
101
+ <p class="text-sm font-medium text-highlighted">Your cart is empty</p>
102
+ <p class="text-xs text-muted">Add items to get started</p>
103
+ <UButton
104
+ to="/products"
105
+ size="sm"
106
+ variant="soft"
107
+ color="primary"
108
+ class="mt-2"
109
+ @click="handleClose"
110
+ >
111
+ Browse Products
112
+ </UButton>
113
+ </div>
114
+
115
+ <!-- Items list -->
116
+ <div v-else class="flex-1 overflow-y-auto overflow-x-hidden">
117
+ <div
118
+ v-for="item in cartItems"
119
+ :key="item.id"
120
+ class="flex items-start gap-3 px-4 py-3 border-b border-(--ui-border) last:border-b-0 overflow-hidden"
121
+ >
122
+ <!-- Thumbnail -->
123
+ <NuxtLink
124
+ :to="resolveProductUrl(item)"
125
+ class="shrink-0 w-16 h-16 rounded-lg overflow-hidden bg-elevated"
126
+ @click="handleClose"
127
+ >
128
+ <img
129
+ v-if="item.image"
130
+ :src="item.image.url"
131
+ :alt="item.image.alt || t(item.name)"
132
+ class="w-full h-full object-cover"
133
+ loading="lazy"
134
+ />
135
+ <div v-else class="w-full h-full flex items-center justify-center">
136
+ <UIcon name="i-heroicons-photo" class="text-lg text-muted" />
137
+ </div>
138
+ </NuxtLink>
139
+
140
+ <!-- Item details -->
141
+ <div class="flex-1 min-w-0">
142
+ <NuxtLink
143
+ :to="resolveProductUrl(item)"
144
+ class="text-sm font-medium text-highlighted hover:text-primary transition-colors line-clamp-1 block"
145
+ @click="handleClose"
146
+ >
147
+ {{ t(item.name) }}
148
+ </NuxtLink>
149
+
150
+ <p v-if="item.variantName" class="text-xs text-muted mt-0.5">
151
+ {{ t(item.variantName) }}
152
+ </p>
153
+
154
+ <!-- Price -->
155
+ <CProductPrice
156
+ v-if="item.price"
157
+ :price="item.price"
158
+ size="xs"
159
+ class="mt-0.5"
160
+ />
161
+
162
+ <!-- Quantity + Remove -->
163
+ <div class="flex items-center justify-between mt-1.5">
164
+ <CQuantitySelector
165
+ :model-value="item.quantity"
166
+ :disabled="loading"
167
+ size="sm"
168
+ @update:model-value="emit('update:quantity', item.id, $event)"
169
+ />
170
+
171
+ <UButton
172
+ icon="i-heroicons-trash-20-solid"
173
+ variant="ghost"
174
+ color="error"
175
+ size="xs"
176
+ :loading="loading"
177
+ @click="emit('remove', item.id)"
178
+ />
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </template>
184
+
185
+ <!-- Footer: subtotal + action buttons -->
186
+ <template #footer>
187
+ <div v-if="cartItems.length > 0" class="space-y-3">
188
+ <!-- Subtotal -->
189
+ <div class="flex items-center justify-between">
190
+ <span class="text-sm text-muted">Subtotal ({{ count }} items)</span>
191
+ <span class="text-base font-bold text-highlighted">{{ subtotalFormatted }}</span>
192
+ </div>
193
+
194
+ <!-- Shipping note -->
195
+ <p class="text-xs text-dimmed">Shipping & taxes calculated at checkout</p>
196
+
197
+ <!-- Action buttons -->
198
+ <div class="flex flex-col gap-2 pt-1">
199
+ <UButton
200
+ :to="checkoutTo"
201
+ color="primary"
202
+ size="lg"
203
+ block
204
+ @click="handleClose"
205
+ >
206
+ {{ checkoutLabel }}
207
+ </UButton>
208
+
209
+ <UButton
210
+ :to="viewCartTo"
211
+ variant="ghost"
212
+ color="neutral"
213
+ size="sm"
214
+ block
215
+ @click="handleClose"
216
+ >
217
+ {{ viewCartLabel }}
218
+ </UButton>
219
+ </div>
220
+ </div>
221
+ </template>
222
+ </USlideover>
223
+ </template>
@@ -0,0 +1,136 @@
1
+ <script setup lang="ts">
2
+ import type { CartItem, LocalizedString } from '@commercejs/types'
3
+
4
+ /**
5
+ * CCartItem — Individual cart line item with image, details, quantity, and remove.
6
+ * Follows Nuxt UI conventions: ui prop, slot-based theming, semantic tokens.
7
+ */
8
+
9
+ export interface CartItemProps {
10
+ /** Cart item data from @commercejs/types */
11
+ item: CartItem
12
+ /** Whether an operation is loading */
13
+ loading?: boolean
14
+ /** Size variant */
15
+ size?: 'sm' | 'md' | 'lg'
16
+ /** Per-instance theme overrides */
17
+ ui?: Partial<{
18
+ root: any
19
+ imageWrapper: any
20
+ image: any
21
+ body: any
22
+ title: any
23
+ variant: any
24
+ priceWrapper: any
25
+ actions: any
26
+ }>
27
+ }
28
+
29
+ const props = withDefaults(defineProps<CartItemProps>(), {
30
+ loading: false,
31
+ size: 'md',
32
+ })
33
+
34
+ const emit = defineEmits<{
35
+ 'update:quantity': [value: number]
36
+ 'remove': []
37
+ }>()
38
+
39
+ function t(value: LocalizedString | string | null | undefined): string {
40
+ if (!value) return ''
41
+ if (typeof value === 'string') return value
42
+ return value.en || value.ar || Object.values(value)[0] || ''
43
+ }
44
+
45
+ const itemName = computed(() => t(props.item.name))
46
+
47
+ // Resolve theme classes from app.config
48
+ const appConfig = useAppConfig()
49
+ const theme = computed(() => (appConfig.ui as any)?.cartItem ?? {})
50
+
51
+ const slotClasses = computed(() => {
52
+ const t = theme.value
53
+ const sizeStyles = t?.variants?.size?.[props.size] ?? {}
54
+ const base = t?.slots ?? {}
55
+
56
+ const merge = (slot: string) => [
57
+ base[slot],
58
+ sizeStyles[slot],
59
+ props.ui?.[slot as keyof typeof props.ui],
60
+ ]
61
+
62
+ return {
63
+ root: merge('root'),
64
+ imageWrapper: merge('imageWrapper'),
65
+ image: merge('image'),
66
+ body: merge('body'),
67
+ title: merge('title'),
68
+ variant: merge('variant'),
69
+ priceWrapper: merge('priceWrapper'),
70
+ actions: merge('actions'),
71
+ }
72
+ })
73
+ </script>
74
+
75
+ <template>
76
+ <div :class="slotClasses.root">
77
+ <!-- Image -->
78
+ <slot name="image" :item="item">
79
+ <div :class="slotClasses.imageWrapper">
80
+ <img
81
+ v-if="item.image"
82
+ :src="item.image.url"
83
+ :alt="item.image.alt || itemName"
84
+ :class="slotClasses.image"
85
+ loading="lazy"
86
+ />
87
+ <div v-else class="size-full flex items-center justify-center">
88
+ <UIcon name="i-heroicons-photo" class="text-xl text-muted" />
89
+ </div>
90
+ </div>
91
+ </slot>
92
+
93
+ <!-- Details -->
94
+ <div :class="slotClasses.body">
95
+ <slot name="title" :name="itemName">
96
+ <h4 :class="slotClasses.title">{{ itemName }}</h4>
97
+ </slot>
98
+
99
+ <slot name="variant" :item="item">
100
+ <p v-if="item.variantId" :class="slotClasses.variant">
101
+ {{ item.variantId }}
102
+ </p>
103
+ </slot>
104
+
105
+ <!-- Price -->
106
+ <slot name="price" :price="item.price">
107
+ <div :class="slotClasses.priceWrapper">
108
+ <CProductPrice :price="item.price" size="sm" :show-discount="false" />
109
+ </div>
110
+ </slot>
111
+
112
+ <!-- Actions: quantity + remove -->
113
+ <div :class="slotClasses.actions">
114
+ <slot name="quantity" :quantity="item.quantity" :update="(v: number) => emit('update:quantity', v)">
115
+ <CQuantitySelector
116
+ :model-value="item.quantity"
117
+ :disabled="loading"
118
+ size="sm"
119
+ @update:model-value="emit('update:quantity', $event)"
120
+ />
121
+ </slot>
122
+
123
+ <slot name="remove" :remove="() => emit('remove')">
124
+ <UButton
125
+ icon="i-heroicons-trash-20-solid"
126
+ variant="ghost"
127
+ color="error"
128
+ size="sm"
129
+ :loading="loading"
130
+ @click="emit('remove')"
131
+ />
132
+ </slot>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </template>
@@ -0,0 +1,127 @@
1
+ <script setup lang="ts">
2
+ import type { Cart } from '@commercejs/types'
3
+
4
+ /**
5
+ * CCartSummary — Order summary sidebar with line items and totals.
6
+ * Follows Nuxt UI conventions: ui prop, slot-based theming, semantic tokens.
7
+ */
8
+
9
+ export interface CartSummaryProps {
10
+ /** Cart data from @commercejs/types */
11
+ cart: Cart
12
+ /** Show the checkout/action button */
13
+ showActions?: boolean
14
+ /** Label for the checkout button */
15
+ actionLabel?: string
16
+ /** Route for the checkout button */
17
+ actionTo?: string
18
+ /** Per-instance theme overrides */
19
+ ui?: Partial<{
20
+ root: any
21
+ title: any
22
+ lineItem: any
23
+ lineLabel: any
24
+ lineValue: any
25
+ separator: any
26
+ total: any
27
+ totalLabel: any
28
+ totalValue: any
29
+ actions: any
30
+ }>
31
+ }
32
+
33
+ const props = withDefaults(defineProps<CartSummaryProps>(), {
34
+ showActions: true,
35
+ actionLabel: 'Proceed to Checkout',
36
+ actionTo: '/checkout',
37
+ })
38
+
39
+ // Resolve theme classes from app.config
40
+ const appConfig = useAppConfig()
41
+ const theme = computed(() => (appConfig.ui as any)?.cartSummary ?? {})
42
+
43
+ const slotClasses = computed(() => {
44
+ const t = theme.value
45
+ const base = t?.slots ?? {}
46
+ const merge = (slot: string) => [
47
+ base[slot],
48
+ props.ui?.[slot as keyof typeof props.ui],
49
+ ]
50
+
51
+ return {
52
+ root: merge('root'),
53
+ title: merge('title'),
54
+ lineItem: merge('lineItem'),
55
+ lineLabel: merge('lineLabel'),
56
+ lineValue: merge('lineValue'),
57
+ separator: merge('separator'),
58
+ total: merge('total'),
59
+ totalLabel: merge('totalLabel'),
60
+ totalValue: merge('totalValue'),
61
+ actions: merge('actions'),
62
+ }
63
+ })
64
+ </script>
65
+
66
+ <template>
67
+ <div :class="slotClasses.root">
68
+ <slot name="title">
69
+ <h3 :class="slotClasses.title">Order Summary</h3>
70
+ </slot>
71
+
72
+ <!-- Line items -->
73
+ <div class="space-y-2">
74
+ <slot name="subtotal" :subtotal="cart.totals.subtotal">
75
+ <div :class="slotClasses.lineItem">
76
+ <span :class="slotClasses.lineLabel">Subtotal ({{ cart.itemCount }} items)</span>
77
+ <span :class="slotClasses.lineValue">{{ cart.totals.subtotal.formatted }}</span>
78
+ </div>
79
+ </slot>
80
+
81
+ <slot name="shipping" :shipping="cart.totals.shipping">
82
+ <div v-if="cart.totals.shipping" :class="slotClasses.lineItem">
83
+ <span :class="slotClasses.lineLabel">Shipping</span>
84
+ <span :class="slotClasses.lineValue">{{ cart.totals.shipping.formatted }}</span>
85
+ </div>
86
+ </slot>
87
+
88
+ <slot name="tax" :tax="cart.totals.tax">
89
+ <div v-if="cart.totals.tax" :class="slotClasses.lineItem">
90
+ <span :class="slotClasses.lineLabel">Tax</span>
91
+ <span :class="slotClasses.lineValue">{{ cart.totals.tax.formatted }}</span>
92
+ </div>
93
+ </slot>
94
+
95
+ <slot name="discount" :discount="cart.totals.discount">
96
+ <div v-if="cart.totals.discount" :class="slotClasses.lineItem">
97
+ <span :class="slotClasses.lineLabel">Discount</span>
98
+ <span class="font-medium text-success">-{{ cart.totals.discount.formatted }}</span>
99
+ </div>
100
+ </slot>
101
+ </div>
102
+
103
+ <USeparator :class="slotClasses.separator" />
104
+
105
+ <!-- Total -->
106
+ <slot name="total" :total="cart.totals.total">
107
+ <div :class="slotClasses.total">
108
+ <span :class="slotClasses.totalLabel">Total</span>
109
+ <span :class="slotClasses.totalValue">{{ cart.totals.total.formatted }}</span>
110
+ </div>
111
+ </slot>
112
+
113
+ <!-- Actions -->
114
+ <slot v-if="showActions" name="actions">
115
+ <div :class="slotClasses.actions">
116
+ <UButton
117
+ :to="actionTo"
118
+ color="primary"
119
+ size="lg"
120
+ block
121
+ >
122
+ {{ actionLabel }}
123
+ </UButton>
124
+ </div>
125
+ </slot>
126
+ </div>
127
+ </template>