@instockng/storefront-ui 1.0.75 → 1.0.76

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 (244) hide show
  1. package/dist/components/ProductAddOns.d.ts +3 -1
  2. package/dist/components/ProductAddOns.d.ts.map +1 -1
  3. package/dist/components/RecommendedProducts.d.ts.map +1 -1
  4. package/dist/components/VariantPickerModal.d.ts +28 -0
  5. package/dist/components/VariantPickerModal.d.ts.map +1 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.mjs +104 -102
  9. package/dist/index10.mjs +162 -121
  10. package/dist/index100.mjs +20 -69
  11. package/dist/index101.mjs +55 -34
  12. package/dist/index102.mjs +32 -42
  13. package/dist/index103.mjs +2 -2
  14. package/dist/index104.mjs +2 -28
  15. package/dist/index105.mjs +20 -17
  16. package/dist/index106.mjs +54 -213
  17. package/dist/index107.mjs +28 -179
  18. package/dist/index108.mjs +6 -21
  19. package/dist/index109.mjs +50 -21
  20. package/dist/index11.mjs +116 -88
  21. package/dist/index110.mjs +6 -33
  22. package/dist/index111.mjs +11 -155
  23. package/dist/index112.mjs +7 -20
  24. package/dist/index113.mjs +28 -31
  25. package/dist/index114.mjs +2 -84
  26. package/dist/index115.mjs +68 -36
  27. package/dist/index116.mjs +163 -141
  28. package/dist/index117.mjs +2 -55
  29. package/dist/index118.mjs +26 -21
  30. package/dist/index119.mjs +11 -20
  31. package/dist/index12.mjs +86 -90
  32. package/dist/index120.mjs +211 -19
  33. package/dist/index121.mjs +174 -19
  34. package/dist/index122.mjs +15 -14
  35. package/dist/index123.mjs +16 -15
  36. package/dist/index124.mjs +26 -14
  37. package/dist/index125.mjs +147 -56
  38. package/dist/index126.mjs +10 -8
  39. package/dist/index127.mjs +22 -30
  40. package/dist/index128.mjs +77 -17
  41. package/dist/index129.mjs +30 -26
  42. package/dist/index13.mjs +83 -185
  43. package/dist/index130.mjs +140 -22
  44. package/dist/index131.mjs +50 -14
  45. package/dist/index132.mjs +17 -15
  46. package/dist/index133.mjs +18 -38
  47. package/dist/index134.mjs +17 -17
  48. package/dist/index135.mjs +19 -262
  49. package/dist/index136.mjs +13 -62
  50. package/dist/index137.mjs +21 -7
  51. package/dist/index138.mjs +21 -2
  52. package/dist/index139.mjs +65 -2
  53. package/dist/index14.mjs +196 -94
  54. package/dist/index140.mjs +12 -27
  55. package/dist/index141.mjs +39 -2
  56. package/dist/index142.mjs +24 -2
  57. package/dist/index143.mjs +33 -20
  58. package/dist/index144.mjs +25 -54
  59. package/dist/index145.mjs +18 -28
  60. package/dist/index146.mjs +20 -6
  61. package/dist/index147.mjs +44 -49
  62. package/dist/index148.mjs +23 -6
  63. package/dist/index149.mjs +267 -11
  64. package/dist/index15.mjs +82 -713
  65. package/dist/index150.mjs +70 -7
  66. package/dist/index151.mjs +8 -28
  67. package/dist/index152.mjs +2 -2
  68. package/dist/index153.mjs +2 -70
  69. package/dist/index154.mjs +30 -164
  70. package/dist/index155.mjs +2 -2
  71. package/dist/index159.mjs +1 -1
  72. package/dist/index16.mjs +722 -53
  73. package/dist/index160.mjs +1 -1
  74. package/dist/index161.mjs +1 -1
  75. package/dist/index162.mjs +1 -1
  76. package/dist/index163.mjs +1 -1
  77. package/dist/index165.mjs +3 -3
  78. package/dist/index166.mjs +1 -1
  79. package/dist/index168.mjs +3 -3
  80. package/dist/index169.mjs +1 -1
  81. package/dist/index17.mjs +58 -60
  82. package/dist/index174.mjs +2 -2
  83. package/dist/index176.mjs +1 -1
  84. package/dist/index18.mjs +60 -22
  85. package/dist/index182.mjs +1 -1
  86. package/dist/index185.mjs +1 -1
  87. package/dist/index188.mjs +3 -3
  88. package/dist/index189.mjs +1 -1
  89. package/dist/index19.mjs +22 -107
  90. package/dist/index193.mjs +2 -2
  91. package/dist/index195.mjs +2 -2
  92. package/dist/index199.mjs +1 -1
  93. package/dist/index2.mjs +2 -2
  94. package/dist/index20.mjs +105 -38
  95. package/dist/index201.mjs +1 -1
  96. package/dist/index21.mjs +37 -37
  97. package/dist/index210.mjs +2 -2
  98. package/dist/index212.mjs +1 -1
  99. package/dist/index213.mjs +1 -1
  100. package/dist/index214.mjs +1 -1
  101. package/dist/index215.mjs +3 -3
  102. package/dist/index218.mjs +3 -3
  103. package/dist/index22.mjs +42 -71
  104. package/dist/index220.mjs +1 -1
  105. package/dist/index222.mjs +3 -3
  106. package/dist/index223.mjs +8 -8
  107. package/dist/index224.mjs +3 -3
  108. package/dist/index226.mjs +2 -2
  109. package/dist/index227.mjs +2 -108
  110. package/dist/index228.mjs +2 -2
  111. package/dist/index229.mjs +2 -2
  112. package/dist/index23.mjs +67 -28
  113. package/dist/index230.mjs +2 -37
  114. package/dist/index231.mjs +2 -2
  115. package/dist/index232.mjs +2 -2
  116. package/dist/index233.mjs +37 -2
  117. package/dist/index234.mjs +2 -2
  118. package/dist/index237.mjs +1 -1
  119. package/dist/index239.mjs +2 -2
  120. package/dist/index24.mjs +29 -6
  121. package/dist/index240.mjs +1 -1
  122. package/dist/index241.mjs +2 -2
  123. package/dist/index242.mjs +2 -2
  124. package/dist/index243.mjs +2 -2
  125. package/dist/index244.mjs +108 -2
  126. package/dist/index245.mjs +2 -2
  127. package/dist/index246.mjs +2 -2
  128. package/dist/index247.mjs +2 -2
  129. package/dist/index248.mjs +2 -2
  130. package/dist/index249.mjs +2 -2
  131. package/dist/index25.mjs +8 -21
  132. package/dist/index250.mjs +2 -2
  133. package/dist/index251.mjs +2 -2
  134. package/dist/index253.mjs +2 -2
  135. package/dist/index254.mjs +2 -2
  136. package/dist/index255.mjs +2 -2
  137. package/dist/index256.mjs +2 -4
  138. package/dist/index257.mjs +4 -2
  139. package/dist/index258.mjs +3 -2
  140. package/dist/index259.mjs +2 -18
  141. package/dist/index26.mjs +19 -34
  142. package/dist/index260.mjs +2 -47
  143. package/dist/index261.mjs +17 -2
  144. package/dist/index262.mjs +13 -2
  145. package/dist/index263.mjs +6 -2
  146. package/dist/index264.mjs +30 -2
  147. package/dist/index265.mjs +2 -91
  148. package/dist/index266.mjs +2 -3
  149. package/dist/index267.mjs +18 -2
  150. package/dist/index268.mjs +47 -2
  151. package/dist/index269.mjs +2 -17
  152. package/dist/index27.mjs +34 -37
  153. package/dist/index270.mjs +2 -13
  154. package/dist/index271.mjs +2 -6
  155. package/dist/index272.mjs +91 -30
  156. package/dist/index273.mjs +2 -2
  157. package/dist/index274.mjs +2 -2
  158. package/dist/index276.mjs +2 -2
  159. package/dist/index277.mjs +5 -0
  160. package/dist/index28.mjs +28 -103
  161. package/dist/index29.mjs +104 -20
  162. package/dist/index3.mjs +5 -5
  163. package/dist/index30.mjs +29 -9
  164. package/dist/index31.mjs +9 -9
  165. package/dist/index32.mjs +11 -116
  166. package/dist/index33.mjs +116 -25
  167. package/dist/index34.mjs +25 -81
  168. package/dist/index35.mjs +81 -112
  169. package/dist/index36.mjs +109 -8
  170. package/dist/index37.mjs +9 -33
  171. package/dist/index38.mjs +26 -19
  172. package/dist/index39.mjs +26 -9
  173. package/dist/index4.mjs +1 -1
  174. package/dist/index40.mjs +10 -122
  175. package/dist/index41.mjs +114 -380
  176. package/dist/index42.mjs +384 -20
  177. package/dist/index43.mjs +24 -31
  178. package/dist/index44.mjs +32 -7
  179. package/dist/index45.mjs +6 -1432
  180. package/dist/index46.mjs +1432 -69
  181. package/dist/index47.mjs +70 -2
  182. package/dist/index48.mjs +2 -60
  183. package/dist/index49.mjs +57 -48
  184. package/dist/index5.mjs +1 -1
  185. package/dist/index50.mjs +51 -33
  186. package/dist/index51.mjs +33 -15
  187. package/dist/index52.mjs +12 -2260
  188. package/dist/index53.mjs +2263 -36
  189. package/dist/index54.mjs +36 -44
  190. package/dist/index55.mjs +44 -99
  191. package/dist/index56.mjs +99 -81
  192. package/dist/index57.mjs +75 -13
  193. package/dist/index58.mjs +15 -125
  194. package/dist/index59.mjs +93 -89
  195. package/dist/index6.mjs +1 -1
  196. package/dist/index60.mjs +102 -56
  197. package/dist/index61.mjs +55 -89
  198. package/dist/index62.mjs +82 -76
  199. package/dist/index63.mjs +100 -13
  200. package/dist/index64.mjs +17 -92
  201. package/dist/index65.mjs +70 -56
  202. package/dist/index66.mjs +62 -44
  203. package/dist/index67.mjs +47 -46
  204. package/dist/index68.mjs +48 -121
  205. package/dist/index69.mjs +133 -22
  206. package/dist/index7.mjs +6 -6
  207. package/dist/index70.mjs +21 -150
  208. package/dist/index71.mjs +2 -23
  209. package/dist/index73.mjs +23 -2
  210. package/dist/index74.mjs +149 -71
  211. package/dist/index75.mjs +74 -14
  212. package/dist/index76.mjs +14 -62
  213. package/dist/index77.mjs +63 -2
  214. package/dist/index78.mjs +234 -5
  215. package/dist/index79.mjs +5 -1133
  216. package/dist/index8.mjs +4 -4
  217. package/dist/index80.mjs +131 -17
  218. package/dist/index81.mjs +67 -54
  219. package/dist/index82.mjs +84 -30
  220. package/dist/index83.mjs +29 -2
  221. package/dist/index84.mjs +9 -235
  222. package/dist/index85.mjs +74 -5
  223. package/dist/index86.mjs +3 -133
  224. package/dist/index87.mjs +2 -68
  225. package/dist/index88.mjs +79 -83
  226. package/dist/index89.mjs +52 -27
  227. package/dist/index9.mjs +3 -3
  228. package/dist/index90.mjs +5 -8
  229. package/dist/index91.mjs +4 -74
  230. package/dist/index92.mjs +178 -3
  231. package/dist/index93.mjs +53 -2
  232. package/dist/index94.mjs +68 -82
  233. package/dist/index95.mjs +33 -53
  234. package/dist/index96.mjs +42 -5
  235. package/dist/index97.mjs +2 -5
  236. package/dist/index98.mjs +5 -178
  237. package/dist/index99.mjs +1134 -53
  238. package/dist/styles.css +1 -1
  239. package/package.json +1 -1
  240. package/src/components/ProductAddOns.stories.tsx +10 -3
  241. package/src/components/ProductAddOns.tsx +167 -112
  242. package/src/components/RecommendedProducts.tsx +130 -88
  243. package/src/components/VariantPickerModal.tsx +154 -0
  244. package/src/index.ts +3 -0
