@thorprovider/blocks 3.0.1 → 4.0.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 (192) hide show
  1. package/README.md +61 -21
  2. package/dist/blocks/designs/marketplace-product-card/block.d.ts +12 -0
  3. package/dist/blocks/designs/marketplace-product-card/block.d.ts.map +1 -0
  4. package/dist/blocks/designs/marketplace-product-card/block.js +243 -0
  5. package/dist/blocks/designs/marketplace-product-card/block.js.map +1 -0
  6. package/dist/blocks/designs/marketplace-product-card/block.mjs +233 -0
  7. package/dist/blocks/designs/marketplace-product-card/block.mjs.map +1 -0
  8. package/dist/blocks/designs/marketplace-product-card/index.d.ts +3 -0
  9. package/dist/blocks/designs/marketplace-product-card/index.d.ts.map +1 -0
  10. package/dist/blocks/designs/marketplace-product-card/index.js +36 -0
  11. package/dist/blocks/designs/marketplace-product-card/index.js.map +1 -0
  12. package/dist/blocks/designs/marketplace-product-card/index.mjs +6 -0
  13. package/dist/blocks/designs/marketplace-product-card/index.mjs.map +1 -0
  14. package/dist/blocks/designs/marketplace-product-card/normalize.d.ts +13 -0
  15. package/dist/blocks/designs/marketplace-product-card/normalize.d.ts.map +1 -0
  16. package/dist/blocks/designs/marketplace-product-card/normalize.js +36 -0
  17. package/dist/blocks/designs/marketplace-product-card/normalize.js.map +1 -0
  18. package/dist/blocks/designs/marketplace-product-card/normalize.mjs +12 -0
  19. package/dist/blocks/designs/marketplace-product-card/normalize.mjs.map +1 -0
  20. package/dist/blocks/designs/marketplace-product-card/variants.d.ts +72 -0
  21. package/dist/blocks/designs/marketplace-product-card/variants.d.ts.map +1 -0
  22. package/dist/blocks/designs/marketplace-product-card/variants.js +123 -0
  23. package/dist/blocks/designs/marketplace-product-card/variants.js.map +1 -0
  24. package/dist/blocks/designs/marketplace-product-card/variants.mjs +89 -0
  25. package/dist/blocks/designs/marketplace-product-card/variants.mjs.map +1 -0
  26. package/dist/blocks/e-commerce/checkout-forms/checkbox-check.js +1 -1
  27. package/dist/blocks/e-commerce/checkout-forms/checkbox-check.js.map +1 -1
  28. package/dist/blocks/e-commerce/checkout-forms/checkbox-check.mjs +1 -1
  29. package/dist/blocks/e-commerce/checkout-forms/checkbox-check.mjs.map +1 -1
  30. package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.d.ts +11 -0
  31. package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.d.ts.map +1 -0
  32. package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.js +46 -0
  33. package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.js.map +1 -0
  34. package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.mjs +22 -0
  35. package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.mjs.map +1 -0
  36. package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.d.ts +28 -0
  37. package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.d.ts.map +1 -0
  38. package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.js +278 -0
  39. package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.js.map +1 -0
  40. package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.mjs +260 -0
  41. package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.mjs.map +1 -0
  42. package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.d.ts +12 -9
  43. package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.d.ts.map +1 -1
  44. package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.js +49 -71
  45. package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.js.map +1 -1
  46. package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.mjs +50 -72
  47. package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.mjs.map +1 -1
  48. package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.d.ts.map +1 -1
  49. package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.js +6 -3
  50. package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.js.map +1 -1
  51. package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.mjs +6 -3
  52. package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.mjs.map +1 -1
  53. package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.d.ts +7 -0
  54. package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.d.ts.map +1 -0
  55. package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.js +41 -0
  56. package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.js.map +1 -0
  57. package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.mjs +16 -0
  58. package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.mjs.map +1 -0
  59. package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.d.ts +24 -0
  60. package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.d.ts.map +1 -0
  61. package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.js +75 -0
  62. package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.js.map +1 -0
  63. package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.mjs +58 -0
  64. package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.mjs.map +1 -0
  65. package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.d.ts +3 -2
  66. package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.d.ts.map +1 -1
  67. package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.js +9 -2
  68. package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.js.map +1 -1
  69. package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.mjs +9 -2
  70. package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.mjs.map +1 -1
  71. package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.d.ts +23 -8
  72. package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.d.ts.map +1 -1
  73. package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.js +136 -31
  74. package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.js.map +1 -1
  75. package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.mjs +143 -32
  76. package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.mjs.map +1 -1
  77. package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.d.ts +13 -7
  78. package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.d.ts.map +1 -1
  79. package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.js +179 -28
  80. package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.js.map +1 -1
  81. package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.mjs +180 -29
  82. package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.mjs.map +1 -1
  83. package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.d.ts +20 -0
  84. package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.d.ts.map +1 -0
  85. package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.js +199 -0
  86. package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.js.map +1 -0
  87. package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.mjs +175 -0
  88. package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.mjs.map +1 -0
  89. package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.d.ts +14 -0
  90. package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.d.ts.map +1 -0
  91. package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.js +136 -0
  92. package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.js.map +1 -0
  93. package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.mjs +109 -0
  94. package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.mjs.map +1 -0
  95. package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.d.ts +32 -10
  96. package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.d.ts.map +1 -1
  97. package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.js +265 -147
  98. package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.js.map +1 -1
  99. package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.mjs +272 -148
  100. package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.mjs.map +1 -1
  101. package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.d.ts +13 -0
  102. package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.d.ts.map +1 -0
  103. package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.js +74 -0
  104. package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.js.map +1 -0
  105. package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.mjs +50 -0
  106. package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.mjs.map +1 -0
  107. package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.d.ts +7 -0
  108. package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.d.ts.map +1 -0
  109. package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.js +91 -0
  110. package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.js.map +1 -0
  111. package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.mjs +65 -0
  112. package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.mjs.map +1 -0
  113. package/dist/blocks/e-commerce/checkout-forms/checkout.d.ts +174 -44
  114. package/dist/blocks/e-commerce/checkout-forms/checkout.d.ts.map +1 -1
  115. package/dist/blocks/e-commerce/checkout-forms/checkout.js +532 -108
  116. package/dist/blocks/e-commerce/checkout-forms/checkout.js.map +1 -1
  117. package/dist/blocks/e-commerce/checkout-forms/checkout.mjs +542 -109
  118. package/dist/blocks/e-commerce/checkout-forms/checkout.mjs.map +1 -1
  119. package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.d.ts.map +1 -1
  120. package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.js +44 -20
  121. package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.js.map +1 -1
  122. package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.mjs +44 -20
  123. package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.mjs.map +1 -1
  124. package/dist/blocks/e-commerce/navigation/storefront-navbar.d.ts.map +1 -1
  125. package/dist/blocks/e-commerce/navigation/storefront-navbar.js +21 -10
  126. package/dist/blocks/e-commerce/navigation/storefront-navbar.js.map +1 -1
  127. package/dist/blocks/e-commerce/navigation/storefront-navbar.mjs +21 -10
  128. package/dist/blocks/e-commerce/navigation/storefront-navbar.mjs.map +1 -1
  129. package/dist/blocks/e-commerce/product-list/product-card.d.ts.map +1 -1
  130. package/dist/blocks/e-commerce/product-list/product-card.js +36 -9
  131. package/dist/blocks/e-commerce/product-list/product-card.js.map +1 -1
  132. package/dist/blocks/e-commerce/product-list/product-card.mjs +36 -9
  133. package/dist/blocks/e-commerce/product-list/product-card.mjs.map +1 -1
  134. package/dist/blocks/e-commerce/product-list/product-showcase-1.d.ts.map +1 -1
  135. package/dist/blocks/e-commerce/product-list/product-showcase-1.js +7 -2
  136. package/dist/blocks/e-commerce/product-list/product-showcase-1.js.map +1 -1
  137. package/dist/blocks/e-commerce/product-list/product-showcase-1.mjs +7 -2
  138. package/dist/blocks/e-commerce/product-list/product-showcase-1.mjs.map +1 -1
  139. package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.d.ts.map +1 -1
  140. package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.js +5 -1
  141. package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.js.map +1 -1
  142. package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.mjs +5 -1
  143. package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.mjs.map +1 -1
  144. package/dist/blocks/e-commerce/storefront-hero/storefront-hero.d.ts.map +1 -1
  145. package/dist/blocks/e-commerce/storefront-hero/storefront-hero.js +71 -51
  146. package/dist/blocks/e-commerce/storefront-hero/storefront-hero.js.map +1 -1
  147. package/dist/blocks/e-commerce/storefront-hero/storefront-hero.mjs +71 -51
  148. package/dist/blocks/e-commerce/storefront-hero/storefront-hero.mjs.map +1 -1
  149. package/dist/blocks/marketing/banners/banner-1.d.ts.map +1 -1
  150. package/dist/blocks/marketing/banners/banner-1.js +15 -27
  151. package/dist/blocks/marketing/banners/banner-1.js.map +1 -1
  152. package/dist/blocks/marketing/banners/banner-1.mjs +16 -28
  153. package/dist/blocks/marketing/banners/banner-1.mjs.map +1 -1
  154. package/dist/index.d.ts +6 -3
  155. package/dist/index.d.ts.map +1 -1
  156. package/dist/index.js +5 -0
  157. package/dist/index.js.map +1 -1
  158. package/dist/index.mjs +4 -0
  159. package/dist/index.mjs.map +1 -1
  160. package/dist/lib/contracts/marketplace-product-card.d.ts +324 -0
  161. package/dist/lib/contracts/marketplace-product-card.d.ts.map +1 -0
  162. package/dist/lib/contracts/marketplace-product-card.js +85 -0
  163. package/dist/lib/contracts/marketplace-product-card.js.map +1 -0
  164. package/dist/lib/contracts/marketplace-product-card.mjs +61 -0
  165. package/dist/lib/contracts/marketplace-product-card.mjs.map +1 -0
  166. package/dist/lib/contracts.d.ts +1861 -88
  167. package/dist/lib/contracts.d.ts.map +1 -1
  168. package/dist/lib/contracts.js +252 -25
  169. package/dist/lib/contracts.js.map +1 -1
  170. package/dist/lib/contracts.mjs +208 -5
  171. package/dist/lib/contracts.mjs.map +1 -1
  172. package/dist/lib/money-schema.d.ts +19 -0
  173. package/dist/lib/money-schema.d.ts.map +1 -0
  174. package/dist/lib/money-schema.js +33 -0
  175. package/dist/lib/money-schema.js.map +1 -0
  176. package/dist/lib/money-schema.mjs +9 -0
  177. package/dist/lib/money-schema.mjs.map +1 -0
  178. package/dist/lib/motion.d.ts +29 -0
  179. package/dist/lib/motion.d.ts.map +1 -0
  180. package/dist/lib/motion.js +65 -0
  181. package/dist/lib/motion.js.map +1 -0
  182. package/dist/lib/motion.mjs +41 -0
  183. package/dist/lib/motion.mjs.map +1 -0
  184. package/dist/ui/accordion.d.ts +3 -1
  185. package/dist/ui/accordion.d.ts.map +1 -1
  186. package/dist/ui/accordion.js +21 -5
  187. package/dist/ui/accordion.js.map +1 -1
  188. package/dist/ui/accordion.mjs +21 -5
  189. package/dist/ui/accordion.mjs.map +1 -1
  190. package/dist/ui/input-group.d.ts +1 -1
  191. package/package.json +19 -5
  192. package/dist/.tsbuildinfo +0 -1
@@ -29,6 +29,7 @@ var import_react = require("motion/react");
29
29
  var import_badge = require("../../../ui/badge.js");
30
30
  var import_button = require("../../../ui/button.js");
31
31
  var import_card = require("../../../ui/card.js");
32
+ var import_motion = require("../../../lib/motion.js");
32
33
  var import_utils = require("../../../lib/utils.js");
33
34
  var import_money = require("../../../lib/money.js");
