@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,110 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CQuantitySelector — Increment/decrement quantity input.
|
|
4
|
+
* Follows Nuxt UI conventions: ui prop, slot-based theming, semantic tokens.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface QuantitySelectorProps {
|
|
8
|
+
/** Current quantity value */
|
|
9
|
+
modelValue: number
|
|
10
|
+
/** Minimum allowed value */
|
|
11
|
+
min?: number
|
|
12
|
+
/** Maximum allowed value (null = unlimited) */
|
|
13
|
+
max?: number | null
|
|
14
|
+
/** Disable the control */
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
/** Size variant */
|
|
17
|
+
size?: 'sm' | 'md' | 'lg'
|
|
18
|
+
/** Per-instance theme overrides */
|
|
19
|
+
ui?: Partial<{
|
|
20
|
+
root: any
|
|
21
|
+
button: any
|
|
22
|
+
input: any
|
|
23
|
+
}>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(defineProps<QuantitySelectorProps>(), {
|
|
27
|
+
min: 1,
|
|
28
|
+
max: null,
|
|
29
|
+
disabled: false,
|
|
30
|
+
size: 'md',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const emit = defineEmits<{
|
|
34
|
+
'update:modelValue': [value: number]
|
|
35
|
+
}>()
|
|
36
|
+
|
|
37
|
+
const canDecrement = computed(() => props.modelValue > props.min)
|
|
38
|
+
const canIncrement = computed(() => props.max === null || props.modelValue < props.max)
|
|
39
|
+
|
|
40
|
+
function decrement() {
|
|
41
|
+
if (canDecrement.value && !props.disabled) {
|
|
42
|
+
emit('update:modelValue', props.modelValue - 1)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function increment() {
|
|
47
|
+
if (canIncrement.value && !props.disabled) {
|
|
48
|
+
emit('update:modelValue', props.modelValue + 1)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Resolve theme classes from app.config
|
|
53
|
+
const appConfig = useAppConfig()
|
|
54
|
+
const theme = computed(() => (appConfig.ui as any)?.quantitySelector ?? {})
|
|
55
|
+
|
|
56
|
+
const buttonSize = computed(() => {
|
|
57
|
+
const map = { sm: 'xs', md: 'sm', lg: 'md' } as const
|
|
58
|
+
return map[props.size] ?? 'sm'
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const slotClasses = computed(() => {
|
|
62
|
+
const t = theme.value
|
|
63
|
+
const sizeVariant = t?.variants?.size?.[props.size] ?? {}
|
|
64
|
+
const base = t?.slots ?? {}
|
|
65
|
+
return {
|
|
66
|
+
root: [base.root, sizeVariant.root, props.ui?.root],
|
|
67
|
+
button: [base.button, sizeVariant.button, props.ui?.button],
|
|
68
|
+
input: [base.input, sizeVariant.input, props.ui?.input],
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<template>
|
|
74
|
+
<div :class="slotClasses.root">
|
|
75
|
+
<slot name="decrement" :decrement="decrement" :disabled="!canDecrement || disabled">
|
|
76
|
+
<UButton
|
|
77
|
+
icon="i-heroicons-minus-20-solid"
|
|
78
|
+
:size="buttonSize"
|
|
79
|
+
variant="soft"
|
|
80
|
+
color="neutral"
|
|
81
|
+
:disabled="!canDecrement || disabled"
|
|
82
|
+
:class="slotClasses.button"
|
|
83
|
+
@click="decrement"
|
|
84
|
+
/>
|
|
85
|
+
</slot>
|
|
86
|
+
|
|
87
|
+
<slot name="value" :value="modelValue">
|
|
88
|
+
<input
|
|
89
|
+
type="text"
|
|
90
|
+
:value="modelValue"
|
|
91
|
+
readonly
|
|
92
|
+
:disabled="disabled"
|
|
93
|
+
:class="slotClasses.input"
|
|
94
|
+
aria-label="Quantity"
|
|
95
|
+
/>
|
|
96
|
+
</slot>
|
|
97
|
+
|
|
98
|
+
<slot name="increment" :increment="increment" :disabled="!canIncrement || disabled">
|
|
99
|
+
<UButton
|
|
100
|
+
icon="i-heroicons-plus-20-solid"
|
|
101
|
+
:size="buttonSize"
|
|
102
|
+
variant="soft"
|
|
103
|
+
color="neutral"
|
|
104
|
+
:disabled="!canIncrement || disabled"
|
|
105
|
+
:class="slotClasses.button"
|
|
106
|
+
@click="increment"
|
|
107
|
+
/>
|
|
108
|
+
</slot>
|
|
109
|
+
</div>
|
|
110
|
+
</template>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Facet, FacetValue, LocalizedString } from '@commercejs/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CCategoryFilter — Faceted category/attribute sidebar filter.
|
|
6
|
+
* Displays facet groups with checkable values and counts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface CategoryFilterProps {
|
|
10
|
+
/** Available facets from search response */
|
|
11
|
+
facets: Facet[]
|
|
12
|
+
/** Currently selected filters: { facetCode: [valueId, ...] } */
|
|
13
|
+
modelValue: Record<string, string[]>
|
|
14
|
+
/** Whether to show value counts */
|
|
15
|
+
showCounts?: boolean
|
|
16
|
+
/** Max visible values per facet before "Show more" */
|
|
17
|
+
maxVisible?: number
|
|
18
|
+
/** Per-instance theme overrides */
|
|
19
|
+
ui?: Partial<{
|
|
20
|
+
root: any
|
|
21
|
+
group: any
|
|
22
|
+
groupTitle: any
|
|
23
|
+
values: any
|
|
24
|
+
value: any
|
|
25
|
+
count: any
|
|
26
|
+
showMore: any
|
|
27
|
+
}>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const props = withDefaults(defineProps<CategoryFilterProps>(), {
|
|
31
|
+
showCounts: true,
|
|
32
|
+
maxVisible: 5,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const emit = defineEmits<{
|
|
36
|
+
'update:modelValue': [value: Record<string, string[]>]
|
|
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
|
+
// Track which facets are expanded
|
|
46
|
+
const expanded = reactive<Record<string, boolean>>({})
|
|
47
|
+
|
|
48
|
+
function isExpanded(code: string): boolean {
|
|
49
|
+
return expanded[code] ?? false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toggleExpanded(code: string) {
|
|
53
|
+
expanded[code] = !expanded[code]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getVisibleItems(facet: Facet) {
|
|
57
|
+
const values = isExpanded(facet.code) || facet.values.length <= props.maxVisible
|
|
58
|
+
? facet.values
|
|
59
|
+
: facet.values.slice(0, props.maxVisible)
|
|
60
|
+
|
|
61
|
+
return values.map(val => ({
|
|
62
|
+
label: t(val.label),
|
|
63
|
+
value: val.value,
|
|
64
|
+
count: val.count,
|
|
65
|
+
}))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getSelectedValues(facetCode: string): string[] {
|
|
69
|
+
return props.modelValue[facetCode] ?? []
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function updateSelection(facetCode: string, values: string[]) {
|
|
73
|
+
emit('update:modelValue', {
|
|
74
|
+
...props.modelValue,
|
|
75
|
+
[facetCode]: values,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Resolve theme from app.config
|
|
80
|
+
const appConfig = useAppConfig()
|
|
81
|
+
const theme = computed(() => (appConfig.ui as any)?.categoryFilter ?? {})
|
|
82
|
+
|
|
83
|
+
const slotClasses = computed(() => {
|
|
84
|
+
const base = theme.value?.slots ?? {}
|
|
85
|
+
const merge = (slot: string) => [
|
|
86
|
+
base[slot],
|
|
87
|
+
props.ui?.[slot as keyof typeof props.ui],
|
|
88
|
+
]
|
|
89
|
+
return {
|
|
90
|
+
root: merge('root'),
|
|
91
|
+
group: merge('group'),
|
|
92
|
+
showMore: merge('showMore'),
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<template>
|
|
98
|
+
<div :class="slotClasses.root">
|
|
99
|
+
<div v-for="facet in facets" :key="facet.code" :class="slotClasses.group">
|
|
100
|
+
<UCheckboxGroup
|
|
101
|
+
:legend="t(facet.name)"
|
|
102
|
+
:items="getVisibleItems(facet)"
|
|
103
|
+
:model-value="getSelectedValues(facet.code)"
|
|
104
|
+
@update:model-value="updateSelection(facet.code, $event as string[])"
|
|
105
|
+
>
|
|
106
|
+
<template #label="{ item }">
|
|
107
|
+
{{ item.label }}
|
|
108
|
+
<UBadge v-if="showCounts && item.count != null" size="xs" color="neutral" variant="subtle" :label="item.count?.toString()" />
|
|
109
|
+
</template>
|
|
110
|
+
</UCheckboxGroup>
|
|
111
|
+
|
|
112
|
+
<UButton
|
|
113
|
+
v-if="facet.values.length > maxVisible"
|
|
114
|
+
:class="slotClasses.showMore"
|
|
115
|
+
variant="link"
|
|
116
|
+
size="sm"
|
|
117
|
+
@click="toggleExpanded(facet.code)"
|
|
118
|
+
>
|
|
119
|
+
{{ isExpanded(facet.code) ? 'Show less' : `Show ${facet.values.length - maxVisible} more` }}
|
|
120
|
+
</UButton>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</template>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Address } from '@commercejs/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CAddressForm — Shipping/billing address form.
|
|
6
|
+
* Uses Nuxt UI form components (UInput, USelect) with GCC-specific fields.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface AddressFormProps {
|
|
10
|
+
/** Current address values */
|
|
11
|
+
modelValue: Partial<Address>
|
|
12
|
+
/** Available countries for the dropdown */
|
|
13
|
+
countries?: { label: string; value: string }[]
|
|
14
|
+
/** Whether to show GCC-specific fields (district, nationalAddress) */
|
|
15
|
+
showGccFields?: boolean
|
|
16
|
+
/** Whether the form is in loading/submitting state */
|
|
17
|
+
loading?: boolean
|
|
18
|
+
/** Per-instance theme overrides */
|
|
19
|
+
ui?: Partial<{
|
|
20
|
+
root: any
|
|
21
|
+
row: any
|
|
22
|
+
field: any
|
|
23
|
+
}>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(defineProps<AddressFormProps>(), {
|
|
27
|
+
countries: () => [],
|
|
28
|
+
showGccFields: true,
|
|
29
|
+
loading: false,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const emit = defineEmits<{
|
|
33
|
+
'update:modelValue': [value: Partial<Address>]
|
|
34
|
+
'submit': [value: Partial<Address>]
|
|
35
|
+
}>()
|
|
36
|
+
|
|
37
|
+
function update(field: keyof Address, value: any) {
|
|
38
|
+
emit('update:modelValue', { ...props.modelValue, [field]: value })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handleSubmit() {
|
|
42
|
+
emit('submit', props.modelValue)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Resolve theme from app.config
|
|
46
|
+
const appConfig = useAppConfig()
|
|
47
|
+
const theme = computed(() => (appConfig.ui as any)?.addressForm ?? {})
|
|
48
|
+
|
|
49
|
+
const slotClasses = computed(() => {
|
|
50
|
+
const base = theme.value?.slots ?? {}
|
|
51
|
+
return {
|
|
52
|
+
root: [base.root, props.ui?.root],
|
|
53
|
+
row: [base.row, props.ui?.row],
|
|
54
|
+
field: [base.field, props.ui?.field],
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<template>
|
|
60
|
+
<form :class="slotClasses.root" @submit.prevent="handleSubmit">
|
|
61
|
+
<!-- Name row -->
|
|
62
|
+
<div :class="slotClasses.row">
|
|
63
|
+
<UFormField label="First Name" :class="slotClasses.field">
|
|
64
|
+
<UInput
|
|
65
|
+
:model-value="modelValue.firstName || ''"
|
|
66
|
+
placeholder="First name"
|
|
67
|
+
required
|
|
68
|
+
:disabled="loading"
|
|
69
|
+
@update:model-value="update('firstName', $event)"
|
|
70
|
+
/>
|
|
71
|
+
</UFormField>
|
|
72
|
+
<UFormField label="Last Name" :class="slotClasses.field">
|
|
73
|
+
<UInput
|
|
74
|
+
:model-value="modelValue.lastName || ''"
|
|
75
|
+
placeholder="Last name"
|
|
76
|
+
required
|
|
77
|
+
:disabled="loading"
|
|
78
|
+
@update:model-value="update('lastName', $event)"
|
|
79
|
+
/>
|
|
80
|
+
</UFormField>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<!-- Phone -->
|
|
84
|
+
<UFormField label="Phone">
|
|
85
|
+
<UInput
|
|
86
|
+
:model-value="modelValue.phone || ''"
|
|
87
|
+
placeholder="+966 5xx xxx xxxx"
|
|
88
|
+
type="tel"
|
|
89
|
+
:disabled="loading"
|
|
90
|
+
@update:model-value="update('phone', $event)"
|
|
91
|
+
/>
|
|
92
|
+
</UFormField>
|
|
93
|
+
|
|
94
|
+
<!-- Street -->
|
|
95
|
+
<UFormField label="Street Address">
|
|
96
|
+
<UInput
|
|
97
|
+
:model-value="modelValue.street || ''"
|
|
98
|
+
placeholder="Street address"
|
|
99
|
+
required
|
|
100
|
+
:disabled="loading"
|
|
101
|
+
@update:model-value="update('street', $event)"
|
|
102
|
+
/>
|
|
103
|
+
</UFormField>
|
|
104
|
+
|
|
105
|
+
<!-- Street 2 -->
|
|
106
|
+
<UFormField label="Apt, Suite, Floor">
|
|
107
|
+
<UInput
|
|
108
|
+
:model-value="modelValue.street2 || ''"
|
|
109
|
+
placeholder="Apartment, suite, etc. (optional)"
|
|
110
|
+
:disabled="loading"
|
|
111
|
+
@update:model-value="update('street2', $event)"
|
|
112
|
+
/>
|
|
113
|
+
</UFormField>
|
|
114
|
+
|
|
115
|
+
<!-- City + State -->
|
|
116
|
+
<div :class="slotClasses.row">
|
|
117
|
+
<UFormField label="City" :class="slotClasses.field">
|
|
118
|
+
<UInput
|
|
119
|
+
:model-value="modelValue.city || ''"
|
|
120
|
+
placeholder="City"
|
|
121
|
+
required
|
|
122
|
+
:disabled="loading"
|
|
123
|
+
@update:model-value="update('city', $event)"
|
|
124
|
+
/>
|
|
125
|
+
</UFormField>
|
|
126
|
+
<UFormField label="State / Province" :class="slotClasses.field">
|
|
127
|
+
<UInput
|
|
128
|
+
:model-value="modelValue.state || ''"
|
|
129
|
+
placeholder="State"
|
|
130
|
+
:disabled="loading"
|
|
131
|
+
@update:model-value="update('state', $event)"
|
|
132
|
+
/>
|
|
133
|
+
</UFormField>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- Country + Postal -->
|
|
137
|
+
<div :class="slotClasses.row">
|
|
138
|
+
<UFormField label="Country" :class="slotClasses.field">
|
|
139
|
+
<USelect
|
|
140
|
+
:model-value="modelValue.country || ''"
|
|
141
|
+
:items="countries"
|
|
142
|
+
placeholder="Select country"
|
|
143
|
+
required
|
|
144
|
+
:disabled="loading"
|
|
145
|
+
@update:model-value="update('country', $event)"
|
|
146
|
+
/>
|
|
147
|
+
</UFormField>
|
|
148
|
+
<UFormField label="Postal Code" :class="slotClasses.field">
|
|
149
|
+
<UInput
|
|
150
|
+
:model-value="modelValue.postalCode || ''"
|
|
151
|
+
placeholder="Postal code"
|
|
152
|
+
:disabled="loading"
|
|
153
|
+
@update:model-value="update('postalCode', $event)"
|
|
154
|
+
/>
|
|
155
|
+
</UFormField>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<!-- GCC-specific fields -->
|
|
159
|
+
<template v-if="showGccFields">
|
|
160
|
+
<slot name="gcc-fields">
|
|
161
|
+
<UFormField label="District (حي)">
|
|
162
|
+
<UInput
|
|
163
|
+
:model-value="modelValue.district || ''"
|
|
164
|
+
placeholder="District / Neighborhood"
|
|
165
|
+
:disabled="loading"
|
|
166
|
+
@update:model-value="update('district', $event)"
|
|
167
|
+
/>
|
|
168
|
+
</UFormField>
|
|
169
|
+
<UFormField label="National Address (العنوان الوطني)">
|
|
170
|
+
<UInput
|
|
171
|
+
:model-value="modelValue.nationalAddress || ''"
|
|
172
|
+
placeholder="Saudi National Address"
|
|
173
|
+
:disabled="loading"
|
|
174
|
+
@update:model-value="update('nationalAddress', $event)"
|
|
175
|
+
/>
|
|
176
|
+
</UFormField>
|
|
177
|
+
</slot>
|
|
178
|
+
</template>
|
|
179
|
+
|
|
180
|
+
<slot name="actions">
|
|
181
|
+
<UButton type="submit" color="primary" :loading="loading" block>
|
|
182
|
+
Save Address
|
|
183
|
+
</UButton>
|
|
184
|
+
</slot>
|
|
185
|
+
</form>
|
|
186
|
+
</template>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CCheckoutStepper — Multi-step checkout progress indicator.
|
|
4
|
+
* Wraps Nuxt UI's UStepper with ecommerce-specific defaults.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface CheckoutStep {
|
|
8
|
+
/** Step key */
|
|
9
|
+
id: string
|
|
10
|
+
/** Display label */
|
|
11
|
+
title: string
|
|
12
|
+
/** Optional description */
|
|
13
|
+
description?: string
|
|
14
|
+
/** Optional icon name */
|
|
15
|
+
icon?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CheckoutStepperProps {
|
|
19
|
+
/** Steps configuration */
|
|
20
|
+
steps: CheckoutStep[]
|
|
21
|
+
/** Current active step (0-indexed) */
|
|
22
|
+
modelValue?: number
|
|
23
|
+
/** Orientation */
|
|
24
|
+
orientation?: 'horizontal' | 'vertical'
|
|
25
|
+
/** Whether steps must be completed in order */
|
|
26
|
+
linear?: boolean
|
|
27
|
+
/** Size variant */
|
|
28
|
+
size?: 'sm' | 'md' | 'lg'
|
|
29
|
+
/** Color */
|
|
30
|
+
color?: 'primary' | 'secondary' | 'success' | 'neutral'
|
|
31
|
+
/** Per-instance theme overrides */
|
|
32
|
+
ui?: Partial<{
|
|
33
|
+
root: any
|
|
34
|
+
}>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const props = withDefaults(defineProps<CheckoutStepperProps>(), {
|
|
38
|
+
modelValue: 0,
|
|
39
|
+
orientation: 'horizontal',
|
|
40
|
+
linear: true,
|
|
41
|
+
size: 'md',
|
|
42
|
+
color: 'primary',
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const emit = defineEmits<{
|
|
46
|
+
'update:modelValue': [step: number]
|
|
47
|
+
}>()
|
|
48
|
+
|
|
49
|
+
// Map our steps to UStepper items format
|
|
50
|
+
const stepperItems = computed(() =>
|
|
51
|
+
props.steps.map(step => ({
|
|
52
|
+
title: step.title,
|
|
53
|
+
description: step.description,
|
|
54
|
+
icon: step.icon,
|
|
55
|
+
}))
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// Resolve theme from app.config
|
|
59
|
+
const appConfig = useAppConfig()
|
|
60
|
+
const theme = computed(() => (appConfig.ui as any)?.checkoutStepper ?? {})
|
|
61
|
+
|
|
62
|
+
const slotClasses = computed(() => {
|
|
63
|
+
const base = theme.value?.slots ?? {}
|
|
64
|
+
return {
|
|
65
|
+
root: [base.root, props.ui?.root],
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<div :class="slotClasses.root">
|
|
72
|
+
<slot :steps="steps" :active="modelValue">
|
|
73
|
+
<UStepper
|
|
74
|
+
:items="stepperItems"
|
|
75
|
+
:model-value="modelValue"
|
|
76
|
+
:orientation="orientation"
|
|
77
|
+
:linear="linear"
|
|
78
|
+
:size="size"
|
|
79
|
+
:color="color"
|
|
80
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
81
|
+
/>
|
|
82
|
+
</slot>
|
|
83
|
+
</div>
|
|
84
|
+
</template>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CEmptyState — Reusable empty state for cart, wishlist, search results, etc.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface EmptyStateProps {
|
|
7
|
+
/** Icon name */
|
|
8
|
+
icon?: string
|
|
9
|
+
/** Title text */
|
|
10
|
+
title?: string
|
|
11
|
+
/** Description text */
|
|
12
|
+
description?: string
|
|
13
|
+
/** CTA button label */
|
|
14
|
+
actionLabel?: string
|
|
15
|
+
/** CTA button link */
|
|
16
|
+
actionTo?: string
|
|
17
|
+
/** Per-instance theme overrides */
|
|
18
|
+
ui?: Partial<{
|
|
19
|
+
root: any
|
|
20
|
+
icon: any
|
|
21
|
+
title: any
|
|
22
|
+
description: any
|
|
23
|
+
}>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(defineProps<EmptyStateProps>(), {
|
|
27
|
+
icon: 'i-heroicons-inbox',
|
|
28
|
+
title: 'Nothing here yet',
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const emit = defineEmits<{
|
|
32
|
+
'action': []
|
|
33
|
+
}>()
|
|
34
|
+
|
|
35
|
+
// Resolve theme from app.config
|
|
36
|
+
const appConfig = useAppConfig()
|
|
37
|
+
const theme = computed(() => (appConfig.ui as any)?.emptyState ?? {})
|
|
38
|
+
|
|
39
|
+
const slotClasses = computed(() => {
|
|
40
|
+
const base = theme.value?.slots ?? {}
|
|
41
|
+
const merge = (slot: string) => [
|
|
42
|
+
base[slot],
|
|
43
|
+
props.ui?.[slot as keyof typeof props.ui],
|
|
44
|
+
]
|
|
45
|
+
return {
|
|
46
|
+
root: merge('root'),
|
|
47
|
+
icon: merge('icon'),
|
|
48
|
+
title: merge('title'),
|
|
49
|
+
description: merge('description'),
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<div :class="['flex flex-col items-center justify-center py-16 px-4', slotClasses.root]">
|
|
56
|
+
<slot name="icon">
|
|
57
|
+
<UIcon :name="icon" :class="['text-5xl text-muted mb-4', slotClasses.icon]" />
|
|
58
|
+
</slot>
|
|
59
|
+
|
|
60
|
+
<slot name="title">
|
|
61
|
+
<h3 :class="['text-lg font-medium text-highlighted mb-1', slotClasses.title]">{{ title }}</h3>
|
|
62
|
+
</slot>
|
|
63
|
+
|
|
64
|
+
<slot name="description">
|
|
65
|
+
<p v-if="description" :class="['text-sm text-muted max-w-md text-center mb-6', slotClasses.description]">
|
|
66
|
+
{{ description }}
|
|
67
|
+
</p>
|
|
68
|
+
</slot>
|
|
69
|
+
|
|
70
|
+
<slot name="action">
|
|
71
|
+
<UButton
|
|
72
|
+
v-if="actionLabel"
|
|
73
|
+
:to="actionTo"
|
|
74
|
+
color="primary"
|
|
75
|
+
@click="emit('action')"
|
|
76
|
+
>
|
|
77
|
+
{{ actionLabel }}
|
|
78
|
+
</UButton>
|
|
79
|
+
</slot>
|
|
80
|
+
</div>
|
|
81
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ProductType } from '@commercejs/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CProductTypeBadge — Small badge indicating a product's type.
|
|
6
|
+
* Used on cards, listings, and detail pages.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ProductTypeBadgeProps {
|
|
10
|
+
type: ProductType
|
|
11
|
+
/** Per-instance theme overrides */
|
|
12
|
+
ui?: Partial<{ root: any }>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const props = defineProps<ProductTypeBadgeProps>()
|
|
16
|
+
|
|
17
|
+
const config = computed(() => {
|
|
18
|
+
const map: Record<ProductType, { label: string; icon: string; color: string }> = {
|
|
19
|
+
physical: { label: 'Physical', icon: 'i-heroicons-cube', color: 'neutral' },
|
|
20
|
+
digital: { label: 'Digital', icon: 'i-heroicons-arrow-down-tray', color: 'info' },
|
|
21
|
+
service: { label: 'Service', icon: 'i-heroicons-wrench-screwdriver', color: 'primary' },
|
|
22
|
+
event: { label: 'Event', icon: 'i-heroicons-ticket', color: 'warning' },
|
|
23
|
+
subscription: { label: 'Subscription', icon: 'i-heroicons-arrow-path', color: 'success' },
|
|
24
|
+
auction: { label: 'Auction', icon: 'i-heroicons-bolt', color: 'error' },
|
|
25
|
+
rental: { label: 'Rental', icon: 'i-heroicons-calendar-days', color: 'info' },
|
|
26
|
+
gift_card: { label: 'Gift Card', icon: 'i-heroicons-gift', color: 'primary' },
|
|
27
|
+
}
|
|
28
|
+
return map[props.type] || map.physical
|
|
29
|
+
})
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<UBadge :color="config.color as any" size="xs" variant="subtle" :class="props.ui?.root">
|
|
34
|
+
<UIcon :name="config.icon" class="me-0.5" />
|
|
35
|
+
{{ config.label }}
|
|
36
|
+
</UBadge>
|
|
37
|
+
</template>
|