@instockng/storefront-ui 1.0.74 → 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 (254) 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/index178.mjs +2 -2
  85. package/dist/index179.mjs +5 -5
  86. package/dist/index18.mjs +60 -22
  87. package/dist/index182.mjs +1 -1
  88. package/dist/index185.mjs +1 -1
  89. package/dist/index188.mjs +3 -3
  90. package/dist/index189.mjs +1 -1
  91. package/dist/index19.mjs +22 -107
  92. package/dist/index193.mjs +2 -2
  93. package/dist/index195.mjs +2 -2
  94. package/dist/index199.mjs +1 -1
  95. package/dist/index2.mjs +2 -2
  96. package/dist/index20.mjs +105 -38
  97. package/dist/index201.mjs +2 -2
  98. package/dist/index21.mjs +37 -37
  99. package/dist/index210.mjs +2 -2
  100. package/dist/index212.mjs +1 -1
  101. package/dist/index213.mjs +1 -1
  102. package/dist/index214.mjs +31 -2
  103. package/dist/index215.mjs +11 -2
  104. package/dist/index216.mjs +4 -2
  105. package/dist/index217.mjs +4 -2
  106. package/dist/index218.mjs +13 -108
  107. package/dist/index219.mjs +7 -2
  108. package/dist/index22.mjs +42 -71
  109. package/dist/index220.mjs +12 -2
  110. package/dist/index221.mjs +4 -36
  111. package/dist/index222.mjs +33 -2
  112. package/dist/index223.mjs +31 -2
  113. package/dist/index224.mjs +28 -2
  114. package/dist/index225.mjs +61 -2
  115. package/dist/index226.mjs +2 -2
  116. package/dist/index227.mjs +2 -2
  117. package/dist/index228.mjs +2 -2
  118. package/dist/index229.mjs +2 -244
  119. package/dist/index23.mjs +67 -28
  120. package/dist/index231.mjs +2 -33
  121. package/dist/index232.mjs +2 -65
  122. package/dist/index233.mjs +31 -19
  123. package/dist/index234.mjs +2 -2
  124. package/dist/index235.mjs +2 -2
  125. package/dist/index237.mjs +244 -2
  126. package/dist/index238.mjs +2 -2
  127. package/dist/index239.mjs +33 -2
  128. package/dist/index24.mjs +29 -6
  129. package/dist/index240.mjs +65 -2
  130. package/dist/index241.mjs +25 -2
  131. package/dist/index243.mjs +2 -2
  132. package/dist/index244.mjs +108 -4
  133. package/dist/index245.mjs +2 -31
  134. package/dist/index246.mjs +2 -11
  135. package/dist/index247.mjs +2 -4
  136. package/dist/index248.mjs +2 -4
  137. package/dist/index249.mjs +2 -13
  138. package/dist/index25.mjs +8 -21
  139. package/dist/index250.mjs +2 -7
  140. package/dist/index251.mjs +2 -12
  141. package/dist/index252.mjs +2 -5
  142. package/dist/index253.mjs +2 -33
  143. package/dist/index254.mjs +2 -31
  144. package/dist/index255.mjs +2 -28
  145. package/dist/index256.mjs +2 -61
  146. package/dist/index257.mjs +4 -2
  147. package/dist/index258.mjs +3 -2
  148. package/dist/index259.mjs +2 -18
  149. package/dist/index26.mjs +19 -34
  150. package/dist/index260.mjs +2 -47
  151. package/dist/index261.mjs +17 -2
  152. package/dist/index262.mjs +13 -2
  153. package/dist/index263.mjs +6 -2
  154. package/dist/index264.mjs +30 -2
  155. package/dist/index265.mjs +2 -91
  156. package/dist/index266.mjs +2 -2
  157. package/dist/index267.mjs +18 -3
  158. package/dist/index268.mjs +47 -2
  159. package/dist/index269.mjs +2 -2
  160. package/dist/index27.mjs +34 -37
  161. package/dist/index270.mjs +2 -17
  162. package/dist/index271.mjs +2 -13
  163. package/dist/index272.mjs +91 -6
  164. package/dist/index273.mjs +2 -30
  165. package/dist/index274.mjs +2 -2
  166. package/dist/index276.mjs +2 -2
  167. package/dist/index277.mjs +5 -0
  168. package/dist/index28.mjs +28 -103
  169. package/dist/index29.mjs +104 -20
  170. package/dist/index3.mjs +5 -5
  171. package/dist/index30.mjs +29 -9
  172. package/dist/index31.mjs +9 -9
  173. package/dist/index32.mjs +11 -116
  174. package/dist/index33.mjs +116 -25
  175. package/dist/index34.mjs +25 -81
  176. package/dist/index35.mjs +81 -112
  177. package/dist/index36.mjs +109 -8
  178. package/dist/index37.mjs +9 -33
  179. package/dist/index38.mjs +27 -20
  180. package/dist/index39.mjs +26 -9
  181. package/dist/index4.mjs +1 -1
  182. package/dist/index40.mjs +10 -122
  183. package/dist/index41.mjs +114 -380
  184. package/dist/index42.mjs +384 -20
  185. package/dist/index43.mjs +24 -31
  186. package/dist/index44.mjs +32 -7
  187. package/dist/index45.mjs +6 -1432
  188. package/dist/index46.mjs +1432 -69
  189. package/dist/index47.mjs +70 -2
  190. package/dist/index48.mjs +2 -60
  191. package/dist/index49.mjs +57 -48
  192. package/dist/index5.mjs +1 -1
  193. package/dist/index50.mjs +51 -33
  194. package/dist/index51.mjs +33 -15
  195. package/dist/index52.mjs +12 -2260
  196. package/dist/index53.mjs +2263 -36
  197. package/dist/index54.mjs +36 -44
  198. package/dist/index55.mjs +44 -99
  199. package/dist/index56.mjs +99 -81
  200. package/dist/index57.mjs +75 -13
  201. package/dist/index58.mjs +15 -125
  202. package/dist/index59.mjs +93 -89
  203. package/dist/index6.mjs +1 -1
  204. package/dist/index60.mjs +102 -56
  205. package/dist/index61.mjs +55 -89
  206. package/dist/index62.mjs +82 -76
  207. package/dist/index63.mjs +100 -13
  208. package/dist/index64.mjs +17 -92
  209. package/dist/index65.mjs +70 -56
  210. package/dist/index66.mjs +62 -44
  211. package/dist/index67.mjs +47 -46
  212. package/dist/index68.mjs +48 -121
  213. package/dist/index69.mjs +133 -22
  214. package/dist/index7.mjs +6 -6
  215. package/dist/index70.mjs +24 -2
  216. package/dist/index71.mjs +2 -153
  217. package/dist/index72.mjs +4 -22
  218. package/dist/index73.mjs +22 -4
  219. package/dist/index74.mjs +149 -71
  220. package/dist/index75.mjs +74 -14
  221. package/dist/index76.mjs +14 -62
  222. package/dist/index77.mjs +63 -2
  223. package/dist/index78.mjs +234 -5
  224. package/dist/index79.mjs +5 -1133
  225. package/dist/index8.mjs +4 -4
  226. package/dist/index80.mjs +131 -17
  227. package/dist/index81.mjs +67 -54
  228. package/dist/index82.mjs +84 -30
  229. package/dist/index83.mjs +29 -2
  230. package/dist/index84.mjs +9 -235
  231. package/dist/index85.mjs +74 -5
  232. package/dist/index86.mjs +3 -133
  233. package/dist/index87.mjs +2 -68
  234. package/dist/index88.mjs +79 -83
  235. package/dist/index89.mjs +52 -27
  236. package/dist/index9.mjs +3 -3
  237. package/dist/index90.mjs +5 -8
  238. package/dist/index91.mjs +4 -74
  239. package/dist/index92.mjs +178 -3
  240. package/dist/index93.mjs +53 -2
  241. package/dist/index94.mjs +68 -82
  242. package/dist/index95.mjs +33 -53
  243. package/dist/index96.mjs +42 -5
  244. package/dist/index97.mjs +2 -5
  245. package/dist/index98.mjs +5 -178
  246. package/dist/index99.mjs +1134 -53
  247. package/dist/styles.css +1 -1
  248. package/package.json +1 -1
  249. package/src/components/ProductAddOns.stories.tsx +10 -3
  250. package/src/components/ProductAddOns.tsx +167 -112
  251. package/src/components/RecommendedProducts.tsx +130 -88
  252. package/src/components/ShoppingCart.tsx +1 -1
  253. package/src/components/VariantPickerModal.tsx +154 -0
  254. 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
+
@@ -153,7 +153,7 @@ export function ShoppingCart({
153
153
 
154
154
  return (
155
155
  <div className="space-y-2">
156
- <p className="text-sm font-medium text-center">
156
+ <p className="text-sm font-bold text-center">
157
157
  {isFreeShipping ? (
158
158
  <span className="text-green-600">You've unlocked FREE delivery! 🎉</span>
159
159
  ) : (
@@ -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