34
35
  function compareAtText(item) {
@@ -77,12 +78,22 @@ function ClassicPresentation({
77
78
  initial: reduceMotion ? false : { opacity: 0, y: 16 },
78
79
  whileInView: { opacity: 1, y: 0 },
79
80
  viewport: { once: true, margin: "-40px" },
80
- whileHover: reduceMotion ? void 0 : { y: -4 },
81
- transition: { duration: 0.35, ease: "easeOut" },
81
+ whileHover: reduceMotion ? void 0 : {
82
+ y: -4,
83
+ transition: {
84
+ duration: import_motion.MOTION.durationSeconds.feedback,
85
+ ease: import_motion.MOTION.easing.standard
86
+ }
87
+ },
88
+ transition: { duration: import_motion.MOTION.durationSeconds.entrance, ease: import_motion.MOTION.easing.standard },
82
89
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
83
90
  import_card.Card,
84
91
  {
85
- className: `flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow duration-300 hover:shadow-md ${onProductClick ? "cursor-pointer" : ""}`,
92
+ className: `flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow hover:shadow-md ${onProductClick ? "cursor-pointer" : ""}`,
93
+ style: {
94
+ transitionDuration: `${import_motion.MOTION.durationMilliseconds.feedback}ms`,
95
+ transitionTimingFunction: import_motion.MOTION.cssEasing.standard
96
+ },
86
97
  onClick: onProductClick ? () => onProductClick(item) : void 0,
87
98
  children: [
88
99
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_card.CardContent, { className: "flex flex-1 flex-col gap-4 px-4 ", children: [
@@ -151,9 +162,13 @@ function MinimalPresentation({
151
162
  import_card.Card,
152
163
  {
153
164
  className: (0, import_utils.cn)(
154
- "group overflow-hidden py-4 shadow-none transition-all hover:shadow-lg",
165
+ "group overflow-hidden py-4 shadow-none transition-shadow hover:shadow-lg",
155
166
  onProductClick && "cursor-pointer"
156
167
  ),
168
+ style: {
169
+ transitionDuration: `${import_motion.MOTION.durationMilliseconds.feedback}ms`,
170
+ transitionTimingFunction: import_motion.MOTION.cssEasing.standard
171
+ },
157
172
  onClick: onProductClick ? () => onProductClick(item) : void 0,
158
173
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_card.CardContent, { className: "flex flex-col gap-4 px-4", children: [
159
174
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative aspect-square overflow-hidden rounded-md", children: [
@@ -165,8 +180,12 @@ function MinimalPresentation({
165
180
  alt: item.imageAlt ?? item.title,
166
181
  width: 400,
167
182
  height: 400,
168
- className: "size-full object-cover transition-transform duration-300 group-hover:scale-105",
169
- loading: "lazy"
183
+ className: "size-full object-cover transition-transform group-hover:scale-105",
184
+ loading: "lazy",
185
+ style: {
186
+ transitionDuration: `${import_motion.MOTION.durationMilliseconds.feedback}ms`,
187
+ transitionTimingFunction: import_motion.MOTION.cssEasing.standard
188
+ }
170
189
  }
171
190
  ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-slot": "product-image-placeholder", "aria-hidden": "true", className: "bg-muted size-full" })
172
191
  ] }),
@@ -249,9 +268,13 @@ function RichPresentation({
249
268
  import_card.Card,
250
269
  {
251
270
  className: (0, import_utils.cn)(
252
- "relative overflow-hidden rounded-md py-0 shadow-none transition-all hover:shadow-md",
271
+ "relative overflow-hidden rounded-md py-0 shadow-none transition-shadow hover:shadow-md",
253
272
  onProductClick && "cursor-pointer"
254
273
  ),
274
+ style: {
275
+ transitionDuration: `${import_motion.MOTION.durationMilliseconds.feedback}ms`,
276
+ transitionTimingFunction: import_motion.MOTION.cssEasing.standard
277
+ },
255
278
  onClick: onProductClick ? () => onProductClick(item) : void 0,
256
279
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_card.CardContent, { className: "px-0", children: [
257
280
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative aspect-square w-full overflow-hidden", children: [
@@ -264,7 +287,7 @@ function RichPresentation({
264
287
  loading: "lazy"
265
288
  }
266
289
  ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-slot": "product-image-placeholder", "aria-hidden": "true", className: "bg-muted size-full" }),
267
- item.badge && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_badge.Badge, { className: "absolute start-2 top-2 z-20 flex items-center gap-1 px-2 py-1 font-semibold text-white backdrop-blur-xs", children: item.badge }),
290
+ item.badge && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_badge.Badge, { className: "absolute start-2 top-2 z-20 flex items-center gap-1 px-2 py-1 font-semibold backdrop-blur-xs", children: item.badge }),
268
291
  rating !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "bg-background/80 absolute start-2 bottom-2 z-10 flex items-center gap-1 rounded-md border px-2 py-1 text-sm backdrop-blur-xs", children: [
269
292
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Star, { className: "fill-foreground text-foreground size-4", "aria-hidden": "true" }),
270
293
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: rating.toFixed(1) })
@@ -274,7 +297,11 @@ function RichPresentation({
274
297
  {
275
298
  variant: "ghost",
276
299
  size: "sm",
277
- className: "group bg-background/80 hover:bg-background h-8 cursor-pointer overflow-hidden rounded-full p-0 px-3 text-xs shadow-xs backdrop-blur-xs transition-all duration-200",
300
+ className: "group bg-background/80 hover:bg-background h-8 cursor-pointer overflow-hidden rounded-full p-0 px-3 text-xs shadow-xs backdrop-blur-xs transition-colors",
301
+ style: {
302
+ transitionDuration: `${import_motion.MOTION.durationMilliseconds.feedback}ms`,
303
+ transitionTimingFunction: import_motion.MOTION.cssEasing.standard
304
+ },
278
305
  "aria-label": viewSimilarLabel,
279
306
  onClick: (e) => {
280
307
  e.stopPropagation();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/blocks/e-commerce/product-list/product-card.tsx"],"sourcesContent":["'use client'\n\nimport { GalleryHorizontalEnd, Heart, ShoppingCart, Star } from 'lucide-react'\nimport { motion, useReducedMotion } from 'motion/react'\n\nimport { Badge } from '../../../ui/badge'\nimport { Button } from '../../../ui/button'\nimport { Card, CardContent, CardFooter } from '../../../ui/card'\nimport { cn } from '../../../lib/utils'\nimport { formatMoney, isAvailableMoney } from '../../../lib/money'\nimport type { ProductCardSpecProps, ProductPresentation } from '../../../lib/contracts'\n\n/**\n * The bounded presentation surface. `classic` is the boxed card with an\n * add-to-cart footer, `minimal` is a lean image-forward tile, and `rich` adds\n * overlay actions (wishlist heart, view similar). The value changes structure,\n * never the data contract or the callbacks.\n */\nexport type ProductCardPresentation = NonNullable<ProductCardSpecProps['presentation']>\n\nexport interface ProductCardProps {\n /** The single canonical product presentation item (canonical `Money`). */\n item: ProductPresentation\n /** Bounded presentation option; defaults to `classic`. */\n presentation?: ProductCardPresentation\n /** Host-resolved wishlist membership; reflects the rich presentation heart. */\n wishlisted?: boolean\n onAddToCart?: (item: ProductPresentation) => void\n /**\n * Hides the add-to-cart control for listings whose only action is opening the\n * product. Without this a host with no cart callback would render an inert\n * button.\n */\n hideAddToCart?: boolean\n onProductClick?: (item: ProductPresentation) => void\n /**\n * Host-owned wishlist toggle; reports the item and the next state from the\n * click handler (never from an effect).\n */\n onToggleWishlist?: (item: ProductPresentation, nextState: boolean) => void\n onViewSimilar?: (item: ProductPresentation) => void\n addToCartLabel?: string\n /** Shown when a price is present but unavailable (never a zero amount). */\n unavailablePriceLabel?: string\n addToWishlistLabel?: string\n removeFromWishlistLabel?: string\n viewSimilarLabel?: string\n}\n\n/** What the presentations receive: every label already resolved by `ProductCard`. */\ntype PresentationProps = ProductCardProps & {\n addToCartLabel: string\n unavailablePriceLabel: string\n addToWishlistLabel: string\n removeFromWishlistLabel: string\n viewSimilarLabel: string\n}\n\n/** Comparison price only when both values are valid and the compare is higher. */\nfunction compareAtText(item: ProductPresentation): string | undefined {\n const compareAt = item.compareAtPrice\n if (!compareAt) return undefined\n const formatted = formatMoney(compareAt)\n const price = formatMoney(item.price)\n if (!formatted || !price) return undefined\n if (Number(compareAt.amount) <= Number(item.price.amount)) return undefined\n return formatted\n}\n\nfunction ratingValue(item: ProductPresentation): number | undefined {\n return typeof item.rating === 'number' && Number.isFinite(item.rating) ? item.rating : undefined\n}\n\nfunction PriceRow({\n item,\n unavailablePriceLabel,\n}: {\n item: ProductPresentation\n unavailablePriceLabel: string\n}) {\n const available = isAvailableMoney(item.price)\n const compareAt = compareAtText(item)\n return (\n <div className='mt-auto flex items-baseline gap-2'>\n <p className='font-semibold'>\n {available ? formatMoney(item.price) : unavailablePriceLabel}\n </p>\n {compareAt && (\n <p className='text-muted-foreground text-sm line-through md:text-base xl:text-sm 2xl:text-base'>\n {compareAt}\n </p>\n )}\n </div>\n )\n}\n\nfunction RatingRow({ value }: { value: number }) {\n return (\n <div className='text-muted-foreground flex items-center gap-1 text-xs'>\n <Star className='size-3.5 fill-current' aria-hidden='true' />\n <span>{value.toFixed(1)}</span>\n </div>\n )\n}\n\nfunction ClassicPresentation({\n item,\n hideAddToCart = false,\n onAddToCart,\n onProductClick,\n addToCartLabel,\n unavailablePriceLabel,\n}: PresentationProps) {\n const reduceMotion = useReducedMotion()\n const available = isAvailableMoney(item.price)\n const rating = ratingValue(item)\n\n return (\n <motion.div\n initial={reduceMotion ? false : { opacity: 0, y: 16 }}\n whileInView={{ opacity: 1, y: 0 }}\n viewport={{ once: true, margin: '-40px' }}\n whileHover={reduceMotion ? undefined : { y: -4 }}\n transition={{ duration: 0.35, ease: 'easeOut' }}\n >\n <Card\n className={`flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow duration-300 hover:shadow-md ${\n onProductClick ? 'cursor-pointer' : ''\n }`}\n onClick={onProductClick ? () => onProductClick(item) : undefined}\n >\n <CardContent className='flex flex-1 flex-col gap-4 px-4 '>\n <div className='relative aspect-square overflow-hidden rounded-md'>\n {item.badge && (\n <span className='bg-destructive text-destructive-foreground absolute top-2 left-2 z-10 rounded-full px-2 py-0.5 text-xs font-semibold'>\n {item.badge}\n </span>\n )}\n {item.image ? (\n <img\n src={item.image}\n alt={item.imageAlt ?? item.title}\n className='size-full rounded-md object-contain'\n loading='lazy'\n width={400}\n height={400}\n />\n ) : (\n <div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full rounded-md' />\n )}\n </div>\n\n <div className='flex flex-1 flex-col'>\n <h3 className='mb-1 font-medium text-balance'>\n {onProductClick ? (\n <button\n type='button'\n onClick={(e) => {\n e.stopPropagation()\n onProductClick(item)\n }}\n className='hover:text-primary cursor-pointer text-left transition-colors'\n >\n {item.title}\n </button>\n ) : (\n item.title\n )}\n </h3>\n {rating !== undefined && <RatingRow value={rating} />}\n <PriceRow item={item} unavailablePriceLabel={unavailablePriceLabel} />\n </div>\n </CardContent>\n\n <CardFooter className='bg-transparent px-3 pt-0 border-t-0 md:px-4'>\n {hideAddToCart ? null : (\n <Button\n variant='outline'\n size='sm'\n className='h-8 w-full cursor-pointer px-3 text-sm text-xs'\n disabled={!available}\n onClick={(e) => {\n e.stopPropagation()\n onAddToCart?.(item)\n }}\n >\n {addToCartLabel}\n </Button>\n )}\n </CardFooter>\n </Card>\n </motion.div>\n )\n}\n\nfunction MinimalPresentation({\n item,\n hideAddToCart = false,\n onAddToCart,\n onProductClick,\n addToCartLabel,\n unavailablePriceLabel,\n}: PresentationProps) {\n const available = isAvailableMoney(item.price)\n const rating = ratingValue(item)\n\n return (\n <Card\n className={cn(\n 'group overflow-hidden py-4 shadow-none transition-all hover:shadow-lg',\n onProductClick && 'cursor-pointer',\n )}\n onClick={onProductClick ? () => onProductClick(item) : undefined}\n >\n <CardContent className='flex flex-col gap-4 px-4'>\n <div className='relative aspect-square overflow-hidden rounded-md'>\n {item.badge && (\n <span className='bg-destructive text-destructive-foreground absolute top-2 left-2 z-10 rounded-full px-2 py-0.5 text-xs font-semibold'>\n {item.badge}\n </span>\n )}\n {item.image ? (\n <img\n src={item.image}\n alt={item.imageAlt ?? item.title}\n width={400}\n height={400}\n className='size-full object-cover transition-transform duration-300 group-hover:scale-105'\n loading='lazy'\n />\n ) : (\n <div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full' />\n )}\n </div>\n\n <div className='flex flex-col gap-2'>\n <h3 className='line-clamp-1 text-lg font-semibold text-balance'>{item.title}</h3>\n {rating !== undefined && <RatingRow value={rating} />}\n <div className='flex items-center justify-between gap-2'>\n <PriceRow item={item} unavailablePriceLabel={unavailablePriceLabel} />\n {hideAddToCart ? null : (\n <Button\n variant='ghost'\n size='icon'\n className='size-8 shrink-0 cursor-pointer'\n aria-label={addToCartLabel}\n disabled={!available}\n onClick={(e) => {\n e.stopPropagation()\n onAddToCart?.(item)\n }}\n >\n <ShoppingCart className='size-4' aria-hidden='true' />\n </Button>\n )}\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n\nfunction WishlistHeart({\n item,\n wishlisted,\n onToggleWishlist,\n addToWishlistLabel,\n removeFromWishlistLabel,\n}: {\n item: ProductPresentation\n wishlisted: boolean\n onToggleWishlist?: (item: ProductPresentation, nextState: boolean) => void\n addToWishlistLabel: string\n removeFromWishlistLabel: string\n}) {\n return (\n <Button\n variant='ghost'\n size='icon'\n className='bg-background/80 absolute end-2 top-2 z-20 size-8 cursor-pointer rounded-full p-0 backdrop-blur-xs disabled:cursor-not-allowed'\n aria-label={wishlisted ? removeFromWishlistLabel : addToWishlistLabel}\n aria-pressed={wishlisted}\n disabled={!onToggleWishlist}\n onClick={(e) => {\n e.stopPropagation()\n onToggleWishlist?.(item, !wishlisted)\n }}\n >\n <Heart\n className={cn(\n 'size-4 transition-colors',\n wishlisted ? 'fill-destructive text-destructive' : 'fill-transparent text-muted-foreground',\n )}\n aria-hidden='true'\n />\n </Button>\n )\n}\n\nfunction RichPresentation({\n item,\n hideAddToCart = false,\n wishlisted = false,\n onAddToCart,\n onProductClick,\n onToggleWishlist,\n onViewSimilar,\n addToCartLabel,\n unavailablePriceLabel,\n addToWishlistLabel,\n removeFromWishlistLabel,\n viewSimilarLabel,\n}: PresentationProps) {\n const available = isAvailableMoney(item.price)\n const rating = ratingValue(item)\n\n return (\n <Card\n className={cn(\n 'relative overflow-hidden rounded-md py-0 shadow-none transition-all hover:shadow-md',\n onProductClick && 'cursor-pointer',\n )}\n onClick={onProductClick ? () => onProductClick(item) : undefined}\n >\n <CardContent className='px-0'>\n <div className='relative aspect-square w-full overflow-hidden'>\n {item.image ? (\n <img\n src={item.image}\n alt={item.imageAlt ?? item.title}\n className='size-full object-cover'\n loading='lazy'\n />\n ) : (\n <div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full' />\n )}\n\n {item.badge && (\n <Badge className='absolute start-2 top-2 z-20 flex items-center gap-1 px-2 py-1 font-semibold text-white backdrop-blur-xs'>\n {item.badge}\n </Badge>\n )}\n\n {rating !== undefined && (\n <div className='bg-background/80 absolute start-2 bottom-2 z-10 flex items-center gap-1 rounded-md border px-2 py-1 text-sm backdrop-blur-xs'>\n <Star className='fill-foreground text-foreground size-4' aria-hidden='true' />\n <span>{rating.toFixed(1)}</span>\n </div>\n )}\n\n {onViewSimilar && (\n <div className='group absolute end-2 bottom-2 z-10'>\n <Button\n variant='ghost'\n size='sm'\n className='group bg-background/80 hover:bg-background h-8 cursor-pointer overflow-hidden rounded-full p-0 px-3 text-xs shadow-xs backdrop-blur-xs transition-all duration-200'\n aria-label={viewSimilarLabel}\n onClick={(e) => {\n e.stopPropagation()\n onViewSimilar(item)\n }}\n >\n <GalleryHorizontalEnd className='absolute start-2 size-4 shrink-0' aria-hidden='true' />\n <span className='ms-6 whitespace-nowrap text-xs opacity-0 group-hover:opacity-100'>\n {viewSimilarLabel}\n </span>\n </Button>\n </div>\n )}\n\n <WishlistHeart\n item={item}\n wishlisted={wishlisted}\n onToggleWishlist={onToggleWishlist}\n addToWishlistLabel={addToWishlistLabel}\n removeFromWishlistLabel={removeFromWishlistLabel}\n />\n </div>\n\n <div className='flex flex-col gap-2 p-4'>\n <h3 className='font-medium text-balance'>{item.title}</h3>\n <PriceRow item={item} unavailablePriceLabel={unavailablePriceLabel} />\n {hideAddToCart ? null : (\n <Button\n variant='outline'\n size='sm'\n className='mt-2 w-full cursor-pointer text-xs'\n disabled={!available}\n onClick={(e) => {\n e.stopPropagation()\n onAddToCart?.(item)\n }}\n >\n {addToCartLabel}\n </Button>\n )}\n </div>\n </CardContent>\n </Card>\n )\n}\n\n/**\n * Canonical product card — the item cell of a product listing. Renders only\n * the canonical `ProductPresentation` contract (canonical `Money`) with\n * host-supplied evidence (rating, badge, compare-at); it owns no listing state\n * and reports every action through its callbacks. Prices render through the\n * canonical `formatMoney` path, so a missing price is an unavailable state,\n * never a zero amount.\n */\nexport function ProductCard({\n item,\n presentation = 'classic',\n wishlisted,\n hideAddToCart,\n onAddToCart,\n onProductClick,\n onToggleWishlist,\n onViewSimilar,\n addToCartLabel = 'Agregar al carrito',\n unavailablePriceLabel = 'Precio no disponible',\n addToWishlistLabel = 'Añadir a favoritos',\n removeFromWishlistLabel = 'Quitar de favoritos',\n viewSimilarLabel = 'Ver similares',\n}: ProductCardProps) {\n const shared: PresentationProps = {\n item,\n presentation,\n wishlisted,\n hideAddToCart,\n onAddToCart,\n onProductClick,\n onToggleWishlist,\n onViewSimilar,\n addToCartLabel,\n unavailablePriceLabel,\n addToWishlistLabel,\n removeFromWishlistLabel,\n viewSimilarLabel,\n }\n\n if (presentation === 'minimal') return <MinimalPresentation {...shared} />\n if (presentation === 'rich') return <RichPresentation {...shared} />\n return <ClassicPresentation {...shared} />\n}\n\nexport default ProductCard\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/blocks/e-commerce/product-list/product-card.tsx"],"sourcesContent":["'use client'\n\nimport { GalleryHorizontalEnd, Heart, ShoppingCart, Star } from 'lucide-react'\nimport { motion, useReducedMotion } from 'motion/react'\n\nimport { Badge } from '../../../ui/badge'\nimport { Button } from '../../../ui/button'\nimport { Card, CardContent, CardFooter } from '../../../ui/card'\nimport { MOTION } from '../../../lib/motion'\nimport { cn } from '../../../lib/utils'\nimport { formatMoney, isAvailableMoney } from '../../../lib/money'\nimport type { ProductCardSpecProps, ProductPresentation } from '../../../lib/contracts'\n\n/**\n * The bounded presentation surface. `classic` is the boxed card with an\n * add-to-cart footer, `minimal` is a lean image-forward tile, and `rich` adds\n * overlay actions (wishlist heart, view similar). The value changes structure,\n * never the data contract or the callbacks.\n */\nexport type ProductCardPresentation = NonNullable<ProductCardSpecProps['presentation']>\n\nexport interface ProductCardProps {\n /** The single canonical product presentation item (canonical `Money`). */\n item: ProductPresentation\n /** Bounded presentation option; defaults to `classic`. */\n presentation?: ProductCardPresentation\n /** Host-resolved wishlist membership; reflects the rich presentation heart. */\n wishlisted?: boolean\n onAddToCart?: (item: ProductPresentation) => void\n /**\n * Hides the add-to-cart control for listings whose only action is opening the\n * product. Without this a host with no cart callback would render an inert\n * button.\n */\n hideAddToCart?: boolean\n onProductClick?: (item: ProductPresentation) => void\n /**\n * Host-owned wishlist toggle; reports the item and the next state from the\n * click handler (never from an effect).\n */\n onToggleWishlist?: (item: ProductPresentation, nextState: boolean) => void\n onViewSimilar?: (item: ProductPresentation) => void\n addToCartLabel?: string\n /** Shown when a price is present but unavailable (never a zero amount). */\n unavailablePriceLabel?: string\n addToWishlistLabel?: string\n removeFromWishlistLabel?: string\n viewSimilarLabel?: string\n}\n\n/** What the presentations receive: every label already resolved by `ProductCard`. */\ntype PresentationProps = ProductCardProps & {\n addToCartLabel: string\n unavailablePriceLabel: string\n addToWishlistLabel: string\n removeFromWishlistLabel: string\n viewSimilarLabel: string\n}\n\n/** Comparison price only when both values are valid and the compare is higher. */\nfunction compareAtText(item: ProductPresentation): string | undefined {\n const compareAt = item.compareAtPrice\n if (!compareAt) return undefined\n const formatted = formatMoney(compareAt)\n const price = formatMoney(item.price)\n if (!formatted || !price) return undefined\n if (Number(compareAt.amount) <= Number(item.price.amount)) return undefined\n return formatted\n}\n\nfunction ratingValue(item: ProductPresentation): number | undefined {\n return typeof item.rating === 'number' && Number.isFinite(item.rating) ? item.rating : undefined\n}\n\nfunction PriceRow({\n item,\n unavailablePriceLabel,\n}: {\n item: ProductPresentation\n unavailablePriceLabel: string\n}) {\n const available = isAvailableMoney(item.price)\n const compareAt = compareAtText(item)\n return (\n <div className='mt-auto flex items-baseline gap-2'>\n <p className='font-semibold'>\n {available ? formatMoney(item.price) : unavailablePriceLabel}\n </p>\n {compareAt && (\n <p className='text-muted-foreground text-sm line-through md:text-base xl:text-sm 2xl:text-base'>\n {compareAt}\n </p>\n )}\n </div>\n )\n}\n\nfunction RatingRow({ value }: { value: number }) {\n return (\n <div className='text-muted-foreground flex items-center gap-1 text-xs'>\n <Star className='size-3.5 fill-current' aria-hidden='true' />\n <span>{value.toFixed(1)}</span>\n </div>\n )\n}\n\nfunction ClassicPresentation({\n item,\n hideAddToCart = false,\n onAddToCart,\n onProductClick,\n addToCartLabel,\n unavailablePriceLabel,\n}: PresentationProps) {\n const reduceMotion = useReducedMotion()\n const available = isAvailableMoney(item.price)\n const rating = ratingValue(item)\n\n return (\n <motion.div\n initial={reduceMotion ? false : { opacity: 0, y: 16 }}\n whileInView={{ opacity: 1, y: 0 }}\n viewport={{ once: true, margin: '-40px' }}\n whileHover={\n reduceMotion\n ? undefined\n : {\n y: -4,\n transition: {\n duration: MOTION.durationSeconds.feedback,\n ease: MOTION.easing.standard,\n },\n }\n }\n transition={{ duration: MOTION.durationSeconds.entrance, ease: MOTION.easing.standard }}\n >\n <Card\n className={`flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow hover:shadow-md ${\n onProductClick ? 'cursor-pointer' : ''\n }`}\n style={{\n transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,\n transitionTimingFunction: MOTION.cssEasing.standard,\n }}\n onClick={onProductClick ? () => onProductClick(item) : undefined}\n >\n <CardContent className='flex flex-1 flex-col gap-4 px-4 '>\n <div className='relative aspect-square overflow-hidden rounded-md'>\n {item.badge && (\n <span className='bg-destructive text-destructive-foreground absolute top-2 left-2 z-10 rounded-full px-2 py-0.5 text-xs font-semibold'>\n {item.badge}\n </span>\n )}\n {item.image ? (\n <img\n src={item.image}\n alt={item.imageAlt ?? item.title}\n className='size-full rounded-md object-contain'\n loading='lazy'\n width={400}\n height={400}\n />\n ) : (\n <div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full rounded-md' />\n )}\n </div>\n\n <div className='flex flex-1 flex-col'>\n <h3 className='mb-1 font-medium text-balance'>\n {onProductClick ? (\n <button\n type='button'\n onClick={(e) => {\n e.stopPropagation()\n onProductClick(item)\n }}\n className='hover:text-primary cursor-pointer text-left transition-colors'\n >\n {item.title}\n </button>\n ) : (\n item.title\n )}\n </h3>\n {rating !== undefined && <RatingRow value={rating} />}\n <PriceRow item={item} unavailablePriceLabel={unavailablePriceLabel} />\n </div>\n </CardContent>\n\n <CardFooter className='bg-transparent px-3 pt-0 border-t-0 md:px-4'>\n {hideAddToCart ? null : (\n <Button\n variant='outline'\n size='sm'\n className='h-8 w-full cursor-pointer px-3 text-sm text-xs'\n disabled={!available}\n onClick={(e) => {\n e.stopPropagation()\n onAddToCart?.(item)\n }}\n >\n {addToCartLabel}\n </Button>\n )}\n </CardFooter>\n </Card>\n </motion.div>\n )\n}\n\nfunction MinimalPresentation({\n item,\n hideAddToCart = false,\n onAddToCart,\n onProductClick,\n addToCartLabel,\n unavailablePriceLabel,\n}: PresentationProps) {\n const available = isAvailableMoney(item.price)\n const rating = ratingValue(item)\n\n return (\n <Card\n className={cn(\n 'group overflow-hidden py-4 shadow-none transition-shadow hover:shadow-lg',\n onProductClick && 'cursor-pointer',\n )}\n style={{\n transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,\n transitionTimingFunction: MOTION.cssEasing.standard,\n }}\n onClick={onProductClick ? () => onProductClick(item) : undefined}\n >\n <CardContent className='flex flex-col gap-4 px-4'>\n <div className='relative aspect-square overflow-hidden rounded-md'>\n {item.badge && (\n <span className='bg-destructive text-destructive-foreground absolute top-2 left-2 z-10 rounded-full px-2 py-0.5 text-xs font-semibold'>\n {item.badge}\n </span>\n )}\n {item.image ? (\n <img\n src={item.image}\n alt={item.imageAlt ?? item.title}\n width={400}\n height={400}\n className='size-full object-cover transition-transform group-hover:scale-105'\n loading='lazy'\n style={{\n transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,\n transitionTimingFunction: MOTION.cssEasing.standard,\n }}\n />\n ) : (\n <div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full' />\n )}\n </div>\n\n <div className='flex flex-col gap-2'>\n <h3 className='line-clamp-1 text-lg font-semibold text-balance'>{item.title}</h3>\n {rating !== undefined && <RatingRow value={rating} />}\n <div className='flex items-center justify-between gap-2'>\n <PriceRow item={item} unavailablePriceLabel={unavailablePriceLabel} />\n {hideAddToCart ? null : (\n <Button\n variant='ghost'\n size='icon'\n className='size-8 shrink-0 cursor-pointer'\n aria-label={addToCartLabel}\n disabled={!available}\n onClick={(e) => {\n e.stopPropagation()\n onAddToCart?.(item)\n }}\n >\n <ShoppingCart className='size-4' aria-hidden='true' />\n </Button>\n )}\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n\nfunction WishlistHeart({\n item,\n wishlisted,\n onToggleWishlist,\n addToWishlistLabel,\n removeFromWishlistLabel,\n}: {\n item: ProductPresentation\n wishlisted: boolean\n onToggleWishlist?: (item: ProductPresentation, nextState: boolean) => void\n addToWishlistLabel: string\n removeFromWishlistLabel: string\n}) {\n return (\n <Button\n variant='ghost'\n size='icon'\n className='bg-background/80 absolute end-2 top-2 z-20 size-8 cursor-pointer rounded-full p-0 backdrop-blur-xs disabled:cursor-not-allowed'\n aria-label={wishlisted ? removeFromWishlistLabel : addToWishlistLabel}\n aria-pressed={wishlisted}\n disabled={!onToggleWishlist}\n onClick={(e) => {\n e.stopPropagation()\n onToggleWishlist?.(item, !wishlisted)\n }}\n >\n <Heart\n className={cn(\n 'size-4 transition-colors',\n wishlisted ? 'fill-destructive text-destructive' : 'fill-transparent text-muted-foreground',\n )}\n aria-hidden='true'\n />\n </Button>\n )\n}\n\nfunction RichPresentation({\n item,\n hideAddToCart = false,\n wishlisted = false,\n onAddToCart,\n onProductClick,\n onToggleWishlist,\n onViewSimilar,\n addToCartLabel,\n unavailablePriceLabel,\n addToWishlistLabel,\n removeFromWishlistLabel,\n viewSimilarLabel,\n}: PresentationProps) {\n const available = isAvailableMoney(item.price)\n const rating = ratingValue(item)\n\n return (\n <Card\n className={cn(\n 'relative overflow-hidden rounded-md py-0 shadow-none transition-shadow hover:shadow-md',\n onProductClick && 'cursor-pointer',\n )}\n style={{\n transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,\n transitionTimingFunction: MOTION.cssEasing.standard,\n }}\n onClick={onProductClick ? () => onProductClick(item) : undefined}\n >\n <CardContent className='px-0'>\n <div className='relative aspect-square w-full overflow-hidden'>\n {item.image ? (\n <img\n src={item.image}\n alt={item.imageAlt ?? item.title}\n className='size-full object-cover'\n loading='lazy'\n />\n ) : (\n <div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full' />\n )}\n\n {item.badge && (\n <Badge className='absolute start-2 top-2 z-20 flex items-center gap-1 px-2 py-1 font-semibold backdrop-blur-xs'>\n {item.badge}\n </Badge>\n )}\n\n {rating !== undefined && (\n <div className='bg-background/80 absolute start-2 bottom-2 z-10 flex items-center gap-1 rounded-md border px-2 py-1 text-sm backdrop-blur-xs'>\n <Star className='fill-foreground text-foreground size-4' aria-hidden='true' />\n <span>{rating.toFixed(1)}</span>\n </div>\n )}\n\n {onViewSimilar && (\n <div className='group absolute end-2 bottom-2 z-10'>\n <Button\n variant='ghost'\n size='sm'\n className='group bg-background/80 hover:bg-background h-8 cursor-pointer overflow-hidden rounded-full p-0 px-3 text-xs shadow-xs backdrop-blur-xs transition-colors'\n style={{\n transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,\n transitionTimingFunction: MOTION.cssEasing.standard,\n }}\n aria-label={viewSimilarLabel}\n onClick={(e) => {\n e.stopPropagation()\n onViewSimilar(item)\n }}\n >\n <GalleryHorizontalEnd className='absolute start-2 size-4 shrink-0' aria-hidden='true' />\n <span className='ms-6 whitespace-nowrap text-xs opacity-0 group-hover:opacity-100'>\n {viewSimilarLabel}\n </span>\n </Button>\n </div>\n )}\n\n <WishlistHeart\n item={item}\n wishlisted={wishlisted}\n onToggleWishlist={onToggleWishlist}\n addToWishlistLabel={addToWishlistLabel}\n removeFromWishlistLabel={removeFromWishlistLabel}\n />\n </div>\n\n <div className='flex flex-col gap-2 p-4'>\n <h3 className='font-medium text-balance'>{item.title}</h3>\n <PriceRow item={item} unavailablePriceLabel={unavailablePriceLabel} />\n {hideAddToCart ? null : (\n <Button\n variant='outline'\n size='sm'\n className='mt-2 w-full cursor-pointer text-xs'\n disabled={!available}\n onClick={(e) => {\n e.stopPropagation()\n onAddToCart?.(item)\n }}\n >\n {addToCartLabel}\n </Button>\n )}\n </div>\n </CardContent>\n </Card>\n )\n}\n\n/**\n * Canonical product card — the item cell of a product listing. Renders only\n * the canonical `ProductPresentation` contract (canonical `Money`) with\n * host-supplied evidence (rating, badge, compare-at); it owns no listing state\n * and reports every action through its callbacks. Prices render through the\n * canonical `formatMoney` path, so a missing price is an unavailable state,\n * never a zero amount.\n */\nexport function ProductCard({\n item,\n presentation = 'classic',\n wishlisted,\n hideAddToCart,\n onAddToCart,\n onProductClick,\n onToggleWishlist,\n onViewSimilar,\n addToCartLabel = 'Agregar al carrito',\n unavailablePriceLabel = 'Precio no disponible',\n addToWishlistLabel = 'Añadir a favoritos',\n removeFromWishlistLabel = 'Quitar de favoritos',\n viewSimilarLabel = 'Ver similares',\n}: ProductCardProps) {\n const shared: PresentationProps = {\n item,\n presentation,\n wishlisted,\n hideAddToCart,\n onAddToCart,\n onProductClick,\n onToggleWishlist,\n onViewSimilar,\n addToCartLabel,\n unavailablePriceLabel,\n addToWishlistLabel,\n removeFromWishlistLabel,\n viewSimilarLabel,\n }\n\n if (presentation === 'minimal') return <MinimalPresentation {...shared} />\n if (presentation === 'rich') return <RichPresentation {...shared} />\n return <ClassicPresentation {...shared} />\n}\n\nexport default ProductCard\n"],"mappings":"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","names":[]}
@@ -5,6 +5,7 @@ import { motion, useReducedMotion } from "motion/react";
5
5
  import { Badge } from "../../../ui/badge.mjs";
6
6
  import { Button } from "../../../ui/button.mjs";
7
7
  import { Card, CardContent, CardFooter } from "../../../ui/card.mjs";
8
+ import { MOTION } from "../../../lib/motion.mjs";
8
9
  import { cn } from "../../../lib/utils.mjs";
9
10
  import { formatMoney, isAvailableMoney } from "../../../lib/money.mjs";
10
11
  function compareAtText(item) {
@@ -53,12 +54,22 @@ function ClassicPresentation({
53
54
  initial: reduceMotion ? false : { opacity: 0, y: 16 },
54
55
  whileInView: { opacity: 1, y: 0 },
55
56
  viewport: { once: true, margin: "-40px" },
56
- whileHover: reduceMotion ? void 0 : { y: -4 },
57
- transition: { duration: 0.35, ease: "easeOut" },
57
+ whileHover: reduceMotion ? void 0 : {
58
+ y: -4,
59
+ transition: {
60
+ duration: MOTION.durationSeconds.feedback,
61
+ ease: MOTION.easing.standard
62
+ }
63
+ },
64
+ transition: { duration: MOTION.durationSeconds.entrance, ease: MOTION.easing.standard },
58
65
  children: /* @__PURE__ */ jsxs(
59
66
  Card,
60
67
  {
61
- className: `flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow duration-300 hover:shadow-md ${onProductClick ? "cursor-pointer" : ""}`,
68
+ className: `flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow hover:shadow-md ${onProductClick ? "cursor-pointer" : ""}`,
69
+ style: {
70
+ transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,
71
+ transitionTimingFunction: MOTION.cssEasing.standard
72
+ },
62
73
  onClick: onProductClick ? () => onProductClick(item) : void 0,
63
74
  children: [
64
75
  /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-1 flex-col gap-4 px-4 ", children: [
@@ -127,9 +138,13 @@ function MinimalPresentation({
127
138
  Card,
128
139
  {
129
140
  className: cn(
130
- "group overflow-hidden py-4 shadow-none transition-all hover:shadow-lg",
141
+ "group overflow-hidden py-4 shadow-none transition-shadow hover:shadow-lg",
131
142
  onProductClick && "cursor-pointer"
132
143
  ),
144
+ style: {
145
+ transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,
146
+ transitionTimingFunction: MOTION.cssEasing.standard
147
+ },
133
148
  onClick: onProductClick ? () => onProductClick(item) : void 0,
134
149
  children: /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col gap-4 px-4", children: [
135
150
  /* @__PURE__ */ jsxs("div", { className: "relative aspect-square overflow-hidden rounded-md", children: [
@@ -141,8 +156,12 @@ function MinimalPresentation({
141
156
  alt: item.imageAlt ?? item.title,
142
157
  width: 400,
143
158
  height: 400,
144
- className: "size-full object-cover transition-transform duration-300 group-hover:scale-105",
145
- loading: "lazy"
159
+ className: "size-full object-cover transition-transform group-hover:scale-105",
160
+ loading: "lazy",
161
+ style: {
162
+ transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,
163
+ transitionTimingFunction: MOTION.cssEasing.standard
164
+ }
146
165
  }
147
166
  ) : /* @__PURE__ */ jsx("div", { "data-slot": "product-image-placeholder", "aria-hidden": "true", className: "bg-muted size-full" })
148
167
  ] }),
@@ -225,9 +244,13 @@ function RichPresentation({
225
244
  Card,
226
245
  {
227
246
  className: cn(
228
- "relative overflow-hidden rounded-md py-0 shadow-none transition-all hover:shadow-md",
247
+ "relative overflow-hidden rounded-md py-0 shadow-none transition-shadow hover:shadow-md",
229
248
  onProductClick && "cursor-pointer"
230
249
  ),
250
+ style: {
251
+ transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,
252
+ transitionTimingFunction: MOTION.cssEasing.standard
253
+ },
231
254
  onClick: onProductClick ? () => onProductClick(item) : void 0,
232
255
  children: /* @__PURE__ */ jsxs(CardContent, { className: "px-0", children: [
233
256
  /* @__PURE__ */ jsxs("div", { className: "relative aspect-square w-full overflow-hidden", children: [
@@ -240,7 +263,7 @@ function RichPresentation({
240
263
  loading: "lazy"
241
264
  }
242
265
  ) : /* @__PURE__ */ jsx("div", { "data-slot": "product-image-placeholder", "aria-hidden": "true", className: "bg-muted size-full" }),
243
- item.badge && /* @__PURE__ */ jsx(Badge, { className: "absolute start-2 top-2 z-20 flex items-center gap-1 px-2 py-1 font-semibold text-white backdrop-blur-xs", children: item.badge }),
266
+ item.badge && /* @__PURE__ */ jsx(Badge, { className: "absolute start-2 top-2 z-20 flex items-center gap-1 px-2 py-1 font-semibold backdrop-blur-xs", children: item.badge }),
244
267
  rating !== void 0 && /* @__PURE__ */ jsxs("div", { className: "bg-background/80 absolute start-2 bottom-2 z-10 flex items-center gap-1 rounded-md border px-2 py-1 text-sm backdrop-blur-xs", children: [
245
268
  /* @__PURE__ */ jsx(Star, { className: "fill-foreground text-foreground size-4", "aria-hidden": "true" }),
246
269
  /* @__PURE__ */ jsx("span", { children: rating.toFixed(1) })
@@ -250,7 +273,11 @@ function RichPresentation({
250
273
  {
251
274
  variant: "ghost",
252
275
  size: "sm",
253
- className: "group bg-background/80 hover:bg-background h-8 cursor-pointer overflow-hidden rounded-full p-0 px-3 text-xs shadow-xs backdrop-blur-xs transition-all duration-200",
276
+ className: "group bg-background/80 hover:bg-background h-8 cursor-pointer overflow-hidden rounded-full p-0 px-3 text-xs shadow-xs backdrop-blur-xs transition-colors",
277
+ style: {
278
+ transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,
279
+ transitionTimingFunction: MOTION.cssEasing.standard
280
+ },
254
281
  "aria-label": viewSimilarLabel,
255
282
  onClick: (e) => {
256
283
  e.stopPropagation();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/blocks/e-commerce/product-list/product-card.tsx"],"sourcesContent":["'use client'\n\nimport { GalleryHorizontalEnd, Heart, ShoppingCart, Star } from 'lucide-react'\nimport { motion, useReducedMotion } from 'motion/react'\n\nimport { Badge } from '../../../ui/badge'\nimport { Button } from '../../../ui/button'\nimport { Card, CardContent, CardFooter } from '../../../ui/card'\nimport { cn } from '../../../lib/utils'\nimport { formatMoney, isAvailableMoney } from '../../../lib/money'\nimport type { ProductCardSpecProps, ProductPresentation } from '../../../lib/contracts'\n\n/**\n * The bounded presentation surface. `classic` is the boxed card with an\n * add-to-cart footer, `minimal` is a lean image-forward tile, and `rich` adds\n * overlay actions (wishlist heart, view similar). The value changes structure,\n * never the data contract or the callbacks.\n */\nexport type ProductCardPresentation = NonNullable<ProductCardSpecProps['presentation']>\n\nexport interface ProductCardProps {\n /** The single canonical product presentation item (canonical `Money`). */\n item: ProductPresentation\n /** Bounded presentation option; defaults to `classic`. */\n presentation?: ProductCardPresentation\n /** Host-resolved wishlist membership; reflects the rich presentation heart. */\n wishlisted?: boolean\n onAddToCart?: (item: ProductPresentation) => void\n /**\n * Hides the add-to-cart control for listings whose only action is opening the\n * product. Without this a host with no cart callback would render an inert\n * button.\n */\n hideAddToCart?: boolean\n onProductClick?: (item: ProductPresentation) => void\n /**\n * Host-owned wishlist toggle; reports the item and the next state from the\n * click handler (never from an effect).\n */\n onToggleWishlist?: (item: ProductPresentation, nextState: boolean) => void\n onViewSimilar?: (item: ProductPresentation) => void\n addToCartLabel?: string\n /** Shown when a price is present but unavailable (never a zero amount). */\n unavailablePriceLabel?: string\n addToWishlistLabel?: string\n removeFromWishlistLabel?: string\n viewSimilarLabel?: string\n}\n\n/** What the presentations receive: every label already resolved by `ProductCard`. */\ntype PresentationProps = ProductCardProps & {\n addToCartLabel: string\n unavailablePriceLabel: string\n addToWishlistLabel: string\n removeFromWishlistLabel: string\n viewSimilarLabel: string\n}\n\n/** Comparison price only when both values are valid and the compare is higher. */\nfunction compareAtText(item: ProductPresentation): string | undefined {\n const compareAt = item.compareAtPrice\n if (!compareAt) return undefined\n const formatted = formatMoney(compareAt)\n const price = formatMoney(item.price)\n if (!formatted || !price) return undefined\n if (Number(compareAt.amount) <= Number(item.price.amount)) return undefined\n return formatted\n}\n\nfunction ratingValue(item: ProductPresentation): number | undefined {\n return typeof item.rating === 'number' && Number.isFinite(item.rating) ? item.rating : undefined\n}\n\nfunction PriceRow({\n item,\n unavailablePriceLabel,\n}: {\n item: ProductPresentation\n unavailablePriceLabel: string\n}) {\n const available = isAvailableMoney(item.price)\n const compareAt = compareAtText(item)\n return (\n <div className='mt-auto flex items-baseline gap-2'>\n <p className='font-semibold'>\n {available ? formatMoney(item.price) : unavailablePriceLabel}\n </p>\n {compareAt && (\n <p className='text-muted-foreground text-sm line-through md:text-base xl:text-sm 2xl:text-base'>\n {compareAt}\n </p>\n )}\n </div>\n )\n}\n\nfunction RatingRow({ value }: { value: number }) {\n return (\n <div className='text-muted-foreground flex items-center gap-1 text-xs'>\n <Star className='size-3.5 fill-current' aria-hidden='true' />\n <span>{value.toFixed(1)}</span>\n </div>\n )\n}\n\nfunction ClassicPresentation({\n item,\n hideAddToCart = false,\n onAddToCart,\n onProductClick,\n addToCartLabel,\n unavailablePriceLabel,\n}: PresentationProps) {\n const reduceMotion = useReducedMotion()\n const available = isAvailableMoney(item.price)\n const rating = ratingValue(item)\n\n return (\n <motion.div\n initial={reduceMotion ? false : { opacity: 0, y: 16 }}\n whileInView={{ opacity: 1, y: 0 }}\n viewport={{ once: true, margin: '-40px' }}\n whileHover={reduceMotion ? undefined : { y: -4 }}\n transition={{ duration: 0.35, ease: 'easeOut' }}\n >\n <Card\n className={`flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow duration-300 hover:shadow-md ${\n onProductClick ? 'cursor-pointer' : ''\n }`}\n onClick={onProductClick ? () => onProductClick(item) : undefined}\n >\n <CardContent className='flex flex-1 flex-col gap-4 px-4 '>\n <div className='relative aspect-square overflow-hidden rounded-md'>\n {item.badge && (\n <span className='bg-destructive text-destructive-foreground absolute top-2 left-2 z-10 rounded-full px-2 py-0.5 text-xs font-semibold'>\n {item.badge}\n </span>\n )}\n {item.image ? (\n <img\n src={item.image}\n alt={item.imageAlt ?? item.title}\n className='size-full rounded-md object-contain'\n loading='lazy'\n width={400}\n height={400}\n />\n ) : (\n <div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full rounded-md' />\n )}\n </div>\n\n <div className='flex flex-1 flex-col'>\n <h3 className='mb-1 font-medium text-balance'>\n {onProductClick ? (\n <button\n type='button'\n onClick={(e) => {\n e.stopPropagation()\n onProductClick(item)\n }}\n className='hover:text-primary cursor-pointer text-left transition-colors'\n >\n {item.title}\n </button>\n ) : (\n item.title\n )}\n </h3>\n {rating !== undefined && <RatingRow value={rating} />}\n <PriceRow item={item} unavailablePriceLabel={unavailablePriceLabel} />\n </div>\n </CardContent>\n\n <CardFooter className='bg-transparent px-3 pt-0 border-t-0 md:px-4'>\n {hideAddToCart ? null : (\n <Button\n variant='outline'\n size='sm'\n className='h-8 w-full cursor-pointer px-3 text-sm text-xs'\n disabled={!available}\n onClick={(e) => {\n e.stopPropagation()\n onAddToCart?.(item)\n }}\n >\n {addToCartLabel}\n </Button>\n )}\n </CardFooter>\n </Card>\n </motion.div>\n )\n}\n\nfunction MinimalPresentation({\n item,\n hideAddToCart = false,\n onAddToCart,\n onProductClick,\n addToCartLabel,\n unavailablePriceLabel,\n}: PresentationProps) {\n const available = isAvailableMoney(item.price)\n const rating = ratingValue(item)\n\n return (\n <Card\n className={cn(\n 'group overflow-hidden py-4 shadow-none transition-all hover:shadow-lg',\n onProductClick && 'cursor-pointer',\n )}\n onClick={onProductClick ? () => onProductClick(item) : undefined}\n >\n <CardContent className='flex flex-col gap-4 px-4'>\n <div className='relative aspect-square overflow-hidden rounded-md'>\n {item.badge && (\n <span className='bg-destructive text-destructive-foreground absolute top-2 left-2 z-10 rounded-full px-2 py-0.5 text-xs font-semibold'>\n {item.badge}\n </span>\n )}\n {item.image ? (\n <img\n src={item.image}\n alt={item.imageAlt ?? item.title}\n width={400}\n height={400}\n className='size-full object-cover transition-transform duration-300 group-hover:scale-105'\n loading='lazy'\n />\n ) : (\n <div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full' />\n )}\n </div>\n\n <div className='flex flex-col gap-2'>\n <h3 className='line-clamp-1 text-lg font-semibold text-balance'>{item.title}</h3>\n {rating !== undefined && <RatingRow value={rating} />}\n <div className='flex items-center justify-between gap-2'>\n <PriceRow item={item} unavailablePriceLabel={unavailablePriceLabel} />\n {hideAddToCart ? null : (\n <Button\n variant='ghost'\n size='icon'\n className='size-8 shrink-0 cursor-pointer'\n aria-label={addToCartLabel}\n disabled={!available}\n onClick={(e) => {\n e.stopPropagation()\n onAddToCart?.(item)\n }}\n >\n <ShoppingCart className='size-4' aria-hidden='true' />\n </Button>\n )}\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n\nfunction WishlistHeart({\n item,\n wishlisted,\n onToggleWishlist,\n addToWishlistLabel,\n removeFromWishlistLabel,\n}: {\n item: ProductPresentation\n wishlisted: boolean\n onToggleWishlist?: (item: ProductPresentation, nextState: boolean) => void\n addToWishlistLabel: string\n removeFromWishlistLabel: string\n}) {\n return (\n <Button\n variant='ghost'\n size='icon'\n className='bg-background/80 absolute end-2 top-2 z-20 size-8 cursor-pointer rounded-full p-0 backdrop-blur-xs disabled:cursor-not-allowed'\n aria-label={wishlisted ? removeFromWishlistLabel : addToWishlistLabel}\n aria-pressed={wishlisted}\n disabled={!onToggleWishlist}\n onClick={(e) => {\n e.stopPropagation()\n onToggleWishlist?.(item, !wishlisted)\n }}\n >\n <Heart\n className={cn(\n 'size-4 transition-colors',\n wishlisted ? 'fill-destructive text-destructive' : 'fill-transparent text-muted-foreground',\n )}\n aria-hidden='true'\n />\n </Button>\n )\n}\n\nfunction RichPresentation({\n item,\n hideAddToCart = false,\n wishlisted = false,\n onAddToCart,\n onProductClick,\n onToggleWishlist,\n onViewSimilar,\n addToCartLabel,\n unavailablePriceLabel,\n addToWishlistLabel,\n removeFromWishlistLabel,\n viewSimilarLabel,\n}: PresentationProps) {\n const available = isAvailableMoney(item.price)\n const rating = ratingValue(item)\n\n return (\n <Card\n className={cn(\n 'relative overflow-hidden rounded-md py-0 shadow-none transition-all hover:shadow-md',\n onProductClick && 'cursor-pointer',\n )}\n onClick={onProductClick ? () => onProductClick(item) : undefined}\n >\n <CardContent className='px-0'>\n <div className='relative aspect-square w-full overflow-hidden'>\n {item.image ? (\n <img\n src={item.image}\n alt={item.imageAlt ?? item.title}\n className='size-full object-cover'\n loading='lazy'\n />\n ) : (\n <div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full' />\n )}\n\n {item.badge && (\n <Badge className='absolute start-2 top-2 z-20 flex items-center gap-1 px-2 py-1 font-semibold text-white backdrop-blur-xs'>\n {item.badge}\n </Badge>\n )}\n\n {rating !== undefined && (\n <div className='bg-background/80 absolute start-2 bottom-2 z-10 flex items-center gap-1 rounded-md border px-2 py-1 text-sm backdrop-blur-xs'>\n <Star className='fill-foreground text-foreground size-4' aria-hidden='true' />\n <span>{rating.toFixed(1)}</span>\n </div>\n )}\n\n {onViewSimilar && (\n <div className='group absolute end-2 bottom-2 z-10'>\n <Button\n variant='ghost'\n size='sm'\n className='group bg-background/80 hover:bg-background h-8 cursor-pointer overflow-hidden rounded-full p-0 px-3 text-xs shadow-xs backdrop-blur-xs transition-all duration-200'\n aria-label={viewSimilarLabel}\n onClick={(e) => {\n e.stopPropagation()\n onViewSimilar(item)\n }}\n >\n <GalleryHorizontalEnd className='absolute start-2 size-4 shrink-0' aria-hidden='true' />\n <span className='ms-6 whitespace-nowrap text-xs opacity-0 group-hover:opacity-100'>\n {viewSimilarLabel}\n </span>\n </Button>\n </div>\n )}\n\n <WishlistHeart\n item={item}\n wishlisted={wishlisted}\n onToggleWishlist={onToggleWishlist}\n addToWishlistLabel={addToWishlistLabel}\n removeFromWishlistLabel={removeFromWishlistLabel}\n />\n </div>\n\n <div className='flex flex-col gap-2 p-4'>\n <h3 className='font-medium text-balance'>{item.title}</h3>\n <PriceRow item={item} unavailablePriceLabel={unavailablePriceLabel} />\n {hideAddToCart ? null : (\n <Button\n variant='outline'\n size='sm'\n className='mt-2 w-full cursor-pointer text-xs'\n disabled={!available}\n onClick={(e) => {\n e.stopPropagation()\n onAddToCart?.(item)\n }}\n >\n {addToCartLabel}\n </Button>\n )}\n </div>\n </CardContent>\n </Card>\n )\n}\n\n/**\n * Canonical product card — the item cell of a product listing. Renders only\n * the canonical `ProductPresentation` contract (canonical `Money`) with\n * host-supplied evidence (rating, badge, compare-at); it owns no listing state\n * and reports every action through its callbacks. Prices render through the\n * canonical `formatMoney` path, so a missing price is an unavailable state,\n * never a zero amount.\n */\nexport function ProductCard({\n item,\n presentation = 'classic',\n wishlisted,\n hideAddToCart,\n onAddToCart,\n onProductClick,\n onToggleWishlist,\n onViewSimilar,\n addToCartLabel = 'Agregar al carrito',\n unavailablePriceLabel = 'Precio no disponible',\n addToWishlistLabel = 'Añadir a favoritos',\n removeFromWishlistLabel = 'Quitar de favoritos',\n viewSimilarLabel = 'Ver similares',\n}: ProductCardProps) {\n const shared: PresentationProps = {\n item,\n presentation,\n wishlisted,\n hideAddToCart,\n onAddToCart,\n onProductClick,\n onToggleWishlist,\n onViewSimilar,\n addToCartLabel,\n unavailablePriceLabel,\n addToWishlistLabel,\n removeFromWishlistLabel,\n viewSimilarLabel,\n }\n\n if (presentation === 'minimal') return <MinimalPresentation {...shared} />\n if (presentation === 'rich') return <RichPresentation {...shared} />\n return <ClassicPresentation {...shared} />\n}\n\nexport default ProductCard\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/blocks/e-commerce/product-list/product-card.tsx"],"sourcesContent":["'use client'\n\nimport { GalleryHorizontalEnd, Heart, ShoppingCart, Star } from 'lucide-react'\nimport { motion, useReducedMotion } from 'motion/react'\n\nimport { Badge } from '../../../ui/badge'\nimport { Button } from '../../../ui/button'\nimport { Card, CardContent, CardFooter } from '../../../ui/card'\nimport { MOTION } from '../../../lib/motion'\nimport { cn } from '../../../lib/utils'\nimport { formatMoney, isAvailableMoney } from '../../../lib/money'\nimport type { ProductCardSpecProps, ProductPresentation } from '../../../lib/contracts'\n\n/**\n * The bounded presentation surface. `classic` is the boxed card with an\n * add-to-cart footer, `minimal` is a lean image-forward tile, and `rich` adds\n * overlay actions (wishlist heart, view similar). The value changes structure,\n * never the data contract or the callbacks.\n */\nexport type ProductCardPresentation = NonNullable<ProductCardSpecProps['presentation']>\n\nexport interface ProductCardProps {\n /** The single canonical product presentation item (canonical `Money`). */\n item: ProductPresentation\n /** Bounded presentation option; defaults to `classic`. */\n presentation?: ProductCardPresentation\n /** Host-resolved wishlist membership; reflects the rich presentation heart. */\n wishlisted?: boolean\n onAddToCart?: (item: ProductPresentation) => void\n /**\n * Hides the add-to-cart control for listings whose only action is opening the\n * product. Without this a host with no cart callback would render an inert\n * button.\n */\n hideAddToCart?: boolean\n onProductClick?: (item: ProductPresentation) => void\n /**\n * Host-owned wishlist toggle; reports the item and the next state from the\n * click handler (never from an effect).\n */\n onToggleWishlist?: (item: ProductPresentation, nextState: boolean) => void\n onViewSimilar?: (item: ProductPresentation) => void\n addToCartLabel?: string\n /** Shown when a price is present but unavailable (never a zero amount). */\n unavailablePriceLabel?: string\n addToWishlistLabel?: string\n removeFromWishlistLabel?: string\n viewSimilarLabel?: string\n}\n\n/** What the presentations receive: every label already resolved by `ProductCard`. */\ntype PresentationProps = ProductCardProps & {\n addToCartLabel: string\n unavailablePriceLabel: string\n addToWishlistLabel: string\n removeFromWishlistLabel: string\n viewSimilarLabel: string\n}\n\n/** Comparison price only when both values are valid and the compare is higher. */\nfunction compareAtText(item: ProductPresentation): string | undefined {\n const compareAt = item.compareAtPrice\n if (!compareAt) return undefined\n const formatted = formatMoney(compareAt)\n const price = formatMoney(item.price)\n if (!formatted || !price) return undefined\n if (Number(compareAt.amount) <= Number(item.price.amount)) return undefined\n return formatted\n}\n\nfunction ratingValue(item: ProductPresentation): number | undefined {\n return typeof item.rating === 'number' && Number.isFinite(item.rating) ? item.rating : undefined\n}\n\nfunction PriceRow({\n item,\n unavailablePriceLabel,\n}: {\n item: ProductPresentation\n unavailablePriceLabel: string\n}) {\n const available = isAvailableMoney(item.price)\n const compareAt = compareAtText(item)\n return (\n <div className='mt-auto flex items-baseline gap-2'>\n <p className='font-semibold'>\n {available ? formatMoney(item.price) : unavailablePriceLabel}\n </p>\n {compareAt && (\n <p className='text-muted-foreground text-sm line-through md:text-base xl:text-sm 2xl:text-base'>\n {compareAt}\n </p>\n )}\n </div>\n )\n}\n\nfunction RatingRow({ value }: { value: number }) {\n return (\n <div className='text-muted-foreground flex items-center gap-1 text-xs'>\n <Star className='size-3.5 fill-current' aria-hidden='true' />\n <span>{value.toFixed(1)}</span>\n </div>\n )\n}\n\nfunction ClassicPresentation({\n item,\n hideAddToCart = false,\n onAddToCart,\n onProductClick,\n addToCartLabel,\n unavailablePriceLabel,\n}: PresentationProps) {\n const reduceMotion = useReducedMotion()\n const available = isAvailableMoney(item.price)\n const rating = ratingValue(item)\n\n return (\n <motion.div\n initial={reduceMotion ? false : { opacity: 0, y: 16 }}\n whileInView={{ opacity: 1, y: 0 }}\n viewport={{ once: true, margin: '-40px' }}\n whileHover={\n reduceMotion\n ? undefined\n : {\n y: -4,\n transition: {\n duration: MOTION.durationSeconds.feedback,\n ease: MOTION.easing.standard,\n },\n }\n }\n transition={{ duration: MOTION.durationSeconds.entrance, ease: MOTION.easing.standard }}\n >\n <Card\n className={`flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow hover:shadow-md ${\n onProductClick ? 'cursor-pointer' : ''\n }`}\n style={{\n transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,\n transitionTimingFunction: MOTION.cssEasing.standard,\n }}\n onClick={onProductClick ? () => onProductClick(item) : undefined}\n >\n <CardContent className='flex flex-1 flex-col gap-4 px-4 '>\n <div className='relative aspect-square overflow-hidden rounded-md'>\n {item.badge && (\n <span className='bg-destructive text-destructive-foreground absolute top-2 left-2 z-10 rounded-full px-2 py-0.5 text-xs font-semibold'>\n {item.badge}\n </span>\n )}\n {item.image ? (\n <img\n src={item.image}\n alt={item.imageAlt ?? item.title}\n className='size-full rounded-md object-contain'\n loading='lazy'\n width={400}\n height={400}\n />\n ) : (\n <div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full rounded-md' />\n )}\n </div>\n\n <div className='flex flex-1 flex-col'>\n <h3 className='mb-1 font-medium text-balance'>\n {onProductClick ? (\n <button\n type='button'\n onClick={(e) => {\n e.stopPropagation()\n onProductClick(item)\n }}\n className='hover:text-primary cursor-pointer text-left transition-colors'\n >\n {item.title}\n </button>\n ) : (\n item.title\n )}\n </h3>\n {rating !== undefined && <RatingRow value={rating} />}\n <PriceRow item={item} unavailablePriceLabel={unavailablePriceLabel} />\n </div>\n </CardContent>\n\n <CardFooter className='bg-transparent px-3 pt-0 border-t-0 md:px-4'>\n {hideAddToCart ? null : (\n <Button\n variant='outline'\n size='sm'\n className='h-8 w-full cursor-pointer px-3 text-sm text-xs'\n disabled={!available}\n onClick={(e) => {\n e.stopPropagation()\n onAddToCart?.(item)\n }}\n >\n {addToCartLabel}\n </Button>\n )}\n </CardFooter>\n </Card>\n </motion.div>\n )\n}\n\nfunction MinimalPresentation({\n item,\n hideAddToCart = false,\n onAddToCart,\n onProductClick,\n addToCartLabel,\n unavailablePriceLabel,\n}: PresentationProps) {\n const available = isAvailableMoney(item.price)\n const rating = ratingValue(item)\n\n return (\n <Card\n className={cn(\n 'group overflow-hidden py-4 shadow-none transition-shadow hover:shadow-lg',\n onProductClick && 'cursor-pointer',\n )}\n style={{\n transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,\n transitionTimingFunction: MOTION.cssEasing.standard,\n }}\n onClick={onProductClick ? () => onProductClick(item) : undefined}\n >\n <CardContent className='flex flex-col gap-4 px-4'>\n <div className='relative aspect-square overflow-hidden rounded-md'>\n {item.badge && (\n <span className='bg-destructive text-destructive-foreground absolute top-2 left-2 z-10 rounded-full px-2 py-0.5 text-xs font-semibold'>\n {item.badge}\n </span>\n )}\n {item.image ? (\n <img\n src={item.image}\n alt={item.imageAlt ?? item.title}\n width={400}\n height={400}\n className='size-full object-cover transition-transform group-hover:scale-105'\n loading='lazy'\n style={{\n transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,\n transitionTimingFunction: MOTION.cssEasing.standard,\n }}\n />\n ) : (\n <div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full' />\n )}\n </div>\n\n <div className='flex flex-col gap-2'>\n <h3 className='line-clamp-1 text-lg font-semibold text-balance'>{item.title}</h3>\n {rating !== undefined && <RatingRow value={rating} />}\n <div className='flex items-center justify-between gap-2'>\n <PriceRow item={item} unavailablePriceLabel={unavailablePriceLabel} />\n {hideAddToCart ? null : (\n <Button\n variant='ghost'\n size='icon'\n className='size-8 shrink-0 cursor-pointer'\n aria-label={addToCartLabel}\n disabled={!available}\n onClick={(e) => {\n e.stopPropagation()\n onAddToCart?.(item)\n }}\n >\n <ShoppingCart className='size-4' aria-hidden='true' />\n </Button>\n )}\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n\nfunction WishlistHeart({\n item,\n wishlisted,\n onToggleWishlist,\n addToWishlistLabel,\n removeFromWishlistLabel,\n}: {\n item: ProductPresentation\n wishlisted: boolean\n onToggleWishlist?: (item: ProductPresentation, nextState: boolean) => void\n addToWishlistLabel: string\n removeFromWishlistLabel: string\n}) {\n return (\n <Button\n variant='ghost'\n size='icon'\n className='bg-background/80 absolute end-2 top-2 z-20 size-8 cursor-pointer rounded-full p-0 backdrop-blur-xs disabled:cursor-not-allowed'\n aria-label={wishlisted ? removeFromWishlistLabel : addToWishlistLabel}\n aria-pressed={wishlisted}\n disabled={!onToggleWishlist}\n onClick={(e) => {\n e.stopPropagation()\n onToggleWishlist?.(item, !wishlisted)\n }}\n >\n <Heart\n className={cn(\n 'size-4 transition-colors',\n wishlisted ? 'fill-destructive text-destructive' : 'fill-transparent text-muted-foreground',\n )}\n aria-hidden='true'\n />\n </Button>\n )\n}\n\nfunction RichPresentation({\n item,\n hideAddToCart = false,\n wishlisted = false,\n onAddToCart,\n onProductClick,\n onToggleWishlist,\n onViewSimilar,\n addToCartLabel,\n unavailablePriceLabel,\n addToWishlistLabel,\n removeFromWishlistLabel,\n viewSimilarLabel,\n}: PresentationProps) {\n const available = isAvailableMoney(item.price)\n const rating = ratingValue(item)\n\n return (\n <Card\n className={cn(\n 'relative overflow-hidden rounded-md py-0 shadow-none transition-shadow hover:shadow-md',\n onProductClick && 'cursor-pointer',\n )}\n style={{\n transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,\n transitionTimingFunction: MOTION.cssEasing.standard,\n }}\n onClick={onProductClick ? () => onProductClick(item) : undefined}\n >\n <CardContent className='px-0'>\n <div className='relative aspect-square w-full overflow-hidden'>\n {item.image ? (\n <img\n src={item.image}\n alt={item.imageAlt ?? item.title}\n className='size-full object-cover'\n loading='lazy'\n />\n ) : (\n <div data-slot='product-image-placeholder' aria-hidden='true' className='bg-muted size-full' />\n )}\n\n {item.badge && (\n <Badge className='absolute start-2 top-2 z-20 flex items-center gap-1 px-2 py-1 font-semibold backdrop-blur-xs'>\n {item.badge}\n </Badge>\n )}\n\n {rating !== undefined && (\n <div className='bg-background/80 absolute start-2 bottom-2 z-10 flex items-center gap-1 rounded-md border px-2 py-1 text-sm backdrop-blur-xs'>\n <Star className='fill-foreground text-foreground size-4' aria-hidden='true' />\n <span>{rating.toFixed(1)}</span>\n </div>\n )}\n\n {onViewSimilar && (\n <div className='group absolute end-2 bottom-2 z-10'>\n <Button\n variant='ghost'\n size='sm'\n className='group bg-background/80 hover:bg-background h-8 cursor-pointer overflow-hidden rounded-full p-0 px-3 text-xs shadow-xs backdrop-blur-xs transition-colors'\n style={{\n transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,\n transitionTimingFunction: MOTION.cssEasing.standard,\n }}\n aria-label={viewSimilarLabel}\n onClick={(e) => {\n e.stopPropagation()\n onViewSimilar(item)\n }}\n >\n <GalleryHorizontalEnd className='absolute start-2 size-4 shrink-0' aria-hidden='true' />\n <span className='ms-6 whitespace-nowrap text-xs opacity-0 group-hover:opacity-100'>\n {viewSimilarLabel}\n </span>\n </Button>\n </div>\n )}\n\n <WishlistHeart\n item={item}\n wishlisted={wishlisted}\n onToggleWishlist={onToggleWishlist}\n addToWishlistLabel={addToWishlistLabel}\n removeFromWishlistLabel={removeFromWishlistLabel}\n />\n </div>\n\n <div className='flex flex-col gap-2 p-4'>\n <h3 className='font-medium text-balance'>{item.title}</h3>\n <PriceRow item={item} unavailablePriceLabel={unavailablePriceLabel} />\n {hideAddToCart ? null : (\n <Button\n variant='outline'\n size='sm'\n className='mt-2 w-full cursor-pointer text-xs'\n disabled={!available}\n onClick={(e) => {\n e.stopPropagation()\n onAddToCart?.(item)\n }}\n >\n {addToCartLabel}\n </Button>\n )}\n </div>\n </CardContent>\n </Card>\n )\n}\n\n/**\n * Canonical product card — the item cell of a product listing. Renders only\n * the canonical `ProductPresentation` contract (canonical `Money`) with\n * host-supplied evidence (rating, badge, compare-at); it owns no listing state\n * and reports every action through its callbacks. Prices render through the\n * canonical `formatMoney` path, so a missing price is an unavailable state,\n * never a zero amount.\n */\nexport function ProductCard({\n item,\n presentation = 'classic',\n wishlisted,\n hideAddToCart,\n onAddToCart,\n onProductClick,\n onToggleWishlist,\n onViewSimilar,\n addToCartLabel = 'Agregar al carrito',\n unavailablePriceLabel = 'Precio no disponible',\n addToWishlistLabel = 'Añadir a favoritos',\n removeFromWishlistLabel = 'Quitar de favoritos',\n viewSimilarLabel = 'Ver similares',\n}: ProductCardProps) {\n const shared: PresentationProps = {\n item,\n presentation,\n wishlisted,\n hideAddToCart,\n onAddToCart,\n onProductClick,\n onToggleWishlist,\n onViewSimilar,\n addToCartLabel,\n unavailablePriceLabel,\n addToWishlistLabel,\n removeFromWishlistLabel,\n viewSimilarLabel,\n }\n\n if (presentation === 'minimal') return <MinimalPresentation {...shared} />\n if (presentation === 'rich') return <RichPresentation {...shared} />\n return <ClassicPresentation {...shared} />\n}\n\nexport default ProductCard\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"product-showcase-1.d.ts","sourceRoot":"","sources":["../../../../src/blocks/e-commerce/product-list/product-showcase-1.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAEjE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,mBAAmB,EAAE,CAAA;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAA;IACpD,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAA;CACxD;AAuBD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,OAAO,EACP,WAAW,EACX,QAA4B,EAC5B,SAAsB,EACtB,SAAuB,EACvB,SAAoB,EACpB,qBAA8C,EAC9C,UAA6C,EAC7C,cAAqC,EACrC,WAAW,EACX,cAAc,GACf,EAAE,qBAAqB,+BA0MvB;AAED,eAAe,gBAAgB,CAAA"}
1
+ {"version":3,"file":"product-showcase-1.d.ts","sourceRoot":"","sources":["../../../../src/blocks/e-commerce/product-list/product-showcase-1.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAEjE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,mBAAmB,EAAE,CAAA;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAA;IACpD,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAA;CACxD;AAuBD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,OAAO,EACP,WAAW,EACX,QAA4B,EAC5B,SAAsB,EACtB,SAAuB,EACvB,SAAoB,EACpB,qBAA8C,EAC9C,UAA6C,EAC7C,cAAqC,EACrC,WAAW,EACX,cAAc,GACf,EAAE,qBAAqB,+BA8MvB;AAED,eAAe,gBAAgB,CAAA"}
@@ -29,6 +29,7 @@ var import_lucide_react = require("lucide-react");
29
29
  var import_react2 = require("motion/react");
30
30
  var import_button = require("../../../ui/button.js");
31
31
  var import_card = require("../../../ui/card.js");
32
+ var import_motion = require("../../../lib/motion.js");
32
33
  var import_money = require("../../../lib/money.js");
33
34
  const DEFAULT_PAGE_SIZE = 6;
34
35
  const SWIPE_THRESHOLD = 48;
@@ -116,14 +117,18 @@ function ProductShowcase1({
116
117
  {
117
118
  initial: reduceMotion ? false : { opacity: 0 },
118
119
  animate: { opacity: 1 },
119
- transition: { duration: 0.3, ease: "easeOut" },
120
+ transition: { duration: import_motion.MOTION.durationSeconds.state, ease: import_motion.MOTION.easing.standard },
120
121
  className: "grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:gap-6",
121
122
  children: visible.map((product) => {
122
123
  const compareAt = compareAtText(product);
123
124
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
124
125
  import_card.Card,
125
126
  {
126
- className: `flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow duration-300 hover:shadow-md ${onProductClick ? "cursor-pointer" : ""}`,
127
+ className: `flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow hover:shadow-md ${onProductClick ? "cursor-pointer" : ""}`,
128
+ style: {
129
+ transitionDuration: `${import_motion.MOTION.durationMilliseconds.feedback}ms`,
130
+ transitionTimingFunction: import_motion.MOTION.cssEasing.standard
131
+ },
127
132
  onClick: onProductClick ? () => onProductClick(product) : void 0,
128
133
  children: [
129
134
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_card.CardContent, { className: "flex flex-1 flex-col gap-4 px-4", children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/blocks/e-commerce/product-list/product-showcase-1.tsx"],"sourcesContent":["'use client'\n\nimport { useMemo, useState } from 'react'\nimport { ChevronLeft, ChevronRight, Star } from 'lucide-react'\nimport { motion, useReducedMotion } from 'motion/react'\n\nimport { Button } from '../../../ui/button'\nimport { Card, CardContent, CardFooter } from '../../../ui/card'\nimport { formatMoney } from '../../../lib/money'\nimport type { ProductPresentation } from '../../../lib/contracts'\n\nexport interface ProductShowcase1Props {\n products: ProductPresentation[]\n heading?: string\n description?: string\n pageSize?: number\n prevLabel?: string\n nextLabel?: string\n /** Label for the current-page status, e.g. \"Página\". */\n pageLabel?: string\n unavailablePriceLabel?: string\n emptyLabel?: string\n addToCartLabel?: string\n onAddToCart?: (product: ProductPresentation) => void\n onProductClick?: (product: ProductPresentation) => void\n}\n\nconst DEFAULT_PAGE_SIZE = 6\nconst SWIPE_THRESHOLD = 48\n\nfunction chunk<T>(items: readonly T[], size: number): T[][] {\n const pages: T[][] = []\n for (let index = 0; index < items.length; index += size) {\n pages.push(items.slice(index, index + size))\n }\n return pages\n}\n\nfunction compareAtText(product: ProductPresentation): string | undefined {\n const compareAt = product.compareAtPrice\n if (!compareAt) return undefined\n const formatted = formatMoney(compareAt)\n const price = formatMoney(product.price)\n if (!formatted || !price) return undefined\n if (Number(compareAt.amount) <= Number(product.price.amount)) return undefined\n return formatted\n}\n\n/**\n * Combined grid-and-carousel product presentation: fixed-size grid pages\n * navigated manually (prev/next) with a visible current-page indication. There\n * is no autoplay, no auto-advance, and no infinite loop. Controls are real\n * buttons (keyboard operable and visible at every breakpoint); touch swipe is an\n * addition, never the only way to navigate. Below-fold images are lazy.\n *\n * Data access lives in the host: this block only renders the props it receives\n * and reports interactions through callbacks (no backend/router/measurement\n * imports).\n */\nexport function ProductShowcase1({\n products,\n heading,\n description,\n pageSize = DEFAULT_PAGE_SIZE,\n prevLabel = 'Anterior',\n nextLabel = 'Siguiente',\n pageLabel = 'Página',\n unavailablePriceLabel = 'Precio no disponible',\n emptyLabel = 'No hay productos para mostrar.',\n addToCartLabel = 'Agregar al carrito',\n onAddToCart,\n onProductClick,\n}: ProductShowcase1Props) {\n const reduceMotion = useReducedMotion()\n const size = Number.isInteger(pageSize) && pageSize > 0 ? pageSize : DEFAULT_PAGE_SIZE\n const pages = useMemo(() => chunk(products, size), [products, size])\n const pageCount = pages.length\n const [page, setPage] = useState(0)\n const [touchStart, setTouchStart] = useState<number | null>(null)\n const safePage = pageCount === 0 ? 0 : Math.min(page, pageCount - 1)\n\n const goTo = (next: number) => {\n if (pageCount === 0) return\n if (next < 0 || next >= pageCount) return\n setPage(next)\n }\n\n if (pageCount === 0) {\n return (\n <section className='py-12'>\n <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>\n {heading && (\n <h2 className='mb-4 text-center text-2xl font-bold text-balance md:text-3xl'>\n {heading}\n </h2>\n )}\n <p className='text-muted-foreground text-center'>{emptyLabel}</p>\n </div>\n </section>\n )\n }\n\n const visible = pages[safePage]\n\n return (\n <section\n className='py-12'\n aria-roledescription='carousel'\n onKeyDown={(event) => {\n if (event.key === 'ArrowLeft') {\n event.preventDefault()\n goTo(safePage - 1)\n } else if (event.key === 'ArrowRight') {\n event.preventDefault()\n goTo(safePage + 1)\n }\n }}\n >\n <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>\n {(heading || description) && (\n <div className='mb-8 text-center'>\n {heading && (\n <h2 className='text-2xl font-bold text-balance md:text-3xl'>{heading}</h2>\n )}\n {description && <p className='text-muted-foreground mt-2 text-balance'>{description}</p>}\n </div>\n )}\n\n <div\n onTouchStart={(event) => setTouchStart(event.touches[0]?.clientX ?? null)}\n onTouchEnd={(event) => {\n if (touchStart === null) return\n const delta = (event.changedTouches[0]?.clientX ?? touchStart) - touchStart\n if (delta <= -SWIPE_THRESHOLD) goTo(safePage + 1)\n else if (delta >= SWIPE_THRESHOLD) goTo(safePage - 1)\n setTouchStart(null)\n }}\n >\n <motion.div\n key={safePage}\n initial={reduceMotion ? false : { opacity: 0 }}\n animate={{ opacity: 1 }}\n transition={{ duration: 0.3, ease: 'easeOut' }}\n className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:gap-6'\n >\n {visible.map((product) => {\n const compareAt = compareAtText(product)\n return (\n <Card\n key={product.id}\n className={`flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow duration-300 hover:shadow-md ${\n onProductClick ? 'cursor-pointer' : ''\n }`}\n onClick={onProductClick ? () => onProductClick(product) : undefined}\n >\n <CardContent className='flex flex-1 flex-col gap-4 px-4'>\n <div className='relative aspect-square overflow-hidden rounded-md'>\n {product.badge && (\n <span className='bg-destructive text-destructive-foreground absolute top-2 left-2 z-10 rounded-full px-2 py-0.5 text-xs font-semibold'>\n {product.badge}\n </span>\n )}\n <img\n src={product.image || undefined}\n alt={product.imageAlt ?? product.title}\n className='size-full rounded-md object-contain'\n loading='lazy'\n width={400}\n height={400}\n />\n </div>\n\n <div className='flex flex-1 flex-col'>\n <h3 className='mb-1 font-medium text-balance'>\n {onProductClick ? (\n <button\n type='button'\n onClick={(event) => {\n event.stopPropagation()\n onProductClick(product)\n }}\n className='hover:text-primary cursor-pointer text-left transition-colors'\n >\n {product.title}\n </button>\n ) : (\n product.title\n )}\n </h3>\n {typeof product.rating === 'number' && Number.isFinite(product.rating) && (\n <div className='text-muted-foreground mb-1 flex items-center gap-1 text-xs'>\n <Star className='size-3.5 fill-current' aria-hidden='true' />\n <span>{product.rating.toFixed(1)}</span>\n </div>\n )}\n <div className='mt-auto flex items-baseline gap-2'>\n <p className='font-semibold'>\n {formatMoney(product.price) ?? unavailablePriceLabel}\n </p>\n {compareAt && (\n <p className='text-muted-foreground text-sm line-through'>{compareAt}</p>\n )}\n </div>\n </div>\n </CardContent>\n\n <CardFooter className='bg-transparent border-t-0 px-3 pt-0 md:px-4'>\n <Button\n variant='outline'\n size='sm'\n className='h-8 w-full cursor-pointer px-3 text-sm'\n onClick={(event) => {\n event.stopPropagation()\n onAddToCart?.(product)\n }}\n >\n {addToCartLabel}\n </Button>\n </CardFooter>\n </Card>\n )\n })}\n </motion.div>\n </div>\n\n {pageCount > 1 && (\n <div className='mt-6 flex items-center justify-center gap-3'>\n <Button\n type='button'\n variant='outline'\n size='icon-sm'\n className='cursor-pointer'\n aria-label={prevLabel}\n disabled={safePage === 0}\n onClick={() => goTo(safePage - 1)}\n >\n <ChevronLeft className='size-4' aria-hidden='true' />\n </Button>\n\n <div className='flex items-center gap-2'>\n {pages.map((_, index) => (\n <button\n key={index}\n type='button'\n aria-label={`${pageLabel} ${index + 1}`}\n aria-current={index === safePage ? 'true' : undefined}\n className={`size-2 rounded-full transition-colors ${\n index === safePage ? 'bg-primary' : 'bg-muted-foreground/40'\n }`}\n onClick={() => goTo(index)}\n />\n ))}\n </div>\n\n <Button\n type='button'\n variant='outline'\n size='icon-sm'\n className='cursor-pointer'\n aria-label={nextLabel}\n disabled={safePage === pageCount - 1}\n onClick={() => goTo(safePage + 1)}\n >\n <ChevronRight className='size-4' aria-hidden='true' />\n </Button>\n </div>\n )}\n\n <p aria-live='polite' className='sr-only'>\n {pageLabel} {safePage + 1} / {pageCount}\n </p>\n </div>\n </section>\n )\n}\n\nexport default ProductShowcase1\n"],"mappings":"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","names":["import_react"]}
1
+ {"version":3,"sources":["../../../../src/blocks/e-commerce/product-list/product-showcase-1.tsx"],"sourcesContent":["'use client'\n\nimport { useMemo, useState } from 'react'\nimport { ChevronLeft, ChevronRight, Star } from 'lucide-react'\nimport { motion, useReducedMotion } from 'motion/react'\n\nimport { Button } from '../../../ui/button'\nimport { Card, CardContent, CardFooter } from '../../../ui/card'\nimport { MOTION } from '../../../lib/motion'\nimport { formatMoney } from '../../../lib/money'\nimport type { ProductPresentation } from '../../../lib/contracts'\n\nexport interface ProductShowcase1Props {\n products: ProductPresentation[]\n heading?: string\n description?: string\n pageSize?: number\n prevLabel?: string\n nextLabel?: string\n /** Label for the current-page status, e.g. \"Página\". */\n pageLabel?: string\n unavailablePriceLabel?: string\n emptyLabel?: string\n addToCartLabel?: string\n onAddToCart?: (product: ProductPresentation) => void\n onProductClick?: (product: ProductPresentation) => void\n}\n\nconst DEFAULT_PAGE_SIZE = 6\nconst SWIPE_THRESHOLD = 48\n\nfunction chunk<T>(items: readonly T[], size: number): T[][] {\n const pages: T[][] = []\n for (let index = 0; index < items.length; index += size) {\n pages.push(items.slice(index, index + size))\n }\n return pages\n}\n\nfunction compareAtText(product: ProductPresentation): string | undefined {\n const compareAt = product.compareAtPrice\n if (!compareAt) return undefined\n const formatted = formatMoney(compareAt)\n const price = formatMoney(product.price)\n if (!formatted || !price) return undefined\n if (Number(compareAt.amount) <= Number(product.price.amount)) return undefined\n return formatted\n}\n\n/**\n * Combined grid-and-carousel product presentation: fixed-size grid pages\n * navigated manually (prev/next) with a visible current-page indication. There\n * is no autoplay, no auto-advance, and no infinite loop. Controls are real\n * buttons (keyboard operable and visible at every breakpoint); touch swipe is an\n * addition, never the only way to navigate. Below-fold images are lazy.\n *\n * Data access lives in the host: this block only renders the props it receives\n * and reports interactions through callbacks (no backend/router/measurement\n * imports).\n */\nexport function ProductShowcase1({\n products,\n heading,\n description,\n pageSize = DEFAULT_PAGE_SIZE,\n prevLabel = 'Anterior',\n nextLabel = 'Siguiente',\n pageLabel = 'Página',\n unavailablePriceLabel = 'Precio no disponible',\n emptyLabel = 'No hay productos para mostrar.',\n addToCartLabel = 'Agregar al carrito',\n onAddToCart,\n onProductClick,\n}: ProductShowcase1Props) {\n const reduceMotion = useReducedMotion()\n const size = Number.isInteger(pageSize) && pageSize > 0 ? pageSize : DEFAULT_PAGE_SIZE\n const pages = useMemo(() => chunk(products, size), [products, size])\n const pageCount = pages.length\n const [page, setPage] = useState(0)\n const [touchStart, setTouchStart] = useState<number | null>(null)\n const safePage = pageCount === 0 ? 0 : Math.min(page, pageCount - 1)\n\n const goTo = (next: number) => {\n if (pageCount === 0) return\n if (next < 0 || next >= pageCount) return\n setPage(next)\n }\n\n if (pageCount === 0) {\n return (\n <section className='py-12'>\n <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>\n {heading && (\n <h2 className='mb-4 text-center text-2xl font-bold text-balance md:text-3xl'>\n {heading}\n </h2>\n )}\n <p className='text-muted-foreground text-center'>{emptyLabel}</p>\n </div>\n </section>\n )\n }\n\n const visible = pages[safePage]\n\n return (\n <section\n className='py-12'\n aria-roledescription='carousel'\n onKeyDown={(event) => {\n if (event.key === 'ArrowLeft') {\n event.preventDefault()\n goTo(safePage - 1)\n } else if (event.key === 'ArrowRight') {\n event.preventDefault()\n goTo(safePage + 1)\n }\n }}\n >\n <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>\n {(heading || description) && (\n <div className='mb-8 text-center'>\n {heading && (\n <h2 className='text-2xl font-bold text-balance md:text-3xl'>{heading}</h2>\n )}\n {description && <p className='text-muted-foreground mt-2 text-balance'>{description}</p>}\n </div>\n )}\n\n <div\n onTouchStart={(event) => setTouchStart(event.touches[0]?.clientX ?? null)}\n onTouchEnd={(event) => {\n if (touchStart === null) return\n const delta = (event.changedTouches[0]?.clientX ?? touchStart) - touchStart\n if (delta <= -SWIPE_THRESHOLD) goTo(safePage + 1)\n else if (delta >= SWIPE_THRESHOLD) goTo(safePage - 1)\n setTouchStart(null)\n }}\n >\n <motion.div\n key={safePage}\n initial={reduceMotion ? false : { opacity: 0 }}\n animate={{ opacity: 1 }}\n transition={{ duration: MOTION.durationSeconds.state, ease: MOTION.easing.standard }}\n className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:gap-6'\n >\n {visible.map((product) => {\n const compareAt = compareAtText(product)\n return (\n <Card\n key={product.id}\n className={`flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow hover:shadow-md ${\n onProductClick ? 'cursor-pointer' : ''\n }`}\n style={{\n transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,\n transitionTimingFunction: MOTION.cssEasing.standard,\n }}\n onClick={onProductClick ? () => onProductClick(product) : undefined}\n >\n <CardContent className='flex flex-1 flex-col gap-4 px-4'>\n <div className='relative aspect-square overflow-hidden rounded-md'>\n {product.badge && (\n <span className='bg-destructive text-destructive-foreground absolute top-2 left-2 z-10 rounded-full px-2 py-0.5 text-xs font-semibold'>\n {product.badge}\n </span>\n )}\n <img\n src={product.image || undefined}\n alt={product.imageAlt ?? product.title}\n className='size-full rounded-md object-contain'\n loading='lazy'\n width={400}\n height={400}\n />\n </div>\n\n <div className='flex flex-1 flex-col'>\n <h3 className='mb-1 font-medium text-balance'>\n {onProductClick ? (\n <button\n type='button'\n onClick={(event) => {\n event.stopPropagation()\n onProductClick(product)\n }}\n className='hover:text-primary cursor-pointer text-left transition-colors'\n >\n {product.title}\n </button>\n ) : (\n product.title\n )}\n </h3>\n {typeof product.rating === 'number' && Number.isFinite(product.rating) && (\n <div className='text-muted-foreground mb-1 flex items-center gap-1 text-xs'>\n <Star className='size-3.5 fill-current' aria-hidden='true' />\n <span>{product.rating.toFixed(1)}</span>\n </div>\n )}\n <div className='mt-auto flex items-baseline gap-2'>\n <p className='font-semibold'>\n {formatMoney(product.price) ?? unavailablePriceLabel}\n </p>\n {compareAt && (\n <p className='text-muted-foreground text-sm line-through'>{compareAt}</p>\n )}\n </div>\n </div>\n </CardContent>\n\n <CardFooter className='bg-transparent border-t-0 px-3 pt-0 md:px-4'>\n <Button\n variant='outline'\n size='sm'\n className='h-8 w-full cursor-pointer px-3 text-sm'\n onClick={(event) => {\n event.stopPropagation()\n onAddToCart?.(product)\n }}\n >\n {addToCartLabel}\n </Button>\n </CardFooter>\n </Card>\n )\n })}\n </motion.div>\n </div>\n\n {pageCount > 1 && (\n <div className='mt-6 flex items-center justify-center gap-3'>\n <Button\n type='button'\n variant='outline'\n size='icon-sm'\n className='cursor-pointer'\n aria-label={prevLabel}\n disabled={safePage === 0}\n onClick={() => goTo(safePage - 1)}\n >\n <ChevronLeft className='size-4' aria-hidden='true' />\n </Button>\n\n <div className='flex items-center gap-2'>\n {pages.map((_, index) => (\n <button\n key={index}\n type='button'\n aria-label={`${pageLabel} ${index + 1}`}\n aria-current={index === safePage ? 'true' : undefined}\n className={`size-2 rounded-full transition-colors ${\n index === safePage ? 'bg-primary' : 'bg-muted-foreground/40'\n }`}\n onClick={() => goTo(index)}\n />\n ))}\n </div>\n\n <Button\n type='button'\n variant='outline'\n size='icon-sm'\n className='cursor-pointer'\n aria-label={nextLabel}\n disabled={safePage === pageCount - 1}\n onClick={() => goTo(safePage + 1)}\n >\n <ChevronRight className='size-4' aria-hidden='true' />\n </Button>\n </div>\n )}\n\n <p aria-live='polite' className='sr-only'>\n {pageLabel} {safePage + 1} / {pageCount}\n </p>\n </div>\n </section>\n )\n}\n\nexport default ProductShowcase1\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2FQ;AAzFR,mBAAkC;AAClC,0BAAgD;AAChD,IAAAA,gBAAyC;AAEzC,oBAAuB;AACvB,kBAA8C;AAC9C,oBAAuB;AACvB,mBAA4B;AAmB5B,MAAM,oBAAoB;AAC1B,MAAM,kBAAkB;AAExB,SAAS,MAAS,OAAqB,MAAqB;AAC1D,QAAM,QAAe,CAAC;AACtB,WAAS,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,MAAM;AACvD,UAAM,KAAK,MAAM,MAAM,OAAO,QAAQ,IAAI,CAAC;AAAA,EAC7C;AACA,SAAO;AACT;AAEA,SAAS,cAAc,SAAkD;AACvE,QAAM,YAAY,QAAQ;AAC1B,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,gBAAY,0BAAY,SAAS;AACvC,QAAM,YAAQ,0BAAY,QAAQ,KAAK;AACvC,MAAI,CAAC,aAAa,CAAC,MAAO,QAAO;AACjC,MAAI,OAAO,UAAU,MAAM,KAAK,OAAO,QAAQ,MAAM,MAAM,EAAG,QAAO;AACrE,SAAO;AACT;AAaO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,wBAAwB;AAAA,EACxB,aAAa;AAAA,EACb,iBAAiB;AAAA,EACjB;AAAA,EACA;AACF,GAA0B;AACxB,QAAM,mBAAe,gCAAiB;AACtC,QAAM,OAAO,OAAO,UAAU,QAAQ,KAAK,WAAW,IAAI,WAAW;AACrE,QAAM,YAAQ,sBAAQ,MAAM,MAAM,UAAU,IAAI,GAAG,CAAC,UAAU,IAAI,CAAC;AACnE,QAAM,YAAY,MAAM;AACxB,QAAM,CAAC,MAAM,OAAO,QAAI,uBAAS,CAAC;AAClC,QAAM,CAAC,YAAY,aAAa,QAAI,uBAAwB,IAAI;AAChE,QAAM,WAAW,cAAc,IAAI,IAAI,KAAK,IAAI,MAAM,YAAY,CAAC;AAEnE,QAAM,OAAO,CAAC,SAAiB;AAC7B,QAAI,cAAc,EAAG;AACrB,QAAI,OAAO,KAAK,QAAQ,UAAW;AACnC,YAAQ,IAAI;AAAA,EACd;AAEA,MAAI,cAAc,GAAG;AACnB,WACE,4CAAC,aAAQ,WAAU,SACjB,uDAAC,SAAI,WAAU,iDACZ;AAAA,iBACC,4CAAC,QAAG,WAAU,gEACX,mBACH;AAAA,MAEF,4CAAC,OAAE,WAAU,qCAAqC,sBAAW;AAAA,OAC/D,GACF;AAAA,EAEJ;AAEA,QAAM,UAAU,MAAM,QAAQ;AAE9B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,wBAAqB;AAAA,MACrB,WAAW,CAAC,UAAU;AACpB,YAAI,MAAM,QAAQ,aAAa;AAC7B,gBAAM,eAAe;AACrB,eAAK,WAAW,CAAC;AAAA,QACnB,WAAW,MAAM,QAAQ,cAAc;AACrC,gBAAM,eAAe;AACrB,eAAK,WAAW,CAAC;AAAA,QACnB;AAAA,MACF;AAAA,MAEA,uDAAC,SAAI,WAAU,iDACX;AAAA,oBAAW,gBACX,6CAAC,SAAI,WAAU,oBACZ;AAAA,qBACC,4CAAC,QAAG,WAAU,+CAA+C,mBAAQ;AAAA,UAEtE,eAAe,4CAAC,OAAE,WAAU,2CAA2C,uBAAY;AAAA,WACtF;AAAA,QAGF;AAAA,UAAC;AAAA;AAAA,YACC,cAAc,CAAC,UAAU,cAAc,MAAM,QAAQ,CAAC,GAAG,WAAW,IAAI;AAAA,YACxE,YAAY,CAAC,UAAU;AACrB,kBAAI,eAAe,KAAM;AACzB,oBAAM,SAAS,MAAM,eAAe,CAAC,GAAG,WAAW,cAAc;AACjE,kBAAI,SAAS,CAAC,gBAAiB,MAAK,WAAW,CAAC;AAAA,uBACvC,SAAS,gBAAiB,MAAK,WAAW,CAAC;AACpD,4BAAc,IAAI;AAAA,YACpB;AAAA,YAEA;AAAA,cAAC,qBAAO;AAAA,cAAP;AAAA,gBAEC,SAAS,eAAe,QAAQ,EAAE,SAAS,EAAE;AAAA,gBAC7C,SAAS,EAAE,SAAS,EAAE;AAAA,gBACtB,YAAY,EAAE,UAAU,qBAAO,gBAAgB,OAAO,MAAM,qBAAO,OAAO,SAAS;AAAA,gBACnF,WAAU;AAAA,gBAET,kBAAQ,IAAI,CAAC,YAAY;AACxB,wBAAM,YAAY,cAAc,OAAO;AACvC,yBACE;AAAA,oBAAC;AAAA;AAAA,sBAEC,WAAW,4GACT,iBAAiB,mBAAmB,EACtC;AAAA,sBACA,OAAO;AAAA,wBACL,oBAAoB,GAAG,qBAAO,qBAAqB,QAAQ;AAAA,wBAC3D,0BAA0B,qBAAO,UAAU;AAAA,sBAC7C;AAAA,sBACA,SAAS,iBAAiB,MAAM,eAAe,OAAO,IAAI;AAAA,sBAE1D;AAAA,qEAAC,2BAAY,WAAU,mCACrB;AAAA,uEAAC,SAAI,WAAU,qDACZ;AAAA,oCAAQ,SACP,4CAAC,UAAK,WAAU,wHACb,kBAAQ,OACX;AAAA,4BAEF;AAAA,8BAAC;AAAA;AAAA,gCACC,KAAK,QAAQ,SAAS;AAAA,gCACtB,KAAK,QAAQ,YAAY,QAAQ;AAAA,gCACjC,WAAU;AAAA,gCACV,SAAQ;AAAA,gCACR,OAAO;AAAA,gCACP,QAAQ;AAAA;AAAA,4BACV;AAAA,6BACF;AAAA,0BAEA,6CAAC,SAAI,WAAU,wBACb;AAAA,wEAAC,QAAG,WAAU,iCACX,2BACC;AAAA,8BAAC;AAAA;AAAA,gCACC,MAAK;AAAA,gCACL,SAAS,CAAC,UAAU;AAClB,wCAAM,gBAAgB;AACtB,iDAAe,OAAO;AAAA,gCACxB;AAAA,gCACA,WAAU;AAAA,gCAET,kBAAQ;AAAA;AAAA,4BACX,IAEA,QAAQ,OAEZ;AAAA,4BACC,OAAO,QAAQ,WAAW,YAAY,OAAO,SAAS,QAAQ,MAAM,KACnE,6CAAC,SAAI,WAAU,8DACb;AAAA,0EAAC,4BAAK,WAAU,yBAAwB,eAAY,QAAO;AAAA,8BAC3D,4CAAC,UAAM,kBAAQ,OAAO,QAAQ,CAAC,GAAE;AAAA,+BACnC;AAAA,4BAEF,6CAAC,SAAI,WAAU,qCACb;AAAA,0EAAC,OAAE,WAAU,iBACV,wCAAY,QAAQ,KAAK,KAAK,uBACjC;AAAA,8BACC,aACC,4CAAC,OAAE,WAAU,8CAA8C,qBAAU;AAAA,+BAEzE;AAAA,6BACF;AAAA,2BACF;AAAA,wBAEA,4CAAC,0BAAW,WAAU,+CACpB;AAAA,0BAAC;AAAA;AAAA,4BACC,SAAQ;AAAA,4BACR,MAAK;AAAA,4BACL,WAAU;AAAA,4BACV,SAAS,CAAC,UAAU;AAClB,oCAAM,gBAAgB;AACtB,4CAAc,OAAO;AAAA,4BACvB;AAAA,4BAEC;AAAA;AAAA,wBACH,GACF;AAAA;AAAA;AAAA,oBAzEK,QAAQ;AAAA,kBA0Ef;AAAA,gBAEJ,CAAC;AAAA;AAAA,cAtFI;AAAA,YAuFP;AAAA;AAAA,QACF;AAAA,QAEC,YAAY,KACX,6CAAC,SAAI,WAAU,+CACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,WAAU;AAAA,cACV,cAAY;AAAA,cACZ,UAAU,aAAa;AAAA,cACvB,SAAS,MAAM,KAAK,WAAW,CAAC;AAAA,cAEhC,sDAAC,mCAAY,WAAU,UAAS,eAAY,QAAO;AAAA;AAAA,UACrD;AAAA,UAEA,4CAAC,SAAI,WAAU,2BACZ,gBAAM,IAAI,CAAC,GAAG,UACb;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,cAAY,GAAG,SAAS,IAAI,QAAQ,CAAC;AAAA,cACrC,gBAAc,UAAU,WAAW,SAAS;AAAA,cAC5C,WAAW,yCACT,UAAU,WAAW,eAAe,wBACtC;AAAA,cACA,SAAS,MAAM,KAAK,KAAK;AAAA;AAAA,YAPpB;AAAA,UAQP,CACD,GACH;AAAA,UAEA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,WAAU;AAAA,cACV,cAAY;AAAA,cACZ,UAAU,aAAa,YAAY;AAAA,cACnC,SAAS,MAAM,KAAK,WAAW,CAAC;AAAA,cAEhC,sDAAC,oCAAa,WAAU,UAAS,eAAY,QAAO;AAAA;AAAA,UACtD;AAAA,WACF;AAAA,QAGF,6CAAC,OAAE,aAAU,UAAS,WAAU,WAC7B;AAAA;AAAA,UAAU;AAAA,UAAE,WAAW;AAAA,UAAE;AAAA,UAAI;AAAA,WAChC;AAAA,SACF;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,6BAAQ;","names":["import_react"]}
@@ -5,6 +5,7 @@ import { ChevronLeft, ChevronRight, Star } from "lucide-react";
5
5
  import { motion, useReducedMotion } from "motion/react";
6
6
  import { Button } from "../../../ui/button.mjs";
7
7
  import { Card, CardContent, CardFooter } from "../../../ui/card.mjs";
8
+ import { MOTION } from "../../../lib/motion.mjs";
8
9
  import { formatMoney } from "../../../lib/money.mjs";
9
10
  const DEFAULT_PAGE_SIZE = 6;
10
11
  const SWIPE_THRESHOLD = 48;
@@ -92,14 +93,18 @@ function ProductShowcase1({
92
93
  {
93
94
  initial: reduceMotion ? false : { opacity: 0 },
94
95
  animate: { opacity: 1 },
95
- transition: { duration: 0.3, ease: "easeOut" },
96
+ transition: { duration: MOTION.durationSeconds.state, ease: MOTION.easing.standard },
96
97
  className: "grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:gap-6",
97
98
  children: visible.map((product) => {
98
99
  const compareAt = compareAtText(product);
99
100
  return /* @__PURE__ */ jsxs(
100
101
  Card,
101
102
  {
102
- className: `flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow duration-300 hover:shadow-md ${onProductClick ? "cursor-pointer" : ""}`,
103
+ className: `flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow hover:shadow-md ${onProductClick ? "cursor-pointer" : ""}`,
104
+ style: {
105
+ transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,
106
+ transitionTimingFunction: MOTION.cssEasing.standard
107
+ },
103
108
  onClick: onProductClick ? () => onProductClick(product) : void 0,
104
109
  children: [
105
110
  /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-1 flex-col gap-4 px-4", children: [