@thorprovider/blocks 3.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -21
- package/dist/blocks/designs/checkout-commerce-v1/block.d.ts +49 -0
- package/dist/blocks/designs/checkout-commerce-v1/block.d.ts.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/block.js +2487 -0
- package/dist/blocks/designs/checkout-commerce-v1/block.js.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/block.mjs +2467 -0
- package/dist/blocks/designs/checkout-commerce-v1/block.mjs.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/data-context.d.ts +8 -0
- package/dist/blocks/designs/checkout-commerce-v1/data-context.d.ts.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/data-context.js +44 -0
- package/dist/blocks/designs/checkout-commerce-v1/data-context.js.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/data-context.mjs +19 -0
- package/dist/blocks/designs/checkout-commerce-v1/data-context.mjs.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/index.d.ts +3 -0
- package/dist/blocks/designs/checkout-commerce-v1/index.d.ts.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/index.js +36 -0
- package/dist/blocks/designs/checkout-commerce-v1/index.js.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/index.mjs +6 -0
- package/dist/blocks/designs/checkout-commerce-v1/index.mjs.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/block.d.ts +12 -0
- package/dist/blocks/designs/marketplace-product-card/block.d.ts.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/block.js +243 -0
- package/dist/blocks/designs/marketplace-product-card/block.js.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/block.mjs +233 -0
- package/dist/blocks/designs/marketplace-product-card/block.mjs.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/index.d.ts +3 -0
- package/dist/blocks/designs/marketplace-product-card/index.d.ts.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/index.js +36 -0
- package/dist/blocks/designs/marketplace-product-card/index.js.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/index.mjs +6 -0
- package/dist/blocks/designs/marketplace-product-card/index.mjs.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.d.ts +13 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.d.ts.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.js +36 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.js.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.mjs +12 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.mjs.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/variants.d.ts +72 -0
- package/dist/blocks/designs/marketplace-product-card/variants.d.ts.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/variants.js +123 -0
- package/dist/blocks/designs/marketplace-product-card/variants.js.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/variants.mjs +89 -0
- package/dist/blocks/designs/marketplace-product-card/variants.mjs.map +1 -0
- package/dist/blocks/e-commerce/account/account-frame.d.ts +38 -0
- package/dist/blocks/e-commerce/account/account-frame.d.ts.map +1 -0
- package/dist/blocks/e-commerce/account/account-frame.js +102 -0
- package/dist/blocks/e-commerce/account/account-frame.js.map +1 -0
- package/dist/blocks/e-commerce/account/account-frame.mjs +78 -0
- package/dist/blocks/e-commerce/account/account-frame.mjs.map +1 -0
- package/dist/blocks/e-commerce/account/address-book.d.ts +79 -0
- package/dist/blocks/e-commerce/account/address-book.d.ts.map +1 -0
- package/dist/blocks/e-commerce/account/address-book.js +250 -0
- package/dist/blocks/e-commerce/account/address-book.js.map +1 -0
- package/dist/blocks/e-commerce/account/address-book.mjs +225 -0
- package/dist/blocks/e-commerce/account/address-book.mjs.map +1 -0
- package/dist/blocks/e-commerce/account/profile-form.d.ts +35 -0
- package/dist/blocks/e-commerce/account/profile-form.d.ts.map +1 -0
- package/dist/blocks/e-commerce/account/profile-form.js +100 -0
- package/dist/blocks/e-commerce/account/profile-form.js.map +1 -0
- package/dist/blocks/e-commerce/account/profile-form.mjs +76 -0
- package/dist/blocks/e-commerce/account/profile-form.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkbox-check.js +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkbox-check.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkbox-check.mjs +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkbox-check.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.d.ts +11 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.js +46 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.mjs +22 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.d.ts +28 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.js +278 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.mjs +260 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.d.ts +12 -9
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.js +49 -71
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.mjs +50 -72
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.js +6 -3
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.mjs +6 -3
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.d.ts +7 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.js +41 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.mjs +16 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.d.ts +24 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.js +75 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.mjs +58 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.d.ts +3 -2
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.js +9 -2
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.mjs +9 -2
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.d.ts +23 -8
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.js +136 -31
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.mjs +143 -32
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.d.ts +13 -7
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.js +179 -28
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.mjs +180 -29
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.d.ts +20 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.js +199 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.mjs +175 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.d.ts +14 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.js +136 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.mjs +109 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.d.ts +32 -10
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.js +265 -147
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.mjs +272 -148
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.d.ts +13 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.js +74 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.mjs +50 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.d.ts +7 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.js +91 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.mjs +65 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout.d.ts +174 -44
- package/dist/blocks/e-commerce/checkout-forms/checkout.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout.js +532 -108
- package/dist/blocks/e-commerce/checkout-forms/checkout.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout.mjs +542 -109
- package/dist/blocks/e-commerce/checkout-forms/checkout.mjs.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.d.ts.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.js +44 -20
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.js.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.mjs +44 -20
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.mjs.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar.d.ts.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar.js +21 -10
- package/dist/blocks/e-commerce/navigation/storefront-navbar.js.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar.mjs +21 -10
- package/dist/blocks/e-commerce/navigation/storefront-navbar.mjs.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-card.d.ts.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-card.js +36 -9
- package/dist/blocks/e-commerce/product-list/product-card.js.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-card.mjs +36 -9
- package/dist/blocks/e-commerce/product-list/product-card.mjs.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-showcase-1.d.ts.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-showcase-1.js +7 -2
- package/dist/blocks/e-commerce/product-list/product-showcase-1.js.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-showcase-1.mjs +7 -2
- package/dist/blocks/e-commerce/product-list/product-showcase-1.mjs.map +1 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.d.ts.map +1 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.js +5 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.js.map +1 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.mjs +5 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.mjs.map +1 -1
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.d.ts.map +1 -1
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.js +71 -51
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.js.map +1 -1
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.mjs +71 -51
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.mjs.map +1 -1
- package/dist/blocks/marketing/banners/banner-1.d.ts.map +1 -1
- package/dist/blocks/marketing/banners/banner-1.js +15 -27
- package/dist/blocks/marketing/banners/banner-1.js.map +1 -1
- package/dist/blocks/marketing/banners/banner-1.mjs +16 -28
- package/dist/blocks/marketing/banners/banner-1.mjs.map +1 -1
- package/dist/index.d.ts +16 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13 -0
- package/dist/index.mjs.map +1 -1
- package/dist/lib/contracts/checkout-commerce-v1.d.ts +2350 -0
- package/dist/lib/contracts/checkout-commerce-v1.d.ts.map +1 -0
- package/dist/lib/contracts/checkout-commerce-v1.js +417 -0
- package/dist/lib/contracts/checkout-commerce-v1.js.map +1 -0
- package/dist/lib/contracts/checkout-commerce-v1.mjs +392 -0
- package/dist/lib/contracts/checkout-commerce-v1.mjs.map +1 -0
- package/dist/lib/contracts/marketplace-product-card.d.ts +324 -0
- package/dist/lib/contracts/marketplace-product-card.d.ts.map +1 -0
- package/dist/lib/contracts/marketplace-product-card.js +85 -0
- package/dist/lib/contracts/marketplace-product-card.js.map +1 -0
- package/dist/lib/contracts/marketplace-product-card.mjs +61 -0
- package/dist/lib/contracts/marketplace-product-card.mjs.map +1 -0
- package/dist/lib/contracts.d.ts +2289 -178
- package/dist/lib/contracts.d.ts.map +1 -1
- package/dist/lib/contracts.js +332 -25
- package/dist/lib/contracts.js.map +1 -1
- package/dist/lib/contracts.mjs +278 -5
- package/dist/lib/contracts.mjs.map +1 -1
- package/dist/lib/money-schema.d.ts +19 -0
- package/dist/lib/money-schema.d.ts.map +1 -0
- package/dist/lib/money-schema.js +33 -0
- package/dist/lib/money-schema.js.map +1 -0
- package/dist/lib/money-schema.mjs +9 -0
- package/dist/lib/money-schema.mjs.map +1 -0
- package/dist/lib/motion.d.ts +29 -0
- package/dist/lib/motion.d.ts.map +1 -0
- package/dist/lib/motion.js +65 -0
- package/dist/lib/motion.js.map +1 -0
- package/dist/lib/motion.mjs +41 -0
- package/dist/lib/motion.mjs.map +1 -0
- package/dist/ui/accordion.d.ts +3 -1
- package/dist/ui/accordion.d.ts.map +1 -1
- package/dist/ui/accordion.js +21 -5
- package/dist/ui/accordion.js.map +1 -1
- package/dist/ui/accordion.mjs +21 -5
- package/dist/ui/accordion.mjs.map +1 -1
- package/dist/ui/input-group.d.ts +1 -1
- package/package.json +18 -3
- 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 : {
|
|
81
|
-
|
|
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
|
|
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-
|
|
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
|
|
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-
|
|
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
|
|
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-
|
|
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 : {
|
|
57
|
-
|
|
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
|
|
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-
|
|
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
|
|
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-
|
|
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
|
|
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-
|
|
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":";AAmFI,SACE,KADF;AAjFJ,SAAS,sBAAsB,OAAO,cAAc,YAAY;AAChE,SAAS,QAAQ,wBAAwB;AAEzC,SAAS,aAAa;AACtB,SAAS,cAAc;AACvB,SAAS,MAAM,aAAa,kBAAkB;AAC9C,SAAS,UAAU;AACnB,SAAS,aAAa,wBAAwB;AAkD9C,SAAS,cAAc,MAA+C;AACpE,QAAM,YAAY,KAAK;AACvB,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,YAAY,YAAY,SAAS;AACvC,QAAM,QAAQ,YAAY,KAAK,KAAK;AACpC,MAAI,CAAC,aAAa,CAAC,MAAO,QAAO;AACjC,MAAI,OAAO,UAAU,MAAM,KAAK,OAAO,KAAK,MAAM,MAAM,EAAG,QAAO;AAClE,SAAO;AACT;AAEA,SAAS,YAAY,MAA+C;AAClE,SAAO,OAAO,KAAK,WAAW,YAAY,OAAO,SAAS,KAAK,MAAM,IAAI,KAAK,SAAS;AACzF;AAEA,SAAS,SAAS;AAAA,EAChB;AAAA,EACA;AACF,GAGG;AACD,QAAM,YAAY,iBAAiB,KAAK,KAAK;AAC7C,QAAM,YAAY,cAAc,IAAI;AACpC,SACE,qBAAC,SAAI,WAAU,qCACb;AAAA,wBAAC,OAAE,WAAU,iBACV,sBAAY,YAAY,KAAK,KAAK,IAAI,uBACzC;AAAA,IACC,aACC,oBAAC,OAAE,WAAU,oFACV,qBACH;AAAA,KAEJ;AAEJ;AAEA,SAAS,UAAU,EAAE,MAAM,GAAsB;AAC/C,SACE,qBAAC,SAAI,WAAU,yDACb;AAAA,wBAAC,QAAK,WAAU,yBAAwB,eAAY,QAAO;AAAA,IAC3D,oBAAC,UAAM,gBAAM,QAAQ,CAAC,GAAE;AAAA,KAC1B;AAEJ;AAEA,SAAS,oBAAoB;AAAA,EAC3B;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAsB;AACpB,QAAM,eAAe,iBAAiB;AACtC,QAAM,YAAY,iBAAiB,KAAK,KAAK;AAC7C,QAAM,SAAS,YAAY,IAAI;AAE/B,SACE;AAAA,IAAC,OAAO;AAAA,IAAP;AAAA,MACC,SAAS,eAAe,QAAQ,EAAE,SAAS,GAAG,GAAG,GAAG;AAAA,MACpD,aAAa,EAAE,SAAS,GAAG,GAAG,EAAE;AAAA,MAChC,UAAU,EAAE,MAAM,MAAM,QAAQ,QAAQ;AAAA,MACxC,YAAY,eAAe,SAAY,EAAE,GAAG,GAAG;AAAA,MAC/C,YAAY,EAAE,UAAU,MAAM,MAAM,UAAU;AAAA,MAE9C;AAAA,QAAC;AAAA;AAAA,UACC,WAAW,yHACT,iBAAiB,mBAAmB,EACtC;AAAA,UACA,SAAS,iBAAiB,MAAM,eAAe,IAAI,IAAI;AAAA,UAEvD;AAAA,iCAAC,eAAY,WAAU,oCACrB;AAAA,mCAAC,SAAI,WAAU,qDACZ;AAAA,qBAAK,SACJ,oBAAC,UAAK,WAAU,wHACb,eAAK,OACR;AAAA,gBAED,KAAK,QACJ;AAAA,kBAAC;AAAA;AAAA,oBACC,KAAK,KAAK;AAAA,oBACV,KAAK,KAAK,YAAY,KAAK;AAAA,oBAC3B,WAAU;AAAA,oBACV,SAAQ;AAAA,oBACR,OAAO;AAAA,oBACP,QAAQ;AAAA;AAAA,gBACV,IAEA,oBAAC,SAAI,aAAU,6BAA4B,eAAY,QAAO,WAAU,iCAAgC;AAAA,iBAE5G;AAAA,cAEA,qBAAC,SAAI,WAAU,wBACb;AAAA,oCAAC,QAAG,WAAU,iCACX,2BACC;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAS,CAAC,MAAM;AACd,wBAAE,gBAAgB;AAClB,qCAAe,IAAI;AAAA,oBACrB;AAAA,oBACA,WAAU;AAAA,oBAET,eAAK;AAAA;AAAA,gBACR,IAEA,KAAK,OAET;AAAA,gBACC,WAAW,UAAa,oBAAC,aAAU,OAAO,QAAQ;AAAA,gBACnD,oBAAC,YAAS,MAAY,uBAA8C;AAAA,iBACtE;AAAA,eACF;AAAA,YAEA,oBAAC,cAAW,WAAU,+CACnB,0BAAgB,OACf;AAAA,cAAC;AAAA;AAAA,gBACC,SAAQ;AAAA,gBACR,MAAK;AAAA,gBACL,WAAU;AAAA,gBACV,UAAU,CAAC;AAAA,gBACX,SAAS,CAAC,MAAM;AACd,oBAAE,gBAAgB;AAClB,gCAAc,IAAI;AAAA,gBACpB;AAAA,gBAEC;AAAA;AAAA,YACH,GAEJ;AAAA;AAAA;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;AAEA,SAAS,oBAAoB;AAAA,EAC3B;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAsB;AACpB,QAAM,YAAY,iBAAiB,KAAK,KAAK;AAC7C,QAAM,SAAS,YAAY,IAAI;AAE/B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,kBAAkB;AAAA,MACpB;AAAA,MACA,SAAS,iBAAiB,MAAM,eAAe,IAAI,IAAI;AAAA,MAEvD,+BAAC,eAAY,WAAU,4BACrB;AAAA,6BAAC,SAAI,WAAU,qDACZ;AAAA,eAAK,SACJ,oBAAC,UAAK,WAAU,wHACb,eAAK,OACR;AAAA,UAED,KAAK,QACJ;AAAA,YAAC;AAAA;AAAA,cACC,KAAK,KAAK;AAAA,cACV,KAAK,KAAK,YAAY,KAAK;AAAA,cAC3B,OAAO;AAAA,cACP,QAAQ;AAAA,cACR,WAAU;AAAA,cACV,SAAQ;AAAA;AAAA,UACV,IAEA,oBAAC,SAAI,aAAU,6BAA4B,eAAY,QAAO,WAAU,sBAAqB;AAAA,WAEjG;AAAA,QAEA,qBAAC,SAAI,WAAU,uBACb;AAAA,8BAAC,QAAG,WAAU,mDAAmD,eAAK,OAAM;AAAA,UAC3E,WAAW,UAAa,oBAAC,aAAU,OAAO,QAAQ;AAAA,UACnD,qBAAC,SAAI,WAAU,2CACb;AAAA,gCAAC,YAAS,MAAY,uBAA8C;AAAA,YACnE,gBAAgB,OACf;AAAA,cAAC;AAAA;AAAA,gBACC,SAAQ;AAAA,gBACR,MAAK;AAAA,gBACL,WAAU;AAAA,gBACV,cAAY;AAAA,gBACZ,UAAU,CAAC;AAAA,gBACX,SAAS,CAAC,MAAM;AACd,oBAAE,gBAAgB;AAClB,gCAAc,IAAI;AAAA,gBACpB;AAAA,gBAEA,8BAAC,gBAAa,WAAU,UAAS,eAAY,QAAO;AAAA;AAAA,YACtD;AAAA,aAEJ;AAAA,WACF;AAAA,SACF;AAAA;AAAA,EACF;AAEJ;AAEA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,WAAU;AAAA,MACV,cAAY,aAAa,0BAA0B;AAAA,MACnD,gBAAc;AAAA,MACd,UAAU,CAAC;AAAA,MACX,SAAS,CAAC,MAAM;AACd,UAAE,gBAAgB;AAClB,2BAAmB,MAAM,CAAC,UAAU;AAAA,MACtC;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,aAAa,sCAAsC;AAAA,UACrD;AAAA,UACA,eAAY;AAAA;AAAA,MACd;AAAA;AAAA,EACF;AAEJ;AAEA,SAAS,iBAAiB;AAAA,EACxB;AAAA,EACA,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAsB;AACpB,QAAM,YAAY,iBAAiB,KAAK,KAAK;AAC7C,QAAM,SAAS,YAAY,IAAI;AAE/B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,kBAAkB;AAAA,MACpB;AAAA,MACA,SAAS,iBAAiB,MAAM,eAAe,IAAI,IAAI;AAAA,MAEvD,+BAAC,eAAY,WAAU,QACrB;AAAA,6BAAC,SAAI,WAAU,iDACZ;AAAA,eAAK,QACJ;AAAA,YAAC;AAAA;AAAA,cACC,KAAK,KAAK;AAAA,cACV,KAAK,KAAK,YAAY,KAAK;AAAA,cAC3B,WAAU;AAAA,cACV,SAAQ;AAAA;AAAA,UACV,IAEA,oBAAC,SAAI,aAAU,6BAA4B,eAAY,QAAO,WAAU,sBAAqB;AAAA,UAG9F,KAAK,SACJ,oBAAC,SAAM,WAAU,2GACd,eAAK,OACR;AAAA,UAGD,WAAW,UACV,qBAAC,SAAI,WAAU,gIACb;AAAA,gCAAC,QAAK,WAAU,0CAAyC,eAAY,QAAO;AAAA,YAC5E,oBAAC,UAAM,iBAAO,QAAQ,CAAC,GAAE;AAAA,aAC3B;AAAA,UAGD,iBACC,oBAAC,SAAI,WAAU,sCACb;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,WAAU;AAAA,cACV,cAAY;AAAA,cACZ,SAAS,CAAC,MAAM;AACd,kBAAE,gBAAgB;AAClB,8BAAc,IAAI;AAAA,cACpB;AAAA,cAEA;AAAA,oCAAC,wBAAqB,WAAU,oCAAmC,eAAY,QAAO;AAAA,gBACtF,oBAAC,UAAK,WAAU,oEACb,4BACH;AAAA;AAAA;AAAA,UACF,GACF;AAAA,UAGF;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA;AAAA,UACF;AAAA,WACF;AAAA,QAEA,qBAAC,SAAI,WAAU,2BACb;AAAA,8BAAC,QAAG,WAAU,4BAA4B,eAAK,OAAM;AAAA,UACrD,oBAAC,YAAS,MAAY,uBAA8C;AAAA,UACnE,gBAAgB,OACf;AAAA,YAAC;AAAA;AAAA,cACC,SAAQ;AAAA,cACR,MAAK;AAAA,cACL,WAAU;AAAA,cACV,UAAU,CAAC;AAAA,cACX,SAAS,CAAC,MAAM;AACd,kBAAE,gBAAgB;AAClB,8BAAc,IAAI;AAAA,cACpB;AAAA,cAEC;AAAA;AAAA,UACH;AAAA,WAEJ;AAAA,SACF;AAAA;AAAA,EACF;AAEJ;AAUO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,wBAAwB;AAAA,EACxB,qBAAqB;AAAA,EACrB,0BAA0B;AAAA,EAC1B,mBAAmB;AACrB,GAAqB;AACnB,QAAM,SAA4B;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,MAAI,iBAAiB,UAAW,QAAO,oBAAC,uBAAqB,GAAG,QAAQ;AACxE,MAAI,iBAAiB,OAAQ,QAAO,oBAAC,oBAAkB,GAAG,QAAQ;AAClE,SAAO,oBAAC,uBAAqB,GAAG,QAAQ;AAC1C;AAEA,IAAO,uBAAQ;","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":"
|
|
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:
|
|
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
|
|
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":"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","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:
|
|
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
|
|
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: [
|