@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.
- package/dist/module.cjs +5 -0
- package/dist/module.d.mts +15 -0
- package/dist/module.d.ts +15 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +30 -0
- package/dist/runtime/app.config.d.ts +0 -0
- package/dist/runtime/app.config.js +341 -0
- package/dist/runtime/components/auction/CAuctionCard.vue +213 -0
- package/dist/runtime/components/auction/CBidPanel.vue +176 -0
- package/dist/runtime/components/cart/CCartDrawer.vue +223 -0
- package/dist/runtime/components/cart/CCartItem.vue +136 -0
- package/dist/runtime/components/cart/CCartSummary.vue +127 -0
- package/dist/runtime/components/cart/CQuantitySelector.vue +110 -0
- package/dist/runtime/components/category/CCategoryFilter.vue +123 -0
- package/dist/runtime/components/checkout/CAddressForm.vue +186 -0
- package/dist/runtime/components/checkout/CCheckoutStepper.vue +84 -0
- package/dist/runtime/components/common/CEmptyState.vue +81 -0
- package/dist/runtime/components/common/CProductTypeBadge.vue +37 -0
- package/dist/runtime/components/event/CEventCard.vue +129 -0
- package/dist/runtime/components/gift-card/CGiftCardBalance.vue +119 -0
- package/dist/runtime/components/gift-card/CGiftCardForm.vue +157 -0
- package/dist/runtime/components/gift-card/CGiftCardForm.vue.backup +138 -0
- package/dist/runtime/components/marketing/CHeroBanner.vue +142 -0
- package/dist/runtime/components/navigation/CSearchBar.vue +127 -0
- package/dist/runtime/components/order/COrderCard.vue +117 -0
- package/dist/runtime/components/order/COrderTimeline.vue +99 -0
- package/dist/runtime/components/product/CProductCard.vue +206 -0
- package/dist/runtime/components/product/CProductGallery.vue +110 -0
- package/dist/runtime/components/product/CProductGrid.vue +82 -0
- package/dist/runtime/components/product/CProductOptions.vue +101 -0
- package/dist/runtime/components/product/CProductPrice.vue +87 -0
- package/dist/runtime/components/promotion/CCouponInput.vue +104 -0
- package/dist/runtime/components/promotion/CPromoBanner.vue +153 -0
- package/dist/runtime/components/rental/CRentalBookingForm.vue +214 -0
- package/dist/runtime/components/rental/CRentalCard.vue +146 -0
- package/dist/runtime/components/review/CReviewCard.vue +96 -0
- package/dist/runtime/components/review/CReviewStars.vue +106 -0
- package/dist/runtime/components/subscription/CSubscriptionCard.vue +137 -0
- package/dist/runtime/components/wholesale/CPriceTierTable.vue +88 -0
- package/dist/runtime/components/wholesale/CQuoteRequestForm.vue +148 -0
- package/dist/runtime/components/wishlist/CWishlistGrid.vue +96 -0
- package/dist/types.d.mts +7 -0
- package/dist/types.d.ts +7 -0
- package/package.json +41 -0
- package/src/module.ts +52 -0
- package/src/runtime/app.config.ts +392 -0
- package/src/runtime/components/auction/CAuctionCard.vue +213 -0
- package/src/runtime/components/auction/CBidPanel.vue +176 -0
- package/src/runtime/components/cart/CCartDrawer.vue +223 -0
- package/src/runtime/components/cart/CCartItem.vue +136 -0
- package/src/runtime/components/cart/CCartSummary.vue +127 -0
- package/src/runtime/components/cart/CQuantitySelector.vue +110 -0
- package/src/runtime/components/category/CCategoryFilter.vue +123 -0
- package/src/runtime/components/checkout/CAddressForm.vue +186 -0
- package/src/runtime/components/checkout/CCheckoutStepper.vue +84 -0
- package/src/runtime/components/common/CEmptyState.vue +81 -0
- package/src/runtime/components/common/CProductTypeBadge.vue +37 -0
- package/src/runtime/components/event/CEventCard.vue +129 -0
- package/src/runtime/components/gift-card/CGiftCardBalance.vue +119 -0
- package/src/runtime/components/gift-card/CGiftCardForm.vue +157 -0
- package/src/runtime/components/gift-card/CGiftCardForm.vue.backup +138 -0
- package/src/runtime/components/marketing/CHeroBanner.vue +142 -0
- package/src/runtime/components/navigation/CSearchBar.vue +127 -0
- package/src/runtime/components/order/COrderCard.vue +117 -0
- package/src/runtime/components/order/COrderTimeline.vue +99 -0
- package/src/runtime/components/product/CProductCard.vue +206 -0
- package/src/runtime/components/product/CProductGallery.vue +110 -0
- package/src/runtime/components/product/CProductGrid.vue +82 -0
- package/src/runtime/components/product/CProductOptions.vue +101 -0
- package/src/runtime/components/product/CProductPrice.vue +87 -0
- package/src/runtime/components/promotion/CCouponInput.vue +104 -0
- package/src/runtime/components/promotion/CPromoBanner.vue +153 -0
- package/src/runtime/components/rental/CRentalBookingForm.vue +214 -0
- package/src/runtime/components/rental/CRentalCard.vue +146 -0
- package/src/runtime/components/review/CReviewCard.vue +96 -0
- package/src/runtime/components/review/CReviewStars.vue +106 -0
- package/src/runtime/components/subscription/CSubscriptionCard.vue +137 -0
- package/src/runtime/components/wholesale/CPriceTierTable.vue +88 -0
- package/src/runtime/components/wholesale/CQuoteRequestForm.vue +148 -0
- package/src/runtime/components/wishlist/CWishlistGrid.vue +96 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { PriceTier } from '@commercejs/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CPriceTierTable — Displays volume-based pricing tiers for B2B/wholesale products.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface PriceTierTableProps {
|
|
9
|
+
/** Price tiers from product.priceTiers */
|
|
10
|
+
tiers: PriceTier[]
|
|
11
|
+
/** Currently selected / applicable quantity */
|
|
12
|
+
currentQuantity?: number
|
|
13
|
+
/** Per-instance theme overrides */
|
|
14
|
+
ui?: Partial<{
|
|
15
|
+
root: any
|
|
16
|
+
row: any
|
|
17
|
+
activeRow: any
|
|
18
|
+
}>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(defineProps<PriceTierTableProps>(), {
|
|
22
|
+
currentQuantity: 0,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function t(value: any): string {
|
|
26
|
+
if (!value) return ''
|
|
27
|
+
if (typeof value === 'string') return value
|
|
28
|
+
return value.en || value.ar || Object.values(value)[0] || ''
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isActiveTier(tier: PriceTier): boolean {
|
|
32
|
+
if (props.currentQuantity < tier.minQuantity) return false
|
|
33
|
+
if (tier.maxQuantity && props.currentQuantity > tier.maxQuantity) return false
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Resolve theme from app.config
|
|
38
|
+
const appConfig = useAppConfig()
|
|
39
|
+
const theme = computed(() => (appConfig.ui as any)?.priceTierTable ?? {})
|
|
40
|
+
|
|
41
|
+
const slotClasses = computed(() => {
|
|
42
|
+
const base = theme.value?.slots ?? {}
|
|
43
|
+
const merge = (slot: string) => [base[slot], props.ui?.[slot as keyof typeof props.ui]]
|
|
44
|
+
return {
|
|
45
|
+
root: merge('root'),
|
|
46
|
+
row: merge('row'),
|
|
47
|
+
activeRow: merge('activeRow'),
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<div :class="['overflow-hidden rounded-lg ring ring-default', slotClasses.root]">
|
|
54
|
+
<table class="w-full text-sm">
|
|
55
|
+
<thead class="bg-elevated text-muted">
|
|
56
|
+
<tr>
|
|
57
|
+
<th class="text-start py-2 px-3 font-medium">Quantity</th>
|
|
58
|
+
<th class="text-start py-2 px-3 font-medium">Price / Unit</th>
|
|
59
|
+
<th v-if="tiers.some(t => t.label)" class="text-start py-2 px-3 font-medium">Tier</th>
|
|
60
|
+
</tr>
|
|
61
|
+
</thead>
|
|
62
|
+
<tbody>
|
|
63
|
+
<tr
|
|
64
|
+
v-for="(tier, i) in tiers"
|
|
65
|
+
:key="i"
|
|
66
|
+
:class="[
|
|
67
|
+
'border-t border-default transition-colors',
|
|
68
|
+
isActiveTier(tier) ? 'bg-primary/5 font-medium' : '',
|
|
69
|
+
slotClasses.row,
|
|
70
|
+
isActiveTier(tier) ? slotClasses.activeRow : '',
|
|
71
|
+
]"
|
|
72
|
+
>
|
|
73
|
+
<td class="py-2.5 px-3 text-highlighted">
|
|
74
|
+
{{ tier.minQuantity }}{{ tier.maxQuantity ? ` – ${tier.maxQuantity}` : '+' }}
|
|
75
|
+
</td>
|
|
76
|
+
<td class="py-2.5 px-3">
|
|
77
|
+
<span :class="isActiveTier(tier) ? 'text-primary font-semibold' : 'text-highlighted'">
|
|
78
|
+
{{ tier.unitPrice.formatted }}
|
|
79
|
+
</span>
|
|
80
|
+
</td>
|
|
81
|
+
<td v-if="tiers.some(t => t.label)" class="py-2.5 px-3 text-muted">
|
|
82
|
+
{{ t(tier.label) }}
|
|
83
|
+
</td>
|
|
84
|
+
</tr>
|
|
85
|
+
</tbody>
|
|
86
|
+
</table>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CreateQuoteInput } from '@commercejs/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CQuoteRequestForm — B2B Request-for-Quote form.
|
|
6
|
+
* Allows buyers to submit RFQ with line items, target prices, and company info.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface QuoteRequestFormProps {
|
|
10
|
+
/** Pre-populated items (e.g., from cart) */
|
|
11
|
+
initialItems?: Array<{
|
|
12
|
+
productId: string
|
|
13
|
+
productName?: string
|
|
14
|
+
quantity: number
|
|
15
|
+
targetPrice?: number
|
|
16
|
+
}>
|
|
17
|
+
/** Whether the form is submitting */
|
|
18
|
+
loading?: boolean
|
|
19
|
+
/** Per-instance theme overrides */
|
|
20
|
+
ui?: Partial<{
|
|
21
|
+
root: any
|
|
22
|
+
items: any
|
|
23
|
+
contact: any
|
|
24
|
+
}>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const props = withDefaults(defineProps<QuoteRequestFormProps>(), {
|
|
28
|
+
initialItems: () => [],
|
|
29
|
+
loading: false,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const emit = defineEmits<{
|
|
33
|
+
'submit': [input: CreateQuoteInput]
|
|
34
|
+
}>()
|
|
35
|
+
|
|
36
|
+
// Form state
|
|
37
|
+
const items = ref(props.initialItems.map(item => ({
|
|
38
|
+
productId: item.productId,
|
|
39
|
+
productName: item.productName || '',
|
|
40
|
+
quantity: item.quantity,
|
|
41
|
+
targetPrice: item.targetPrice,
|
|
42
|
+
note: '',
|
|
43
|
+
})))
|
|
44
|
+
|
|
45
|
+
const companyName = ref('')
|
|
46
|
+
const contactEmail = ref('')
|
|
47
|
+
const note = ref('')
|
|
48
|
+
|
|
49
|
+
function addItem() {
|
|
50
|
+
items.value.push({ productId: '', productName: '', quantity: 1, targetPrice: undefined, note: '' })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function removeItem(index: number) {
|
|
54
|
+
items.value.splice(index, 1)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function handleSubmit() {
|
|
58
|
+
emit('submit', {
|
|
59
|
+
items: items.value.map(item => ({
|
|
60
|
+
productId: item.productId,
|
|
61
|
+
quantity: item.quantity,
|
|
62
|
+
targetPrice: item.targetPrice,
|
|
63
|
+
note: item.note || undefined,
|
|
64
|
+
})),
|
|
65
|
+
companyName: companyName.value || undefined,
|
|
66
|
+
contactEmail: contactEmail.value,
|
|
67
|
+
note: note.value || undefined,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Resolve theme from app.config
|
|
72
|
+
const appConfig = useAppConfig()
|
|
73
|
+
const theme = computed(() => (appConfig.ui as any)?.quoteRequestForm ?? {})
|
|
74
|
+
|
|
75
|
+
const slotClasses = computed(() => {
|
|
76
|
+
const base = theme.value?.slots ?? {}
|
|
77
|
+
const merge = (slot: string) => [base[slot], props.ui?.[slot as keyof typeof props.ui]]
|
|
78
|
+
return {
|
|
79
|
+
root: merge('root'),
|
|
80
|
+
items: merge('items'),
|
|
81
|
+
contact: merge('contact'),
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<template>
|
|
87
|
+
<form :class="['space-y-6', slotClasses.root]" @submit.prevent="handleSubmit">
|
|
88
|
+
<h3 class="text-lg font-semibold text-highlighted">Request for Quote</h3>
|
|
89
|
+
|
|
90
|
+
<!-- Line items -->
|
|
91
|
+
<div :class="['space-y-4', slotClasses.items]">
|
|
92
|
+
<div
|
|
93
|
+
v-for="(item, i) in items"
|
|
94
|
+
:key="i"
|
|
95
|
+
class="rounded-lg bg-elevated p-4 ring ring-default relative"
|
|
96
|
+
>
|
|
97
|
+
<UButton
|
|
98
|
+
v-if="items.length > 1"
|
|
99
|
+
icon="i-heroicons-x-mark-20-solid"
|
|
100
|
+
variant="ghost"
|
|
101
|
+
color="error"
|
|
102
|
+
size="xs"
|
|
103
|
+
class="absolute top-2 end-2"
|
|
104
|
+
@click="removeItem(i)"
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
108
|
+
<UFormField label="Product">
|
|
109
|
+
<UInput v-model="item.productName" :placeholder="item.productId || 'Product ID'" disabled size="sm" />
|
|
110
|
+
</UFormField>
|
|
111
|
+
<UFormField label="Quantity">
|
|
112
|
+
<UInput v-model.number="item.quantity" type="number" min="1" size="sm" required />
|
|
113
|
+
</UFormField>
|
|
114
|
+
<UFormField label="Target Price (optional)">
|
|
115
|
+
<UInput v-model.number="item.targetPrice" type="number" min="0" step="0.01" placeholder="Your ideal price" size="sm" />
|
|
116
|
+
</UFormField>
|
|
117
|
+
</div>
|
|
118
|
+
<UFormField label="Note" class="mt-2">
|
|
119
|
+
<UInput v-model="item.note" placeholder="Special requirements…" size="sm" />
|
|
120
|
+
</UFormField>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<UButton variant="outline" size="sm" @click="addItem" icon="i-heroicons-plus-20-solid">
|
|
124
|
+
Add Item
|
|
125
|
+
</UButton>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<USeparator />
|
|
129
|
+
|
|
130
|
+
<!-- Contact info -->
|
|
131
|
+
<div :class="['grid grid-cols-1 sm:grid-cols-2 gap-4', slotClasses.contact]">
|
|
132
|
+
<UFormField label="Company Name">
|
|
133
|
+
<UInput v-model="companyName" placeholder="Your company" />
|
|
134
|
+
</UFormField>
|
|
135
|
+
<UFormField label="Contact Email" required>
|
|
136
|
+
<UInput v-model="contactEmail" type="email" placeholder="buyer@company.com" required />
|
|
137
|
+
</UFormField>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<UFormField label="Additional Notes">
|
|
141
|
+
<UTextarea v-model="note" placeholder="Any additional requirements, delivery timeline, etc." rows="3" />
|
|
142
|
+
</UFormField>
|
|
143
|
+
|
|
144
|
+
<UButton type="submit" block size="lg" color="primary" :loading="loading">
|
|
145
|
+
Submit Quote Request
|
|
146
|
+
</UButton>
|
|
147
|
+
</form>
|
|
148
|
+
</template>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { WishlistItem, Product } from '@commercejs/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CWishlistGrid — Displays wishlist items in a responsive grid.
|
|
6
|
+
* Composes CProductCard for each item with remove + add-to-cart actions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface WishlistGridProps {
|
|
10
|
+
items: WishlistItem[]
|
|
11
|
+
/** Number of grid columns (2–6) */
|
|
12
|
+
columns?: 2 | 3 | 4 | 5 | 6
|
|
13
|
+
/** Whether any action is loading */
|
|
14
|
+
loading?: boolean
|
|
15
|
+
/** Per-instance theme overrides */
|
|
16
|
+
ui?: Partial<{
|
|
17
|
+
root: any
|
|
18
|
+
item: any
|
|
19
|
+
actions: any
|
|
20
|
+
}>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(defineProps<WishlistGridProps>(), {
|
|
24
|
+
columns: 4,
|
|
25
|
+
loading: false,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits<{
|
|
29
|
+
'remove': [itemId: string]
|
|
30
|
+
'add-to-cart': [product: Product]
|
|
31
|
+
}>()
|
|
32
|
+
|
|
33
|
+
const colClass = computed(() => {
|
|
34
|
+
const map: Record<number, string> = {
|
|
35
|
+
2: 'grid-cols-1 sm:grid-cols-2',
|
|
36
|
+
3: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3',
|
|
37
|
+
4: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4',
|
|
38
|
+
5: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5',
|
|
39
|
+
6: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6',
|
|
40
|
+
}
|
|
41
|
+
return map[props.columns] || map[4]
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Resolve theme
|
|
45
|
+
const appConfig = useAppConfig()
|
|
46
|
+
const theme = computed(() => (appConfig.ui as any)?.wishlistGrid ?? {})
|
|
47
|
+
|
|
48
|
+
const slotClasses = computed(() => {
|
|
49
|
+
const base = theme.value?.slots ?? {}
|
|
50
|
+
const merge = (slot: string) => [base[slot], props.ui?.[slot as keyof typeof props.ui]]
|
|
51
|
+
return {
|
|
52
|
+
root: merge('root'),
|
|
53
|
+
item: merge('item'),
|
|
54
|
+
actions: merge('actions'),
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<template>
|
|
60
|
+
<div v-if="items.length === 0">
|
|
61
|
+
<CEmptyState
|
|
62
|
+
icon="i-heroicons-heart"
|
|
63
|
+
title="Your wishlist is empty"
|
|
64
|
+
description="Save your favorite products to find them later"
|
|
65
|
+
action-label="Start Shopping"
|
|
66
|
+
action-to="/products"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div v-else :class="['grid gap-4', colClass, slotClasses.root]">
|
|
71
|
+
<div v-for="item in items" :key="item.id" :class="['relative group', slotClasses.item]">
|
|
72
|
+
<CProductCard :product="item.product" />
|
|
73
|
+
|
|
74
|
+
<!-- Wishlist-specific actions overlay -->
|
|
75
|
+
<div :class="['absolute bottom-0 inset-x-0 bg-gradient-to-t from-black/60 to-transparent p-3 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity', slotClasses.actions]">
|
|
76
|
+
<UButton
|
|
77
|
+
size="xs"
|
|
78
|
+
color="primary"
|
|
79
|
+
class="flex-1"
|
|
80
|
+
:loading="loading"
|
|
81
|
+
@click="emit('add-to-cart', item.product)"
|
|
82
|
+
>
|
|
83
|
+
<UIcon name="i-heroicons-shopping-cart-20-solid" class="me-1" />
|
|
84
|
+
Add to Cart
|
|
85
|
+
</UButton>
|
|
86
|
+
<UButton
|
|
87
|
+
icon="i-heroicons-trash-20-solid"
|
|
88
|
+
size="xs"
|
|
89
|
+
variant="ghost"
|
|
90
|
+
color="error"
|
|
91
|
+
@click="emit('remove', item.id)"
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|