@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,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>