@@ -14,6 +14,7 @@ import { formatCurrency, cn } from '../lib/utils';
14
14
  import { Button } from './ui/button';
15
15
  import { ShoppingCart, Loader2, Package } from 'lucide-react';
16
16
  import { useState, useEffect } from 'react';
17
+ import { VariantPickerModal, type VariantPickerProduct, type Variant } from './VariantPickerModal';
17
18
 
18
19
  export interface RecommendedProductsProps {
19
20
  /** Number of recommendations to fetch */
@@ -32,6 +33,8 @@ export function RecommendedProducts({
32
33
  const [fadingProductIds, setFadingProductIds] = useState<string[]>([]);
33
34
  const [collapsingProductIds, setCollapsingProductIds] = useState<string[]>([]);
34
35
  const [hiddenProductIds, setHiddenProductIds] = useState<string[]>([]);
36
+ const [variantPickerProduct, setVariantPickerProduct] = useState<NonNullable<typeof recommendations>[number] | null>(null);
37
+ const [isAddingFromModal, setIsAddingFromModal] = useState(false);
35
38
 
36
39
  // Reset hidden products when recommendations change (e.g., when cart items are removed)
37
40
  useEffect(() => {
@@ -87,100 +90,139 @@ export function RecommendedProducts({
87
90
  }
88
91
  };
89
92
 
93
+ const handleAddClick = (product: typeof visibleRecommendations[0]) => {
94
+ // For multi-variant products, open modal
95
+ if (product.variants.length > 1) {
96
+ setVariantPickerProduct(product);
97
+ return;
98
+ }
99
+ // For single-variant, add directly
100
+ const firstVariant = product.variants[0];
101
+ if (firstVariant) {
102
+ handleAddToCart(product, firstVariant);
103
+ }
104
+ };
105
+
106
+ const handleVariantConfirm = async (product: VariantPickerProduct, variant: Variant) => {
107
+ setIsAddingFromModal(true);
108
+ try {
109
+ const fullProduct = visibleRecommendations.find(p => p.id === product.id);
110
+ const fullVariant = fullProduct?.variants.find(v => v.id === variant.id);
111
+ if (fullProduct && fullVariant) {
112
+ await handleAddToCart(fullProduct, fullVariant);
113
+ }
114
+ setVariantPickerProduct(null);
115
+ } finally {
116
+ setIsAddingFromModal(false);
117
+ }
118
+ };
119
+
90
120
  return (
91
- <div className={cn('pt-2', className)}>
92
- {/* Heading */}
93
- <div className="mb-2 px-4">
94
- <h3 className="text-lg font-semibold text-gray-900">Frequently bought together</h3>
95
- </div>
121
+ <>
122
+ <div className={cn('pt-2', className)}>
123
+ {/* Heading */}
124
+ <div className="mb-2 px-4">
125
+ <h3 className="text-lg font-semibold text-gray-900">Frequently bought together</h3>
126
+ </div>
96
127
 
97
- {/* Horizontal Scrollable Product List */}
98
- <div className="overflow-x-auto">
99
- <div className="flex gap-4 pb-2 transition-all duration-500">
100
- {/* Left spacer */}
101
- <div className="flex-shrink-0 w-1" />
102
-
103
- {visibleRecommendations?.map((product) => {
104
- const firstVariant = product.variants[0];
105
- if (!firstVariant) return null;
106
-
107
- const isAdding = addingProductIds.includes(product.id);
108
- const isFading = fadingProductIds.includes(product.id);
109
- const isCollapsing = collapsingProductIds.includes(product.id);
110
-
111
- return (
112
- <div
113
- key={product.id}
114
- className={cn(
115
- "flex flex-row flex-shrink-0 w-64 h-24 bg-white border border-gray-300 rounded-lg overflow-hidden transition-all duration-500",
116
- isFading && "opacity-0 blur-sm scale-95 pointer-events-none",
117
- isCollapsing && "w-0 h-0 mr-[-1rem] overflow-hidden"
118
- )}
119
- >
120
- {/* Clickable Product Link */}
121
- <a
122
- href={`/product/${product.slug}`}
123
- className="flex flex-row flex-1 hover:bg-gray-50 transition-colors"
128
+ {/* Horizontal Scrollable Product List */}
129
+ <div className="overflow-x-auto">
130
+ <div className="flex gap-4 pb-2 transition-all duration-500">
131
+ {/* Left spacer */}
132
+ <div className="flex-shrink-0 w-1" />
133
+
134
+ {visibleRecommendations?.map((product) => {
135
+ const firstVariant = product.variants[0];
136
+ if (!firstVariant) return null;
137
+
138
+ const isAdding = addingProductIds.includes(product.id);
139
+ const isFading = fadingProductIds.includes(product.id);
140
+ const isCollapsing = collapsingProductIds.includes(product.id);
141
+
142
+ return (
143
+ <div
144
+ key={product.id}
145
+ className={cn(
146
+ "flex flex-row flex-shrink-0 w-64 h-24 bg-white border border-gray-300 rounded-lg overflow-hidden transition-all duration-500",
147
+ isFading && "opacity-0 blur-sm scale-95 pointer-events-none",
148
+ isCollapsing && "w-0 h-0 mr-[-1rem] overflow-hidden"
149
+ )}
124
150
  >
125
- {/* Product Image */}
126
- <div className="relative w-20 h-full flex-shrink-0 bg-gray-100">
127
- {product.thumbnailUrl ? (
128
- <img
129
- src={product.thumbnailUrl}
130
- alt={product.name}
131
- className="w-full h-full object-cover"
132
- />
133
- ) : (
134
- <div className="w-full h-full flex items-center justify-center">
135
- <Package className="h-8 w-8 text-gray-300" />
136
- </div>
137
- )}
138
- </div>
139
-
140
- {/* Product Info */}
141
- <div className="p-2 flex flex-col flex-1 justify-between">
142
- <div>
143
- <h4 className="text-xs font-medium text-gray-900 line-clamp-2 leading-tight">
144
- {product.name}
145
- </h4>
146
- <p className="text-xs font-semibold text-accent-600 mt-0.5">
147
- {formatCurrency(firstVariant.price)}
148
- </p>
149
- </div>
150
-
151
- {/* Add to Cart Button */}
152
- <Button
153
- size="sm"
154
- onClick={(e) => {
155
- e.preventDefault();
156
- e.stopPropagation();
157
- handleAddToCart(product, firstVariant);
158
- }}
159
- disabled={isAdding}
160
- className="w-full bg-accent-500 hover:bg-accent-600 text-white text-xs h-6 mt-1"
161
- >
162
- {isAdding ? (
163
- <>
164
- <Loader2 className="h-3 w-3 animate-spin" />
165
- Adding...
166
- </>
151
+ {/* Clickable Product Link */}
152
+ <a
153
+ href={`/product/${product.slug}`}
154
+ className="flex flex-row flex-1 hover:bg-gray-50 transition-colors"
155
+ >
156
+ {/* Product Image */}
157
+ <div className="relative w-20 h-full flex-shrink-0 bg-gray-100">
158
+ {product.thumbnailUrl ? (
159
+ <img
160
+ src={product.thumbnailUrl}
161
+ alt={product.name}
162
+ className="w-full h-full object-cover"
163
+ />
167
164
  ) : (
168
- <>
169
- <ShoppingCart className="h-3 w-3" />
170
- Add
171
- </>
165
+ <div className="w-full h-full flex items-center justify-center">
166
+ <Package className="h-8 w-8 text-gray-300" />
167
+ </div>
172
168
  )}
173
- </Button>
174
- </div>
175
- </a>
176
- </div>
177
- );
178
- })}
179
-
180
- {/* Right spacer */}
181
- <div className="flex-shrink-0 w-1" />
169
+ </div>
170
+
171
+ {/* Product Info */}
172
+ <div className="p-2 flex flex-col flex-1 justify-between">
173
+ <div>
174
+ <h4 className="text-xs font-medium text-gray-900 line-clamp-2 leading-tight">
175
+ {product.name}
176
+ </h4>
177
+ <p className="text-xs font-semibold text-accent-600 mt-0.5">
178
+ {formatCurrency(firstVariant.price)}
179
+ </p>
180
+ </div>
181
+
182
+ {/* Add to Cart Button */}
183
+ <Button
184
+ size="sm"
185
+ onClick={(e) => {
186
+ e.preventDefault();
187
+ e.stopPropagation();
188
+ handleAddClick(product);
189
+ }}
190
+ disabled={isAdding}
191
+ className="w-full bg-accent-500 hover:bg-accent-600 text-white text-xs h-6 mt-1"
192
+ >
193
+ {isAdding ? (
194
+ <>
195
+ <Loader2 className="h-3 w-3 animate-spin" />
196
+ Adding...
197
+ </>
198
+ ) : (
199
+ <>
200
+ <ShoppingCart className="h-3 w-3" />
201
+ Add
202
+ </>
203
+ )}
204
+ </Button>
205
+ </div>
206
+ </a>
207
+ </div>
208
+ );
209
+ })}
210
+
211
+ {/* Right spacer */}
212
+ <div className="flex-shrink-0 w-1" />
213
+ </div>
182
214
  </div>
183
215
  </div>
184
- </div>
216
+
217
+ {/* Variant Picker Modal */}
218
+ <VariantPickerModal
219
+ isOpen={!!variantPickerProduct}
220
+ onClose={() => setVariantPickerProduct(null)}
221
+ product={variantPickerProduct}
222
+ onConfirm={handleVariantConfirm}
223
+ isLoading={isAddingFromModal}
224
+ />
225
+ </>
185
226
  );
186
227
  }
228
+
@@ -0,0 +1,154 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * VariantPickerModal Component
5
+ *
6
+ * Modal for selecting a product variant before adding to cart.
7
+ * Used by ProductAddOns and RecommendedProducts for multi-variant products.
8
+ */
9
+
10
+ import { useState } from 'react';
11
+ import { Modal } from './ui/modal';
12
+ import { Button } from './ui/button';
13
+ import { formatCurrency, cn } from '../lib/utils';
14
+ import { ShoppingCart, Loader2, Package } from 'lucide-react';
15
+
16
+ export interface Variant {
17
+ id: string;
18
+ name?: string | null;
19
+ price: number;
20
+ sku: string;
21
+ thumbnailUrl?: string | null;
22
+ }
23
+
24
+ export interface VariantPickerProduct {
25
+ id: string;
26
+ name: string;
27
+ slug: string;
28
+ thumbnailUrl?: string | null;
29
+ variants: Variant[];
30
+ }
31
+
32
+ export interface VariantPickerModalProps {
33
+ /** Controls modal visibility */
34
+ isOpen: boolean;
35
+ /** Callback when modal should close */
36
+ onClose: () => void;
37
+ /** Product to select variant for */
38
+ product: VariantPickerProduct | null;
39
+ /** Callback when variant is selected and confirmed */
40
+ onConfirm: (product: VariantPickerProduct, variant: Variant) => void;
41
+ /** Whether add operation is in progress */
42
+ isLoading?: boolean;
43
+ }
44
+
45
+ export function VariantPickerModal({
46
+ isOpen,
47
+ onClose,
48
+ product,
49
+ onConfirm,
50
+ isLoading = false,
51
+ }: VariantPickerModalProps) {
52
+ const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
53
+
54
+ // Reset selection when product changes
55
+ const effectiveVariantId = selectedVariantId || product?.variants[0]?.id || null;
56
+ const selectedVariant = product?.variants.find(v => v.id === effectiveVariantId);
57
+
58
+ const handleConfirm = () => {
59
+ if (product && selectedVariant) {
60
+ onConfirm(product, selectedVariant);
61
+ }
62
+ };
63
+
64
+ if (!product) return null;
65
+
66
+ return (
67
+ <Modal
68
+ isOpen={isOpen}
69
+ onClose={onClose}
70
+ title="Select Option"
71
+ size="sm"
72
+ footer={
73
+ <Button
74
+ onClick={handleConfirm}
75
+ disabled={!selectedVariant || isLoading}
76
+ className="w-full bg-accent-500 hover:bg-accent-600 text-white"
77
+ size="lg"
78
+ >
79
+ {isLoading ? (
80
+ <>
81
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
82
+ Adding...
83
+ </>
84
+ ) : (
85
+ <>
86
+ <ShoppingCart className="h-4 w-4 mr-2" />
87
+ Add to Cart - {selectedVariant && formatCurrency(selectedVariant.price)}
88
+ </>
89
+ )}
90
+ </Button>
91
+ }
92
+ >
93
+ <div className="space-y-4">
94
+ {/* Product Info */}
95
+ <div className="flex gap-4">
96
+ {/* Thumbnail - shows variant image if available, otherwise product image */}
97
+ <div className="w-20 h-20 flex-shrink-0 rounded-lg overflow-hidden bg-gray-100">
98
+ {(selectedVariant?.thumbnailUrl || product.thumbnailUrl) ? (
99
+ <img
100
+ src={selectedVariant?.thumbnailUrl || product.thumbnailUrl || ''}
101
+ alt={selectedVariant?.name || product.name}
102
+ className="w-full h-full object-cover"
103
+ />
104
+ ) : (
105
+ <div className="w-full h-full flex items-center justify-center">
106
+ <Package className="h-8 w-8 text-gray-300" />
107
+ </div>
108
+ )}
109
+ </div>
110
+
111
+ {/* Name */}
112
+ <div className="flex-1">
113
+ <h3 className="font-semibold text-gray-900">{product.name}</h3>
114
+ <p className="text-sm text-gray-500 mt-1">
115
+ {product.variants.length} options available
116
+ </p>
117
+ </div>
118
+ </div>
119
+
120
+ {/* Variant Buttons */}
121
+ <div className="space-y-2">
122
+ <p className="text-sm font-medium text-gray-700">Choose an option:</p>
123
+ <div className="flex flex-wrap gap-2">
124
+ {product.variants.map((variant) => {
125
+ const isSelected = variant.id === effectiveVariantId;
126
+ const variantImage = variant.thumbnailUrl || product.thumbnailUrl;
127
+ return (
128
+ <button
129
+ key={variant.id}
130
+ onClick={() => setSelectedVariantId(variant.id)}
131
+ className={cn(
132
+ "flex items-center gap-2 px-3 py-2 rounded-full text-sm font-medium border-2 transition-all",
133
+ isSelected
134
+ ? "border-accent-500 bg-accent-50 text-accent-700"
135
+ : "border-gray-200 bg-white text-gray-700 hover:border-gray-300"
136
+ )}
137
+ >
138
+ {variantImage && (
139
+ <img
140
+ src={variantImage}
141
+ alt={variant.name || 'Variant'}
142
+ className="w-6 h-6 rounded-full object-cover flex-shrink-0"
143
+ />
144
+ )}
145
+ {variant.name || 'Default'} - {formatCurrency(variant.price)}
146
+ </button>
147
+ );
148
+ })}
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </Modal>
153
+ );
154
+ }
package/src/index.ts CHANGED
@@ -37,6 +37,9 @@ export type { ProductAddOnsProps, AddOnProduct } from './components/ProductAddOn
37
37
  export { RecommendedProducts } from './components/RecommendedProducts';
38
38
  export type { RecommendedProductsProps } from './components/RecommendedProducts';
39
39
 
40
+ export { VariantPickerModal } from './components/VariantPickerModal';
41
+ export type { VariantPickerModalProps, VariantPickerProduct, Variant } from './components/VariantPickerModal';
42
+
40
43
  export { CartItem } from './components/CartItem';
41
44
  export type { CartItemProps } from './components/CartItem';
42
45