pixelize-design-library 2.4.2-beta.57 → 2.4.2-beta.58
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/CHANGELOG.md +11 -0
- package/DESIGN-GUIDE.md +1 -1
- package/dist/Components/Commerce/commerceTypes.d.ts +5 -0
- package/dist/Components/MediaGallery/MediaGallery.js +1 -1
- package/dist/Components/ProductCard/ProductCard.js +8 -3
- package/dist/Components/ProductCard/ProductCardMedia.js +3 -3
- package/dist/Components/ProductCard/QuickAddSheet.js +2 -2
- package/dist/Components/ProductCard/cardMetrics.d.ts +1 -0
- package/dist/Components/ProductCard/cardMetrics.js +3 -2
- package/dist/Components/ProductDetails/ProductDetails.d.ts +1 -1
- package/dist/Components/ProductDetails/ProductDetails.js +6 -5
- package/dist/Components/ProductDetails/ProductDetailsProps.d.ts +2 -0
- package/dist/Hooks/useMatchMedia.d.ts +2 -0
- package/dist/Hooks/useMatchMedia.js +36 -0
- package/dist/Theme/chakra/motion.js +2 -16
- package/dist/Utils/price.js +11 -6
- package/dist/esm/Components/MediaGallery/MediaGallery.js +1 -1
- package/dist/esm/Components/ProductCard/ProductCard.js +9 -4
- package/dist/esm/Components/ProductCard/ProductCardMedia.js +3 -3
- package/dist/esm/Components/ProductCard/QuickAddSheet.js +2 -2
- package/dist/esm/Components/ProductCard/cardMetrics.js +2 -1
- package/dist/esm/Components/ProductDetails/ProductDetails.js +6 -5
- package/dist/esm/Hooks/useMatchMedia.js +32 -0
- package/dist/esm/Theme/chakra/motion.js +2 -16
- package/dist/esm/Utils/price.js +11 -6
- package/package.json +1 -1
- package/dist/Components/MediaGallery/useMatchMedia.d.ts +0 -2
- package/dist/Components/MediaGallery/useMatchMedia.js +0 -20
- package/dist/Components/ProductCard/useMinWidth.d.ts +0 -2
- package/dist/Components/ProductCard/useMinWidth.js +0 -20
- package/dist/esm/Components/MediaGallery/useMatchMedia.js +0 -16
- package/dist/esm/Components/ProductCard/useMinWidth.js +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -32,6 +32,10 @@ next major.
|
|
|
32
32
|
`discountAmount`, `unitPrice`. A **Storefront** page example in the playground wires them into a
|
|
33
33
|
working shop app (home, listing, product, wishlist, bag, checkout, order tracking).
|
|
34
34
|
- **`star` palette token.** Rating-star gold, derived (`buildStar`) and identical in every brand.
|
|
35
|
+
- **`ProductDetails` — `size`.** `md` | `lg` height of add-to-bag, buy-now and quantity controls;
|
|
36
|
+
defaults to `md` on the split layout, `lg` on the phone page. The desktop wishlist follows it.
|
|
37
|
+
- **`ProductMedia` — `srcSet` / `sizes`.** Forwarded to the `ProductCard` `<img>` so a grid tile
|
|
38
|
+
downloads a tile-sized rendition. `thumb` now has JSDoc.
|
|
35
39
|
|
|
36
40
|
- **`Spinner` — `testId`.** Maps to `data-testid` on the rendered node, wins over a raw
|
|
37
41
|
`data-testid` passthrough. Lets consumers select a loading spinner in tests.
|
|
@@ -121,6 +125,13 @@ next major.
|
|
|
121
125
|
|
|
122
126
|
### Fixed
|
|
123
127
|
|
|
128
|
+
- **Commerce performance (consumer report on 2.4.2-beta.57).** `ProductCard` no longer loads the
|
|
129
|
+
hover image on devices that cannot hover; `QuickAddSheet` mounts on first open, not on first
|
|
130
|
+
paint; `formatPrice` caches its `Intl.NumberFormat`; every `matchMedia` consumer
|
|
131
|
+
(`usePrefersReducedMotion` and the layout hooks) shares one listener per query through a single
|
|
132
|
+
`useMatchMedia`, replacing two duplicate hooks. `ProductDetails` desktop wishlist no longer sits a
|
|
133
|
+
size step above the buy buttons.
|
|
134
|
+
|
|
124
135
|
- **`SingleDatePicker` / `RangeDatePicker` / `TimeOnlyPicker` popovers clipped inside a scrolling or
|
|
125
136
|
`overflow: hidden` modal.** They now portal to `document.body` (via Chakra's own `Portal`,
|
|
126
137
|
matching `Select`), so the calendar is no longer cut off by an ancestor's clipping or scroll
|
package/DESIGN-GUIDE.md
CHANGED
|
@@ -61,7 +61,7 @@ stay light in dark mode.
|
|
|
61
61
|
| --- | --- |
|
|
62
62
|
| Page canvas | `colors.backgroundColor.base` |
|
|
63
63
|
| Card / panel surface | `colors.background[50]` |
|
|
64
|
-
| Your own overlay (popover, panel, `ModalContent bg=`) | `colors.surfaceRaised` — divider inside it `raisedRule(colors)` |
|
|
64
|
+
| Your own overlay (popover, panel, `ModalContent bg=`) | `colors.surfaceRaised` — divider inside it `raisedRule(colors)`; `Popover*` and `Menu*` come from the package root (Primitives barrel) |
|
|
65
65
|
| Subtle inset (toolbar strip, hovered row, code) | `colors.backgroundColor.secondary` |
|
|
66
66
|
| Border / divider | `colors.boxborder[200]` |
|
|
67
67
|
| Heading ink | `colors.header[900]` |
|
|
@@ -2,7 +2,12 @@ export type ProductMedia = {
|
|
|
2
2
|
id: string;
|
|
3
3
|
src: string;
|
|
4
4
|
alt?: string;
|
|
5
|
+
/** Small rendition (about 150px); used by the quick-add sheet, cart lines and gallery thumbnails. */
|
|
5
6
|
thumb?: string;
|
|
7
|
+
/** Responsive candidates for `src`, e.g. "a-400.jpg 400w, a-800.jpg 800w"; the product card forwards it to its `<img>`. */
|
|
8
|
+
srcSet?: string;
|
|
9
|
+
/** Rendered width hint paired with `srcSet`, e.g. "(min-width: 768px) 25vw, 50vw". */
|
|
10
|
+
sizes?: string;
|
|
6
11
|
type?: "image" | "video";
|
|
7
12
|
poster?: string;
|
|
8
13
|
};
|
|
@@ -19,7 +19,7 @@ const MediaLightbox_1 = __importDefault(require("./MediaLightbox"));
|
|
|
19
19
|
const glass_1 = require("./glass");
|
|
20
20
|
const motion_3 = require("./motion");
|
|
21
21
|
const useContainerWidth_1 = require("../Commerce/useContainerWidth");
|
|
22
|
-
const useMatchMedia_1 = require("
|
|
22
|
+
const useMatchMedia_1 = require("../../Hooks/useMatchMedia");
|
|
23
23
|
const mediaGalleryModel_1 = require("./mediaGalleryModel");
|
|
24
24
|
const MediaGalleryProps_1 = require("./MediaGalleryProps");
|
|
25
25
|
const WIDE_QUERY = "(min-width: 768px)";
|
|
@@ -18,6 +18,7 @@ const StockStatus_1 = __importDefault(require("../StockStatus/StockStatus"));
|
|
|
18
18
|
const WishlistToggle_1 = __importDefault(require("../WishlistToggle/WishlistToggle"));
|
|
19
19
|
const optionInk_1 = require("../OptionChip/optionInk");
|
|
20
20
|
const cardLabels_1 = require("./cardLabels");
|
|
21
|
+
const useMatchMedia_1 = require("../../Hooks/useMatchMedia");
|
|
21
22
|
const cardMetrics_1 = require("./cardMetrics");
|
|
22
23
|
const productCardModel_1 = require("./productCardModel");
|
|
23
24
|
const ProductCardAction_1 = __importDefault(require("./ProductCardAction"));
|
|
@@ -35,6 +36,7 @@ const ProductCard = (props) => {
|
|
|
35
36
|
const reduced = (0, motion_2.usePrefersReducedMotion)();
|
|
36
37
|
const titleId = (0, react_1.useId)();
|
|
37
38
|
const [sheetOpen, setSheetOpen] = (0, react_1.useState)(false);
|
|
39
|
+
const [sheetMounted, setSheetMounted] = (0, react_1.useState)(false);
|
|
38
40
|
const [picked, setPicked] = (0, react_1.useState)(null);
|
|
39
41
|
const [previewed, setPreviewed] = (0, react_1.useState)(null);
|
|
40
42
|
const [innerQuantity, setInnerQuantity] = (0, react_1.useState)(0);
|
|
@@ -71,8 +73,10 @@ const ProductCard = (props) => {
|
|
|
71
73
|
const press = () => {
|
|
72
74
|
if (action === "notify")
|
|
73
75
|
onNotify === null || onNotify === void 0 ? void 0 : onNotify();
|
|
74
|
-
else if (action === "options")
|
|
76
|
+
else if (action === "options") {
|
|
77
|
+
setSheetMounted(true);
|
|
75
78
|
setSheetOpen(true);
|
|
79
|
+
}
|
|
76
80
|
else
|
|
77
81
|
add();
|
|
78
82
|
};
|
|
@@ -89,7 +93,8 @@ const ProductCard = (props) => {
|
|
|
89
93
|
const slotted = horizontal ? ((0, jsx_runtime_1.jsx)(react_2.Box, { "data-control": true, position: "relative", zIndex: 2, children: actions })) : ((0, jsx_runtime_1.jsx)(react_2.Box, { "data-control": true, position: "absolute", right: m.actionInset, bottom: m.actionInset, zIndex: 2, children: actions }));
|
|
90
94
|
const control = actions ? slotted : defaultControl;
|
|
91
95
|
const wishlist = ((0, jsx_runtime_1.jsx)(WishlistToggle_1.default, { variant: horizontal ? "soft" : "glass", size: "md", label: copy.save(title), selected: wishlisted, onSelectedChange: onWishlistChange }));
|
|
92
|
-
const
|
|
96
|
+
const canHover = (0, useMatchMedia_1.useMatchMedia)(cardMetrics_1.FINE_POINTER_QUERY, true);
|
|
97
|
+
const hoverFrame = previewed === null && canHover ? frames[1] : undefined;
|
|
93
98
|
const pressable = interactive && !reduced;
|
|
94
99
|
const ink = (0, optionInk_1.optionInk)(colors).selected;
|
|
95
100
|
const metaLine = meta !== null && meta !== void 0 ? meta : (left !== undefined ? (0, jsx_runtime_1.jsx)(StockStatus_1.default, { status: "low-stock", count: left, size: "sm" }) : null);
|
|
@@ -110,7 +115,7 @@ const ProductCard = (props) => {
|
|
|
110
115
|
outline: "none",
|
|
111
116
|
"&::after": { content: '""', position: "absolute", inset: 0, zIndex: 1, borderRadius: cardMetrics_1.MEDIA_RADIUS },
|
|
112
117
|
"&:focus-visible::after": (0, focusRing_styles_1.focusVisibleRing)(colors),
|
|
113
|
-
}, children: title })) : (title) }), selected ? (0, jsx_runtime_1.jsx)(react_2.VisuallyHidden, { children: copy.selected }) : null, rating !== undefined && rating > 0 ? ((0, jsx_runtime_1.jsx)(Rating_1.default, { value: rating, count: ratingCount, size: "xs", compactCount: true })) : horizontal ? null : ((0, jsx_runtime_1.jsx)(react_2.Box, { minH: "1rem", "aria-hidden": true })), (0, jsx_runtime_1.jsx)(PriceBlock_1.default, { size: m.price, price: price, mrp: soldOut ? undefined : mrp, priceRange: range, unitPrice: unitPrice, currency: currency, locale: locale }), soldOut ? (0, jsx_runtime_1.jsx)(StockStatus_1.default, { status: "out-of-stock", message: copy.soldOut, size: "sm" }) : null, metaLine, swatchAxis && activeId ? ((0, jsx_runtime_1.jsx)(ProductCardSwatches_1.default, { axis: swatchAxis, limit: swatchLimit, dot: m.dot, activeId: activeId, soldOutIds: soldOutValues, labels: copy, onPreview: setPreviewed, onPick: setPicked })) : null, horizontal ? ((0, jsx_runtime_1.jsxs)(react_2.Flex, { "data-control": true, position: "relative", zIndex: 2, mt: "auto", pt: 2, align: "center", justify: "space-between", gap: 1, children: [wishlist, control] })) : null] }), action === "options" ? ((0, jsx_runtime_1.jsx)(QuickAddSheet_1.default, { isOpen: sheetOpen, onClose: () => setSheetOpen(false), title: title, brand: brand, media: frames[0], axes: axes, variants: variants, defaultSelection: swatchAxis && picked ? { [swatchAxis.id]: picked } : undefined, currency: currency, locale: locale, presentation: quickAddPresentation, drawerContentProps: quickAddDrawerProps, onConfirm: (variant, count) => {
|
|
118
|
+
}, children: title })) : (title) }), selected ? (0, jsx_runtime_1.jsx)(react_2.VisuallyHidden, { children: copy.selected }) : null, rating !== undefined && rating > 0 ? ((0, jsx_runtime_1.jsx)(Rating_1.default, { value: rating, count: ratingCount, size: "xs", compactCount: true })) : horizontal ? null : ((0, jsx_runtime_1.jsx)(react_2.Box, { minH: "1rem", "aria-hidden": true })), (0, jsx_runtime_1.jsx)(PriceBlock_1.default, { size: m.price, price: price, mrp: soldOut ? undefined : mrp, priceRange: range, unitPrice: unitPrice, currency: currency, locale: locale }), soldOut ? (0, jsx_runtime_1.jsx)(StockStatus_1.default, { status: "out-of-stock", message: copy.soldOut, size: "sm" }) : null, metaLine, swatchAxis && activeId ? ((0, jsx_runtime_1.jsx)(ProductCardSwatches_1.default, { axis: swatchAxis, limit: swatchLimit, dot: m.dot, activeId: activeId, soldOutIds: soldOutValues, labels: copy, onPreview: setPreviewed, onPick: setPicked })) : null, horizontal ? ((0, jsx_runtime_1.jsxs)(react_2.Flex, { "data-control": true, position: "relative", zIndex: 2, mt: "auto", pt: 2, align: "center", justify: "space-between", gap: 1, children: [wishlist, control] })) : null] }), action === "options" && sheetMounted ? ((0, jsx_runtime_1.jsx)(QuickAddSheet_1.default, { isOpen: sheetOpen, onClose: () => setSheetOpen(false), title: title, brand: brand, media: frames[0], axes: axes, variants: variants, defaultSelection: swatchAxis && picked ? { [swatchAxis.id]: picked } : undefined, currency: currency, locale: locale, presentation: quickAddPresentation, drawerContentProps: quickAddDrawerProps, onConfirm: (variant, count) => {
|
|
114
119
|
onAdd === null || onAdd === void 0 ? void 0 : onAdd(variant.id, count);
|
|
115
120
|
setSheetOpen(false);
|
|
116
121
|
} })) : null] }));
|
|
@@ -9,7 +9,7 @@ const motion_1 = require("../../Theme/tokens/primitives/motion");
|
|
|
9
9
|
const motion_2 = require("../../Theme/chakra/motion");
|
|
10
10
|
const cardMetrics_1 = require("./cardMetrics");
|
|
11
11
|
const DIMMED_OPACITY = 0.6;
|
|
12
|
-
const Frame = ({ src, alt, fallbackText, quiet = false }) => {
|
|
12
|
+
const Frame = ({ src, srcSet, sizes, alt, fallbackText, quiet = false }) => {
|
|
13
13
|
const { colors } = (0, useCustomTheme_1.useCustomTheme)();
|
|
14
14
|
const ref = (0, react_1.useRef)(null);
|
|
15
15
|
const [loaded, setLoaded] = (0, react_1.useState)(false);
|
|
@@ -24,12 +24,12 @@ const Frame = ({ src, alt, fallbackText, quiet = false }) => {
|
|
|
24
24
|
return null;
|
|
25
25
|
return ((0, jsx_runtime_1.jsxs)(react_2.Box, { role: "img", "aria-label": alt, position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 1.5, color: colors.textMuted, bg: colors.background[100], children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ImageOff, { size: 24, "aria-hidden": true }), fallbackText ? ((0, jsx_runtime_1.jsx)(react_2.Text, { as: "span", fontSize: "xs", fontWeight: 500, children: fallbackText })) : null] }));
|
|
26
26
|
}
|
|
27
|
-
return ((0, jsx_runtime_1.jsx)(react_2.Box, { as: "img", ref: ref, src: src, alt: alt, loading: "lazy", decoding: "async", draggable: false, onLoad: () => setLoaded(true), onError: () => setFailed(true), position: "absolute", inset: 0, w: "100%", h: "100%", objectFit: "cover", display: "block", opacity: loaded ? 1 : 0, transition: (0, motion_1.transitionFor)(["opacity"]), sx: (0, motion_2.reducedMotionSx)() }));
|
|
27
|
+
return ((0, jsx_runtime_1.jsx)(react_2.Box, { as: "img", ref: ref, src: src, srcSet: srcSet, sizes: sizes, alt: alt, loading: "lazy", decoding: "async", draggable: false, onLoad: () => setLoaded(true), onError: () => setFailed(true), position: "absolute", inset: 0, w: "100%", h: "100%", objectFit: "cover", display: "block", opacity: loaded ? 1 : 0, transition: (0, motion_1.transitionFor)(["opacity"]), sx: (0, motion_2.reducedMotionSx)() }));
|
|
28
28
|
};
|
|
29
29
|
/** Tonal media well: cover-fitted primary frame, optional hover frame, broken-image tile; overlays are children. */
|
|
30
30
|
const ProductCardMedia = ({ primary, hover, title, alt, aspectRatio, dimmed, selected, fallbackText, width, children, }) => {
|
|
31
31
|
var _a, _b;
|
|
32
32
|
const { colors } = (0, useCustomTheme_1.useCustomTheme)();
|
|
33
|
-
return ((0, jsx_runtime_1.jsxs)(react_2.Box, { "data-media": true, "data-ratio": aspectRatio, position: "relative", flexShrink: 0, w: width !== null && width !== void 0 ? width : "100%", sx: { aspectRatio: cardMetrics_1.CSS_RATIO[aspectRatio] }, borderRadius: cardMetrics_1.MEDIA_RADIUS, overflow: "hidden", bg: colors.background[100], boxShadow: selected ? `0 0 0 2px ${colors.header[900]}` : "none", transition: (0, motion_1.transitionFor)(["box-shadow"]), children: [(0, jsx_runtime_1.jsx)(react_2.Box, { "data-primary": true, position: "absolute", inset: 0, opacity: dimmed ? DIMMED_OPACITY : 1, transition: (0, motion_1.transitionFor)(["transform", "opacity"], "base"), sx: (0, motion_2.reducedMotionSx)(), children: primary ? ((0, jsx_runtime_1.jsx)(Frame, { src: primary.src, alt: (_b = (_a = primary.alt) !== null && _a !== void 0 ? _a : alt) !== null && _b !== void 0 ? _b : title, fallbackText: fallbackText }, primary.src)) : ((0, jsx_runtime_1.jsx)(Frame, { src: "", alt: alt !== null && alt !== void 0 ? alt : title, fallbackText: fallbackText }, "none")) }), hover ? ((0, jsx_runtime_1.jsx)(react_2.Box, { "data-hover-frame": true, "aria-hidden": true, position: "absolute", inset: 0, opacity: 0, pointerEvents: "none", transition: (0, motion_1.transitionFor)(["opacity"], "base"), sx: (0, motion_2.reducedMotionSx)(), children: (0, jsx_runtime_1.jsx)(Frame, { src: hover.src, alt: "", quiet: true }, hover.src) })) : null, children] }));
|
|
33
|
+
return ((0, jsx_runtime_1.jsxs)(react_2.Box, { "data-media": true, "data-ratio": aspectRatio, position: "relative", flexShrink: 0, w: width !== null && width !== void 0 ? width : "100%", sx: { aspectRatio: cardMetrics_1.CSS_RATIO[aspectRatio] }, borderRadius: cardMetrics_1.MEDIA_RADIUS, overflow: "hidden", bg: colors.background[100], boxShadow: selected ? `0 0 0 2px ${colors.header[900]}` : "none", transition: (0, motion_1.transitionFor)(["box-shadow"]), children: [(0, jsx_runtime_1.jsx)(react_2.Box, { "data-primary": true, position: "absolute", inset: 0, opacity: dimmed ? DIMMED_OPACITY : 1, transition: (0, motion_1.transitionFor)(["transform", "opacity"], "base"), sx: (0, motion_2.reducedMotionSx)(), children: primary ? ((0, jsx_runtime_1.jsx)(Frame, { src: primary.src, srcSet: primary.srcSet, sizes: primary.sizes, alt: (_b = (_a = primary.alt) !== null && _a !== void 0 ? _a : alt) !== null && _b !== void 0 ? _b : title, fallbackText: fallbackText }, primary.src)) : ((0, jsx_runtime_1.jsx)(Frame, { src: "", alt: alt !== null && alt !== void 0 ? alt : title, fallbackText: fallbackText }, "none")) }), hover ? ((0, jsx_runtime_1.jsx)(react_2.Box, { "data-hover-frame": true, "aria-hidden": true, position: "absolute", inset: 0, opacity: 0, pointerEvents: "none", transition: (0, motion_1.transitionFor)(["opacity"], "base"), sx: (0, motion_2.reducedMotionSx)(), children: (0, jsx_runtime_1.jsx)(Frame, { src: hover.src, srcSet: hover.srcSet, sizes: hover.sizes, alt: "", quiet: true }, hover.src) })) : null, children] }));
|
|
34
34
|
};
|
|
35
35
|
exports.default = ProductCardMedia;
|
|
@@ -49,7 +49,7 @@ const QuantityStepper_1 = __importDefault(require("../QuantityStepper/QuantitySt
|
|
|
49
49
|
const VariantSelector_1 = __importDefault(require("../VariantSelector/VariantSelector"));
|
|
50
50
|
const commerceVariants_1 = require("../Commerce/commerceVariants");
|
|
51
51
|
const productCardModel_1 = require("./productCardModel");
|
|
52
|
-
const
|
|
52
|
+
const useMatchMedia_1 = require("../../Hooks/useMatchMedia");
|
|
53
53
|
const DEFAULT_LABELS = {
|
|
54
54
|
heading: "Choose options",
|
|
55
55
|
quantity: "Quantity",
|
|
@@ -62,7 +62,7 @@ const DESKTOP_QUERY = "(min-width: 48em)";
|
|
|
62
62
|
const QuickAddSheet = ({ isOpen, onClose, title, brand, media, axes, variants, defaultSelection, onConfirm, presentation = "auto", drawerContentProps, labels, currency, locale, "data-testid": testId, }) => {
|
|
63
63
|
var _a, _b, _c;
|
|
64
64
|
const { colors } = (0, useCustomTheme_1.useCustomTheme)();
|
|
65
|
-
const desktop = (0,
|
|
65
|
+
const desktop = (0, useMatchMedia_1.useMatchMedia)(DESKTOP_QUERY);
|
|
66
66
|
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
67
67
|
const [selection, setSelection] = (0, react_1.useState)(defaultSelection !== null && defaultSelection !== void 0 ? defaultSelection : {});
|
|
68
68
|
const [quantity, setQuantity] = (0, react_1.useState)(1);
|
|
@@ -24,4 +24,5 @@ export declare const DENSITY: Record<ProductCardDensity, DensityMetrics>;
|
|
|
24
24
|
export declare const CSS_RATIO: Record<ProductCardAspectRatio, string>;
|
|
25
25
|
export declare const MEDIA_RADIUS = "1rem";
|
|
26
26
|
export declare const TITLE_LINES = 2;
|
|
27
|
+
export declare const FINE_POINTER_QUERY = "(hover: hover) and (pointer: fine)";
|
|
27
28
|
export declare const FINE_POINTER = "@media (hover: hover) and (pointer: fine)";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FINE_POINTER = exports.TITLE_LINES = exports.MEDIA_RADIUS = exports.CSS_RATIO = exports.DENSITY = void 0;
|
|
3
|
+
exports.FINE_POINTER = exports.FINE_POINTER_QUERY = exports.TITLE_LINES = exports.MEDIA_RADIUS = exports.CSS_RATIO = exports.DENSITY = void 0;
|
|
4
4
|
exports.DENSITY = {
|
|
5
5
|
compact: {
|
|
6
6
|
badgeInset: "0.5rem",
|
|
@@ -52,4 +52,5 @@ exports.CSS_RATIO = {
|
|
|
52
52
|
};
|
|
53
53
|
exports.MEDIA_RADIUS = "1rem";
|
|
54
54
|
exports.TITLE_LINES = 2;
|
|
55
|
-
exports.
|
|
55
|
+
exports.FINE_POINTER_QUERY = "(hover: hover) and (pointer: fine)";
|
|
56
|
+
exports.FINE_POINTER = `@media ${exports.FINE_POINTER_QUERY}`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ProductDetailsProps } from "./ProductDetailsProps";
|
|
2
2
|
/** Product page that wires one selection to price, stock, gallery frames and the primary action; every region has a slot. */
|
|
3
|
-
declare const ProductDetails: ({ product, selection: selectionProp, defaultSelection, onSelectionChange, quantity: quantityProp, defaultQuantity, onQuantityChange, maxQuantity, onAddToBag, onBuyNow, onNotify, onGoToBag, addedDuration, wishlisted, defaultWishlisted, onWishlistChange, onShare, offers, offersProps, trustItems, pincode, sizeGuide, onGuideClick, reviews, ratingBreakdown, reviewsProps, onWriteReview, detailSectionsProps, care, breadcrumbs, layout, mediaSlot, metaSlot, infoSlot, belowFold, relatedSlot, isLoading, labels: labelOverrides, currency, locale, "data-testid": testId, }: ProductDetailsProps) => import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
declare const ProductDetails: ({ product, selection: selectionProp, defaultSelection, onSelectionChange, quantity: quantityProp, defaultQuantity, onQuantityChange, maxQuantity, onAddToBag, onBuyNow, onNotify, onGoToBag, addedDuration, wishlisted, defaultWishlisted, onWishlistChange, onShare, offers, offersProps, trustItems, pincode, sizeGuide, onGuideClick, reviews, ratingBreakdown, reviewsProps, onWriteReview, detailSectionsProps, care, breadcrumbs, layout, size, mediaSlot, metaSlot, infoSlot, belowFold, relatedSlot, isLoading, labels: labelOverrides, currency, locale, "data-testid": testId, }: ProductDetailsProps) => import("react/jsx-runtime").JSX.Element;
|
|
4
4
|
export default ProductDetails;
|
|
@@ -16,7 +16,7 @@ const Button_1 = __importDefault(require("../Button/Button"));
|
|
|
16
16
|
const Skeletons_1 = __importDefault(require("../Skeletons/Skeletons"));
|
|
17
17
|
const MediaGallery_1 = __importDefault(require("../MediaGallery/MediaGallery"));
|
|
18
18
|
const useContainerWidth_1 = require("../Commerce/useContainerWidth");
|
|
19
|
-
const useMatchMedia_1 = require("
|
|
19
|
+
const useMatchMedia_1 = require("../../Hooks/useMatchMedia");
|
|
20
20
|
const ProductBadges_1 = __importDefault(require("../ProductBadges/ProductBadges"));
|
|
21
21
|
const Rating_1 = __importDefault(require("../Rating/Rating"));
|
|
22
22
|
const PriceBlock_1 = __importDefault(require("../PriceBlock/PriceBlock"));
|
|
@@ -64,7 +64,7 @@ const scrollToNode = (node, reduced) => {
|
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
66
|
/** Product page that wires one selection to price, stock, gallery frames and the primary action; every region has a slot. */
|
|
67
|
-
const ProductDetails = ({ product, selection: selectionProp, defaultSelection, onSelectionChange, quantity: quantityProp, defaultQuantity = 1, onQuantityChange, maxQuantity = productDetailsModel_1.DEFAULT_MAX_QUANTITY, onAddToBag, onBuyNow, onNotify, onGoToBag, addedDuration = DEFAULT_ADDED_MS, wishlisted, defaultWishlisted, onWishlistChange, onShare, offers, offersProps, trustItems, pincode, sizeGuide, onGuideClick, reviews, ratingBreakdown, reviewsProps, onWriteReview, detailSectionsProps, care, breadcrumbs, layout = "auto", mediaSlot, metaSlot, infoSlot, belowFold, relatedSlot, isLoading = false, labels: labelOverrides, currency, locale, "data-testid": testId, }) => {
|
|
67
|
+
const ProductDetails = ({ product, selection: selectionProp, defaultSelection, onSelectionChange, quantity: quantityProp, defaultQuantity = 1, onQuantityChange, maxQuantity = productDetailsModel_1.DEFAULT_MAX_QUANTITY, onAddToBag, onBuyNow, onNotify, onGoToBag, addedDuration = DEFAULT_ADDED_MS, wishlisted, defaultWishlisted, onWishlistChange, onShare, offers, offersProps, trustItems, pincode, sizeGuide, onGuideClick, reviews, ratingBreakdown, reviewsProps, onWriteReview, detailSectionsProps, care, breadcrumbs, layout = "auto", size, mediaSlot, metaSlot, infoSlot, belowFold, relatedSlot, isLoading = false, labels: labelOverrides, currency, locale, "data-testid": testId, }) => {
|
|
68
68
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
69
69
|
const { colors } = (0, useCustomTheme_1.useCustomTheme)();
|
|
70
70
|
const reduced = (0, motion_2.usePrefersReducedMotion)();
|
|
@@ -75,6 +75,7 @@ const ProductDetails = ({ product, selection: selectionProp, defaultSelection, o
|
|
|
75
75
|
const containerWidth = (0, useContainerWidth_1.useContainerWidth)(rootRef);
|
|
76
76
|
const wide = (0, useMatchMedia_1.useMatchMedia)(DESKTOP_QUERY, false);
|
|
77
77
|
const desktop = layout === "auto" ? (containerWidth === undefined ? wide : containerWidth >= DESKTOP_MIN_WIDTH) : layout === "desktop";
|
|
78
|
+
const controlSize = size !== null && size !== void 0 ? size : (desktop ? "md" : "lg");
|
|
78
79
|
const variants = (0, react_1.useMemo)(() => (0, productDetailsModel_1.resolveVariants)(product), [product]);
|
|
79
80
|
const axes = (0, react_1.useMemo)(() => withGuide(product.axes, sizeGuide), [product.axes, sizeGuide]);
|
|
80
81
|
const [innerSelection, setInnerSelection] = (0, react_1.useState)(defaultSelection !== null && defaultSelection !== void 0 ? defaultSelection : {});
|
|
@@ -206,11 +207,11 @@ const ProductDetails = ({ product, selection: selectionProp, defaultSelection, o
|
|
|
206
207
|
const showReviews = reviews !== undefined || product.ratingCount !== undefined;
|
|
207
208
|
const sectionGap = desktop ? 18 : 10;
|
|
208
209
|
const gutter = desktop ? 6 : 4;
|
|
209
|
-
const wishlist = ((0, jsx_runtime_1.jsx)(WishlistToggle_1.default, { selected: wishlisted, defaultSelected: defaultWishlisted, onSelectedChange: onWishlistChange, size: desktop ?
|
|
210
|
+
const wishlist = ((0, jsx_runtime_1.jsx)(WishlistToggle_1.default, { selected: wishlisted, defaultSelected: defaultWishlisted, onSelectedChange: onWishlistChange, size: desktop ? controlSize : "md", variant: desktop ? "soft" : "glass", label: labels.wishlistSave }));
|
|
210
211
|
const badgeSlot = product.badges && product.badges.length > 0 ? (0, jsx_runtime_1.jsx)(ProductBadges_1.default, { badges: product.badges, max: 2, variant: "glass", size: "md" }) : undefined;
|
|
211
212
|
const gallery = mediaSlot !== null && mediaSlot !== void 0 ? mediaSlot : ((0, jsx_runtime_1.jsx)(MediaGallery_1.default, { media: product.media, layout: desktop ? "thumbs-left" : "swipe", aspectRatio: "4/5", activeMediaIds: activeIds, sticky: desktop, stickyTop: 24, badges: badgeSlot, actions: desktop ? undefined : wishlist, isLoading: isLoading, "aria-label": product.title }));
|
|
212
|
-
const primaryButton = ((0, jsx_runtime_1.jsx)(Button_1.default, { size:
|
|
213
|
-
const buyPanel = isLoading ? ((0, jsx_runtime_1.jsxs)(react_2.Flex, { direction: "column", gap: 4, "aria-busy": "true", children: [(0, jsx_runtime_1.jsx)(Skeletons_1.default, { type: "skeleton", height: "1rem", width: "30%" }), (0, jsx_runtime_1.jsx)(Skeletons_1.default, { type: "skeleton", height: "2rem", width: "90%" }), (0, jsx_runtime_1.jsx)(Skeletons_1.default, { type: "skeleton", height: "2.5rem", width: "45%" }), (0, jsx_runtime_1.jsx)(Skeletons_1.default, { type: "skeleton", height: "3rem", width: "100%" }), (0, jsx_runtime_1.jsx)(Skeletons_1.default, { type: "skeleton", height: "3rem", width: "100%" })] })) : ((0, jsx_runtime_1.jsxs)(react_2.Flex, { direction: "column", gap: 6, children: [(0, jsx_runtime_1.jsxs)(react_2.Flex, { direction: "column", gap: 2, children: [(0, jsx_runtime_1.jsxs)(react_2.Flex, { align: "center", justify: "space-between", gap: 3, minH: product.brand || onShare ? "2.75rem" : undefined, children: [product.brand ? ((0, jsx_runtime_1.jsx)(react_2.Text, { as: "span", fontSize: "sm", fontWeight: 600, color: colors.textMuted, children: product.brand })) : ((0, jsx_runtime_1.jsx)("span", {})), onShare ? ((0, jsx_runtime_1.jsx)(react_2.Box, { as: "button", type: "button", "aria-label": labels.share, onClick: onShare, display: "inline-flex", alignItems: "center", justifyContent: "center", boxSize: "2.75rem", mr: "-0.625rem", borderRadius: "full", color: colors.header[900], transition: (0, motion_1.transitionFor)(["background-color"]), _hover: { bg: colors.background[100] }, sx: { ...(0, focusRing_styles_1.focusVisibleStyles)(colors), ...(0, motion_2.reducedMotionSx)() }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Share2, { size: 20, "aria-hidden": true }) })) : null] }), (0, jsx_runtime_1.jsx)(react_2.Text, { as: "h1", fontSize: desktop ? "1.75rem" : "1.375rem", lineHeight: 1.25, fontWeight: 700, letterSpacing: "-0.02em", color: colors.header[900], children: product.title }), product.rating !== undefined && showReviews ? ((0, jsx_runtime_1.jsx)(react_2.Box, { as: "button", type: "button", onClick: jumpToReviews, "aria-label": labels.jumpToReviews(ratingText), alignSelf: "flex-start", display: "inline-flex", alignItems: "center", minH: "2.75rem", borderRadius: "md", sx: { ...(0, focusRing_styles_1.focusVisibleStyles)(colors) }, children: (0, jsx_runtime_1.jsx)(Rating_1.default, { value: product.rating, count: product.ratingCount, size: "sm", "aria-label": ratingText, locale: locale }) })) : product.rating !== undefined ? ((0, jsx_runtime_1.jsx)(Rating_1.default, { value: product.rating, count: product.ratingCount, size: "sm", locale: locale })) : null] }), (0, jsx_runtime_1.jsxs)(react_2.Flex, { direction: "column", gap: 2, children: [(0, jsx_runtime_1.jsx)(PriceBlock_1.default, { size: "lg", price: price, mrp: mrp, priceRange: showRange ? range : undefined, showSavings: true, taxNote: labels.taxNote, unitPrice: (0, productDetailsModel_1.packUnitPrice)(product, selection, price), ...fmt }), state ? (0, jsx_runtime_1.jsx)(StockStatus_1.default, { status: state, count: variant === null || variant === void 0 ? void 0 : variant.stock }) : null, metaSlot] }), offers && offers.length > 0 ? (0, jsx_runtime_1.jsx)(OffersList_1.default, { offers: offers, ...offersProps }) : null, axes.length > 0 ? ((0, jsx_runtime_1.jsx)(react_2.Box, { ref: selectorRef, children: (0, jsx_runtime_1.jsx)(VariantSelector_1.default, { axes: axes, variants: variants, value: selection, onValueChange: handleSelection, error: showSelectError && !complete ? selectPrompt : undefined, showSelectedLabel: true, showLowStock: true, onGuideClick: onGuideClick }) })) : null, (0, jsx_runtime_1.jsxs)(react_2.Flex, { direction: "column", gap: 3, ref: ctaRef, children: [(0, jsx_runtime_1.jsxs)(react_2.Flex, { align: "center", gap: 3, children: [showStepper ? ((0, jsx_runtime_1.jsx)(QuantityStepper_1.default, { size:
|
|
213
|
+
const primaryButton = ((0, jsx_runtime_1.jsx)(Button_1.default, { size: controlSize, width: "100%", variant: notifyMode ? "outline" : "solid", label: primaryLabel, leftIcon: bag === "added" ? (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 18, "aria-hidden": true }) : notifyMode ? (0, jsx_runtime_1.jsx)(lucide_react_1.BellRing, { size: 18, "aria-hidden": true }) : undefined, isLoading: bag === "adding", onClick: notifyMode ? handleNotify : handleAdd, fontWeight: 600 }));
|
|
214
|
+
const buyPanel = isLoading ? ((0, jsx_runtime_1.jsxs)(react_2.Flex, { direction: "column", gap: 4, "aria-busy": "true", children: [(0, jsx_runtime_1.jsx)(Skeletons_1.default, { type: "skeleton", height: "1rem", width: "30%" }), (0, jsx_runtime_1.jsx)(Skeletons_1.default, { type: "skeleton", height: "2rem", width: "90%" }), (0, jsx_runtime_1.jsx)(Skeletons_1.default, { type: "skeleton", height: "2.5rem", width: "45%" }), (0, jsx_runtime_1.jsx)(Skeletons_1.default, { type: "skeleton", height: "3rem", width: "100%" }), (0, jsx_runtime_1.jsx)(Skeletons_1.default, { type: "skeleton", height: "3rem", width: "100%" })] })) : ((0, jsx_runtime_1.jsxs)(react_2.Flex, { direction: "column", gap: 6, children: [(0, jsx_runtime_1.jsxs)(react_2.Flex, { direction: "column", gap: 2, children: [(0, jsx_runtime_1.jsxs)(react_2.Flex, { align: "center", justify: "space-between", gap: 3, minH: product.brand || onShare ? "2.75rem" : undefined, children: [product.brand ? ((0, jsx_runtime_1.jsx)(react_2.Text, { as: "span", fontSize: "sm", fontWeight: 600, color: colors.textMuted, children: product.brand })) : ((0, jsx_runtime_1.jsx)("span", {})), onShare ? ((0, jsx_runtime_1.jsx)(react_2.Box, { as: "button", type: "button", "aria-label": labels.share, onClick: onShare, display: "inline-flex", alignItems: "center", justifyContent: "center", boxSize: "2.75rem", mr: "-0.625rem", borderRadius: "full", color: colors.header[900], transition: (0, motion_1.transitionFor)(["background-color"]), _hover: { bg: colors.background[100] }, sx: { ...(0, focusRing_styles_1.focusVisibleStyles)(colors), ...(0, motion_2.reducedMotionSx)() }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Share2, { size: 20, "aria-hidden": true }) })) : null] }), (0, jsx_runtime_1.jsx)(react_2.Text, { as: "h1", fontSize: desktop ? "1.75rem" : "1.375rem", lineHeight: 1.25, fontWeight: 700, letterSpacing: "-0.02em", color: colors.header[900], children: product.title }), product.rating !== undefined && showReviews ? ((0, jsx_runtime_1.jsx)(react_2.Box, { as: "button", type: "button", onClick: jumpToReviews, "aria-label": labels.jumpToReviews(ratingText), alignSelf: "flex-start", display: "inline-flex", alignItems: "center", minH: "2.75rem", borderRadius: "md", sx: { ...(0, focusRing_styles_1.focusVisibleStyles)(colors) }, children: (0, jsx_runtime_1.jsx)(Rating_1.default, { value: product.rating, count: product.ratingCount, size: "sm", "aria-label": ratingText, locale: locale }) })) : product.rating !== undefined ? ((0, jsx_runtime_1.jsx)(Rating_1.default, { value: product.rating, count: product.ratingCount, size: "sm", locale: locale })) : null] }), (0, jsx_runtime_1.jsxs)(react_2.Flex, { direction: "column", gap: 2, children: [(0, jsx_runtime_1.jsx)(PriceBlock_1.default, { size: "lg", price: price, mrp: mrp, priceRange: showRange ? range : undefined, showSavings: true, taxNote: labels.taxNote, unitPrice: (0, productDetailsModel_1.packUnitPrice)(product, selection, price), ...fmt }), state ? (0, jsx_runtime_1.jsx)(StockStatus_1.default, { status: state, count: variant === null || variant === void 0 ? void 0 : variant.stock }) : null, metaSlot] }), offers && offers.length > 0 ? (0, jsx_runtime_1.jsx)(OffersList_1.default, { offers: offers, ...offersProps }) : null, axes.length > 0 ? ((0, jsx_runtime_1.jsx)(react_2.Box, { ref: selectorRef, children: (0, jsx_runtime_1.jsx)(VariantSelector_1.default, { axes: axes, variants: variants, value: selection, onValueChange: handleSelection, error: showSelectError && !complete ? selectPrompt : undefined, showSelectedLabel: true, showLowStock: true, onGuideClick: onGuideClick }) })) : null, (0, jsx_runtime_1.jsxs)(react_2.Flex, { direction: "column", gap: 3, ref: ctaRef, children: [(0, jsx_runtime_1.jsxs)(react_2.Flex, { align: "center", gap: 3, children: [showStepper ? ((0, jsx_runtime_1.jsx)(QuantityStepper_1.default, { size: controlSize, value: quantity, onValueChange: handleQuantity, min: 1, max: ceiling, itemLabel: product.title })) : null, (0, jsx_runtime_1.jsx)(react_2.Box, { flex: 1, minW: 0, children: primaryButton }), desktop ? wishlist : null] }), showBuyNow ? (0, jsx_runtime_1.jsx)(Button_1.default, { size: controlSize, width: "100%", variant: "outline", label: labels.buyNow, onClick: handleBuyNow }) : null, addFailed ? ((0, jsx_runtime_1.jsxs)(react_2.Flex, { role: "alert", align: "center", gap: 1.5, color: colors.semanticText.error, fontSize: "sm", fontWeight: 600, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleAlert, { size: 14, "aria-hidden": true }), (0, jsx_runtime_1.jsx)(react_2.Text, { as: "span", children: labels.addFailed })] })) : null] }), pincode ? (0, jsx_runtime_1.jsx)(PincodeChecker_1.default, { ...pincode }) : null, trustItems && trustItems.length > 0 ? (0, jsx_runtime_1.jsx)(TrustBadges_1.default, { items: trustItems, layout: desktop ? "grid" : "row" }) : null, infoSlot] }));
|
|
214
215
|
return ((0, jsx_runtime_1.jsxs)(react_2.Box, { ref: rootRef, "data-testid": testId, "data-layout": desktop ? "desktop" : "mobile", w: "100%", maxW: "82rem", mx: "auto", px: desktop ? 6 : 0, children: [(0, jsx_runtime_1.jsx)(react_2.VisuallyHidden, { role: "status", "aria-live": "polite", "aria-atomic": "true", children: announcement }), breadcrumbs ? (0, jsx_runtime_1.jsx)(react_2.Box, { px: desktop ? 0 : gutter, py: 3, children: breadcrumbs }) : null, (0, jsx_runtime_1.jsxs)(react_2.Flex, { direction: desktop ? "row" : "column", align: desktop ? "flex-start" : "stretch", gap: desktop ? 12 : 5, children: [(0, jsx_runtime_1.jsx)(react_2.Box, { flex: desktop ? "1.63 1 0" : undefined, minW: 0, w: desktop ? undefined : "100%", children: gallery }), (0, jsx_runtime_1.jsx)(react_2.Box, { flex: desktop ? "1 1 0" : undefined, minW: 0, px: desktop ? 0 : gutter, children: buyPanel })] }), !isLoading ? ((0, jsx_runtime_1.jsxs)(react_2.Flex, { direction: "column", gap: sectionGap, mt: sectionGap, px: desktop ? 0 : gutter, children: [(0, jsx_runtime_1.jsx)(DetailSections_1.default, { description: product.description, highlights: product.highlights, specs: product.specs, care: care, shipping: product.returnsNote, legal: hasLegal
|
|
215
216
|
? {
|
|
216
217
|
netQuantity: product.netQuantity,
|
|
@@ -126,6 +126,8 @@ export interface ProductDetailsProps extends PriceFormat {
|
|
|
126
126
|
breadcrumbs?: ReactNode;
|
|
127
127
|
/** `auto` splits into gallery and buy panel from 960px of the component's own width, not the window. */
|
|
128
128
|
layout?: ProductDetailsLayout;
|
|
129
|
+
/** Height of the add-to-bag, buy-now and quantity controls; defaults to `md` (40px) on the split layout, `lg` (48px) on the phone page. */
|
|
130
|
+
size?: "md" | "lg";
|
|
129
131
|
/** Replaces the gallery; its frames, badges and wishlist toggle go with it. */
|
|
130
132
|
mediaSlot?: ReactNode;
|
|
131
133
|
/** Extra content in the buy panel under the price, e.g. an EMI line. */
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useMatchMedia = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const entries = new Map();
|
|
6
|
+
const supported = () => typeof window !== "undefined" && typeof window.matchMedia === "function";
|
|
7
|
+
const subscriberFor = (query) => (notify) => {
|
|
8
|
+
if (!supported())
|
|
9
|
+
return () => undefined;
|
|
10
|
+
let entry = entries.get(query);
|
|
11
|
+
if (!entry) {
|
|
12
|
+
const list = window.matchMedia(query);
|
|
13
|
+
const created = { list, listeners: new Set(), onChange: () => created.listeners.forEach((fn) => fn()) };
|
|
14
|
+
list.addEventListener("change", created.onChange);
|
|
15
|
+
entries.set(query, created);
|
|
16
|
+
entry = created;
|
|
17
|
+
}
|
|
18
|
+
entry.listeners.add(notify);
|
|
19
|
+
return () => {
|
|
20
|
+
const current = entries.get(query);
|
|
21
|
+
if (!current)
|
|
22
|
+
return;
|
|
23
|
+
current.listeners.delete(notify);
|
|
24
|
+
if (current.listeners.size === 0) {
|
|
25
|
+
current.list.removeEventListener("change", current.onChange);
|
|
26
|
+
entries.delete(query);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
/** `matches` for a media query, sharing one listener per query; `fallback` where matchMedia is missing (SSR, jsdom). */
|
|
31
|
+
const useMatchMedia = (query, fallback = false) => {
|
|
32
|
+
const subscribe = (0, react_1.useMemo)(() => subscriberFor(query), [query]);
|
|
33
|
+
const read = () => { var _a, _b; return (_b = (_a = entries.get(query)) === null || _a === void 0 ? void 0 : _a.list.matches) !== null && _b !== void 0 ? _b : (supported() ? window.matchMedia(query).matches : fallback); };
|
|
34
|
+
return (0, react_1.useSyncExternalStore)(subscribe, read, () => fallback);
|
|
35
|
+
};
|
|
36
|
+
exports.useMatchMedia = useMatchMedia;
|
|
@@ -1,24 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.reducedMotionSx = exports.usePrefersReducedMotion = exports.REDUCED_MOTION_QUERY = void 0;
|
|
4
|
-
const
|
|
4
|
+
const useMatchMedia_1 = require("../../Hooks/useMatchMedia");
|
|
5
5
|
exports.REDUCED_MOTION_QUERY = "@media (prefers-reduced-motion: reduce)";
|
|
6
6
|
const MEDIA = "(prefers-reduced-motion: reduce)";
|
|
7
|
-
|
|
8
|
-
const hasMatchMedia = () => typeof window !== "undefined" && typeof window.matchMedia === "function";
|
|
9
|
-
const readPreference = () => hasMatchMedia() && window.matchMedia(MEDIA).matches;
|
|
10
|
-
const usePrefersReducedMotion = () => {
|
|
11
|
-
const [reduced, setReduced] = (0, react_1.useState)(readPreference);
|
|
12
|
-
(0, react_1.useEffect)(() => {
|
|
13
|
-
if (!hasMatchMedia())
|
|
14
|
-
return;
|
|
15
|
-
const query = window.matchMedia(MEDIA);
|
|
16
|
-
const onChange = () => setReduced(query.matches);
|
|
17
|
-
query.addEventListener("change", onChange);
|
|
18
|
-
return () => query.removeEventListener("change", onChange);
|
|
19
|
-
}, []);
|
|
20
|
-
return reduced;
|
|
21
|
-
};
|
|
7
|
+
const usePrefersReducedMotion = () => (0, useMatchMedia_1.useMatchMedia)(MEDIA, false);
|
|
22
8
|
exports.usePrefersReducedMotion = usePrefersReducedMotion;
|
|
23
9
|
/** Spread into `sx`: drops transitions and animations when the user asks for less motion. */
|
|
24
10
|
const reducedMotionSx = (overrides = {}) => ({
|
package/dist/Utils/price.js
CHANGED
|
@@ -7,15 +7,20 @@ exports.discountAmount = discountAmount;
|
|
|
7
7
|
exports.unitPrice = unitPrice;
|
|
8
8
|
exports.DEFAULT_CURRENCY = "INR";
|
|
9
9
|
exports.DEFAULT_LOCALE = "en-IN";
|
|
10
|
+
const formatters = new Map();
|
|
11
|
+
const formatterFor = (locale, currency, minimum, maximum) => {
|
|
12
|
+
const key = `${locale}|${currency}|${minimum}|${maximum}`;
|
|
13
|
+
let formatter = formatters.get(key);
|
|
14
|
+
if (!formatter) {
|
|
15
|
+
formatter = new Intl.NumberFormat(locale, { style: "currency", currency, minimumFractionDigits: minimum, maximumFractionDigits: maximum });
|
|
16
|
+
formatters.set(key, formatter);
|
|
17
|
+
}
|
|
18
|
+
return formatter;
|
|
19
|
+
};
|
|
10
20
|
function formatPrice(value, options = {}) {
|
|
11
21
|
const { currency = exports.DEFAULT_CURRENCY, locale = exports.DEFAULT_LOCALE, maximumFractionDigits } = options;
|
|
12
22
|
const whole = Number.isInteger(value);
|
|
13
|
-
return
|
|
14
|
-
style: "currency",
|
|
15
|
-
currency,
|
|
16
|
-
minimumFractionDigits: whole ? 0 : 2,
|
|
17
|
-
maximumFractionDigits: maximumFractionDigits !== null && maximumFractionDigits !== void 0 ? maximumFractionDigits : (whole ? 0 : 2),
|
|
18
|
-
}).format(value);
|
|
23
|
+
return formatterFor(locale, currency, whole ? 0 : 2, maximumFractionDigits !== null && maximumFractionDigits !== void 0 ? maximumFractionDigits : (whole ? 0 : 2)).format(value);
|
|
19
24
|
}
|
|
20
25
|
function discountPercent(price, mrp) {
|
|
21
26
|
if (mrp === undefined || mrp <= price || mrp <= 0)
|
|
@@ -14,7 +14,7 @@ import MediaLightbox from "./MediaLightbox.js";
|
|
|
14
14
|
import { GlassIconButton, GlassPill, glassStyles } from "./glass.js";
|
|
15
15
|
import { CROSSFADE_MS, fadeIn } from "./motion.js";
|
|
16
16
|
import { useContainerWidth } from "../Commerce/useContainerWidth.js";
|
|
17
|
-
import { useMatchMedia } from "
|
|
17
|
+
import { useMatchMedia } from "../../Hooks/useMatchMedia.js";
|
|
18
18
|
import { clampIndex, frameKind, frameSignature, indexForKey, keyBelongsToTarget, resolveFrames } from "./mediaGalleryModel.js";
|
|
19
19
|
import { defaultMediaGalleryLabels } from "./MediaGalleryProps.js";
|
|
20
20
|
const WIDE_QUERY = "(min-width: 768px)";
|
|
@@ -13,7 +13,8 @@ import StockStatus from "../StockStatus/StockStatus.js";
|
|
|
13
13
|
import WishlistToggle from "../WishlistToggle/WishlistToggle.js";
|
|
14
14
|
import { optionInk } from "../OptionChip/optionInk.js";
|
|
15
15
|
import { DEFAULT_LABELS } from "./cardLabels.js";
|
|
16
|
-
import {
|
|
16
|
+
import { useMatchMedia } from "../../Hooks/useMatchMedia.js";
|
|
17
|
+
import { DENSITY, FINE_POINTER, FINE_POINTER_QUERY, MEDIA_RADIUS, TITLE_LINES } from "./cardMetrics.js";
|
|
17
18
|
import { DEFAULT_SWATCH_LIMIT, deriveAction, framesFor, lowStockCount, rangeOf, soleVariantOf, swatchAxisOf, valueSoldOut, variantsFor, isSoldOut, } from "./productCardModel.js";
|
|
18
19
|
import ProductCardAction from "./ProductCardAction.js";
|
|
19
20
|
import ProductCardMedia from "./ProductCardMedia.js";
|
|
@@ -30,6 +31,7 @@ const ProductCard = (props) => {
|
|
|
30
31
|
const reduced = usePrefersReducedMotion();
|
|
31
32
|
const titleId = useId();
|
|
32
33
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
34
|
+
const [sheetMounted, setSheetMounted] = useState(false);
|
|
33
35
|
const [picked, setPicked] = useState(null);
|
|
34
36
|
const [previewed, setPreviewed] = useState(null);
|
|
35
37
|
const [innerQuantity, setInnerQuantity] = useState(0);
|
|
@@ -66,8 +68,10 @@ const ProductCard = (props) => {
|
|
|
66
68
|
const press = () => {
|
|
67
69
|
if (action === "notify")
|
|
68
70
|
onNotify === null || onNotify === void 0 ? void 0 : onNotify();
|
|
69
|
-
else if (action === "options")
|
|
71
|
+
else if (action === "options") {
|
|
72
|
+
setSheetMounted(true);
|
|
70
73
|
setSheetOpen(true);
|
|
74
|
+
}
|
|
71
75
|
else
|
|
72
76
|
add();
|
|
73
77
|
};
|
|
@@ -84,7 +88,8 @@ const ProductCard = (props) => {
|
|
|
84
88
|
const slotted = horizontal ? (_jsx(Box, { "data-control": true, position: "relative", zIndex: 2, children: actions })) : (_jsx(Box, { "data-control": true, position: "absolute", right: m.actionInset, bottom: m.actionInset, zIndex: 2, children: actions }));
|
|
85
89
|
const control = actions ? slotted : defaultControl;
|
|
86
90
|
const wishlist = (_jsx(WishlistToggle, { variant: horizontal ? "soft" : "glass", size: "md", label: copy.save(title), selected: wishlisted, onSelectedChange: onWishlistChange }));
|
|
87
|
-
const
|
|
91
|
+
const canHover = useMatchMedia(FINE_POINTER_QUERY, true);
|
|
92
|
+
const hoverFrame = previewed === null && canHover ? frames[1] : undefined;
|
|
88
93
|
const pressable = interactive && !reduced;
|
|
89
94
|
const ink = optionInk(colors).selected;
|
|
90
95
|
const metaLine = meta !== null && meta !== void 0 ? meta : (left !== undefined ? _jsx(StockStatus, { status: "low-stock", count: left, size: "sm" }) : null);
|
|
@@ -105,7 +110,7 @@ const ProductCard = (props) => {
|
|
|
105
110
|
outline: "none",
|
|
106
111
|
"&::after": { content: '""', position: "absolute", inset: 0, zIndex: 1, borderRadius: MEDIA_RADIUS },
|
|
107
112
|
"&:focus-visible::after": focusVisibleRing(colors),
|
|
108
|
-
}, children: title })) : (title) }), selected ? _jsx(VisuallyHidden, { children: copy.selected }) : null, rating !== undefined && rating > 0 ? (_jsx(Rating, { value: rating, count: ratingCount, size: "xs", compactCount: true })) : horizontal ? null : (_jsx(Box, { minH: "1rem", "aria-hidden": true })), _jsx(PriceBlock, { size: m.price, price: price, mrp: soldOut ? undefined : mrp, priceRange: range, unitPrice: unitPrice, currency: currency, locale: locale }), soldOut ? _jsx(StockStatus, { status: "out-of-stock", message: copy.soldOut, size: "sm" }) : null, metaLine, swatchAxis && activeId ? (_jsx(ProductCardSwatches, { axis: swatchAxis, limit: swatchLimit, dot: m.dot, activeId: activeId, soldOutIds: soldOutValues, labels: copy, onPreview: setPreviewed, onPick: setPicked })) : null, horizontal ? (_jsxs(Flex, { "data-control": true, position: "relative", zIndex: 2, mt: "auto", pt: 2, align: "center", justify: "space-between", gap: 1, children: [wishlist, control] })) : null] }), action === "options" ? (_jsx(QuickAddSheet, { isOpen: sheetOpen, onClose: () => setSheetOpen(false), title: title, brand: brand, media: frames[0], axes: axes, variants: variants, defaultSelection: swatchAxis && picked ? { [swatchAxis.id]: picked } : undefined, currency: currency, locale: locale, presentation: quickAddPresentation, drawerContentProps: quickAddDrawerProps, onConfirm: (variant, count) => {
|
|
113
|
+
}, children: title })) : (title) }), selected ? _jsx(VisuallyHidden, { children: copy.selected }) : null, rating !== undefined && rating > 0 ? (_jsx(Rating, { value: rating, count: ratingCount, size: "xs", compactCount: true })) : horizontal ? null : (_jsx(Box, { minH: "1rem", "aria-hidden": true })), _jsx(PriceBlock, { size: m.price, price: price, mrp: soldOut ? undefined : mrp, priceRange: range, unitPrice: unitPrice, currency: currency, locale: locale }), soldOut ? _jsx(StockStatus, { status: "out-of-stock", message: copy.soldOut, size: "sm" }) : null, metaLine, swatchAxis && activeId ? (_jsx(ProductCardSwatches, { axis: swatchAxis, limit: swatchLimit, dot: m.dot, activeId: activeId, soldOutIds: soldOutValues, labels: copy, onPreview: setPreviewed, onPick: setPicked })) : null, horizontal ? (_jsxs(Flex, { "data-control": true, position: "relative", zIndex: 2, mt: "auto", pt: 2, align: "center", justify: "space-between", gap: 1, children: [wishlist, control] })) : null] }), action === "options" && sheetMounted ? (_jsx(QuickAddSheet, { isOpen: sheetOpen, onClose: () => setSheetOpen(false), title: title, brand: brand, media: frames[0], axes: axes, variants: variants, defaultSelection: swatchAxis && picked ? { [swatchAxis.id]: picked } : undefined, currency: currency, locale: locale, presentation: quickAddPresentation, drawerContentProps: quickAddDrawerProps, onConfirm: (variant, count) => {
|
|
109
114
|
onAdd === null || onAdd === void 0 ? void 0 : onAdd(variant.id, count);
|
|
110
115
|
setSheetOpen(false);
|
|
111
116
|
} })) : null] }));
|
|
@@ -7,7 +7,7 @@ import { transitionFor } from "../../Theme/tokens/primitives/motion.js";
|
|
|
7
7
|
import { reducedMotionSx } from "../../Theme/chakra/motion.js";
|
|
8
8
|
import { CSS_RATIO, MEDIA_RADIUS } from "./cardMetrics.js";
|
|
9
9
|
const DIMMED_OPACITY = 0.6;
|
|
10
|
-
const Frame = ({ src, alt, fallbackText, quiet = false }) => {
|
|
10
|
+
const Frame = ({ src, srcSet, sizes, alt, fallbackText, quiet = false }) => {
|
|
11
11
|
const { colors } = useCustomTheme();
|
|
12
12
|
const ref = useRef(null);
|
|
13
13
|
const [loaded, setLoaded] = useState(false);
|
|
@@ -22,12 +22,12 @@ const Frame = ({ src, alt, fallbackText, quiet = false }) => {
|
|
|
22
22
|
return null;
|
|
23
23
|
return (_jsxs(Box, { role: "img", "aria-label": alt, position: "absolute", inset: 0, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 1.5, color: colors.textMuted, bg: colors.background[100], children: [_jsx(ImageOff, { size: 24, "aria-hidden": true }), fallbackText ? (_jsx(Text, { as: "span", fontSize: "xs", fontWeight: 500, children: fallbackText })) : null] }));
|
|
24
24
|
}
|
|
25
|
-
return (_jsx(Box, { as: "img", ref: ref, src: src, alt: alt, loading: "lazy", decoding: "async", draggable: false, onLoad: () => setLoaded(true), onError: () => setFailed(true), position: "absolute", inset: 0, w: "100%", h: "100%", objectFit: "cover", display: "block", opacity: loaded ? 1 : 0, transition: transitionFor(["opacity"]), sx: reducedMotionSx() }));
|
|
25
|
+
return (_jsx(Box, { as: "img", ref: ref, src: src, srcSet: srcSet, sizes: sizes, alt: alt, loading: "lazy", decoding: "async", draggable: false, onLoad: () => setLoaded(true), onError: () => setFailed(true), position: "absolute", inset: 0, w: "100%", h: "100%", objectFit: "cover", display: "block", opacity: loaded ? 1 : 0, transition: transitionFor(["opacity"]), sx: reducedMotionSx() }));
|
|
26
26
|
};
|
|
27
27
|
/** Tonal media well: cover-fitted primary frame, optional hover frame, broken-image tile; overlays are children. */
|
|
28
28
|
const ProductCardMedia = ({ primary, hover, title, alt, aspectRatio, dimmed, selected, fallbackText, width, children, }) => {
|
|
29
29
|
var _a, _b;
|
|
30
30
|
const { colors } = useCustomTheme();
|
|
31
|
-
return (_jsxs(Box, { "data-media": true, "data-ratio": aspectRatio, position: "relative", flexShrink: 0, w: width !== null && width !== void 0 ? width : "100%", sx: { aspectRatio: CSS_RATIO[aspectRatio] }, borderRadius: MEDIA_RADIUS, overflow: "hidden", bg: colors.background[100], boxShadow: selected ? `0 0 0 2px ${colors.header[900]}` : "none", transition: transitionFor(["box-shadow"]), children: [_jsx(Box, { "data-primary": true, position: "absolute", inset: 0, opacity: dimmed ? DIMMED_OPACITY : 1, transition: transitionFor(["transform", "opacity"], "base"), sx: reducedMotionSx(), children: primary ? (_jsx(Frame, { src: primary.src, alt: (_b = (_a = primary.alt) !== null && _a !== void 0 ? _a : alt) !== null && _b !== void 0 ? _b : title, fallbackText: fallbackText }, primary.src)) : (_jsx(Frame, { src: "", alt: alt !== null && alt !== void 0 ? alt : title, fallbackText: fallbackText }, "none")) }), hover ? (_jsx(Box, { "data-hover-frame": true, "aria-hidden": true, position: "absolute", inset: 0, opacity: 0, pointerEvents: "none", transition: transitionFor(["opacity"], "base"), sx: reducedMotionSx(), children: _jsx(Frame, { src: hover.src, alt: "", quiet: true }, hover.src) })) : null, children] }));
|
|
31
|
+
return (_jsxs(Box, { "data-media": true, "data-ratio": aspectRatio, position: "relative", flexShrink: 0, w: width !== null && width !== void 0 ? width : "100%", sx: { aspectRatio: CSS_RATIO[aspectRatio] }, borderRadius: MEDIA_RADIUS, overflow: "hidden", bg: colors.background[100], boxShadow: selected ? `0 0 0 2px ${colors.header[900]}` : "none", transition: transitionFor(["box-shadow"]), children: [_jsx(Box, { "data-primary": true, position: "absolute", inset: 0, opacity: dimmed ? DIMMED_OPACITY : 1, transition: transitionFor(["transform", "opacity"], "base"), sx: reducedMotionSx(), children: primary ? (_jsx(Frame, { src: primary.src, srcSet: primary.srcSet, sizes: primary.sizes, alt: (_b = (_a = primary.alt) !== null && _a !== void 0 ? _a : alt) !== null && _b !== void 0 ? _b : title, fallbackText: fallbackText }, primary.src)) : (_jsx(Frame, { src: "", alt: alt !== null && alt !== void 0 ? alt : title, fallbackText: fallbackText }, "none")) }), hover ? (_jsx(Box, { "data-hover-frame": true, "aria-hidden": true, position: "absolute", inset: 0, opacity: 0, pointerEvents: "none", transition: transitionFor(["opacity"], "base"), sx: reducedMotionSx(), children: _jsx(Frame, { src: hover.src, srcSet: hover.srcSet, sizes: hover.sizes, alt: "", quiet: true }, hover.src) })) : null, children] }));
|
|
32
32
|
};
|
|
33
33
|
export default ProductCardMedia;
|
|
@@ -11,7 +11,7 @@ import QuantityStepper from "../QuantityStepper/QuantityStepper.js";
|
|
|
11
11
|
import VariantSelector from "../VariantSelector/VariantSelector.js";
|
|
12
12
|
import { findVariant } from "../Commerce/commerceVariants.js";
|
|
13
13
|
import { rangeOf, firstMissingAxis } from "./productCardModel.js";
|
|
14
|
-
import {
|
|
14
|
+
import { useMatchMedia } from "../../Hooks/useMatchMedia.js";
|
|
15
15
|
const DEFAULT_LABELS = {
|
|
16
16
|
heading: "Choose options",
|
|
17
17
|
quantity: "Quantity",
|
|
@@ -24,7 +24,7 @@ const DESKTOP_QUERY = "(min-width: 48em)";
|
|
|
24
24
|
const QuickAddSheet = ({ isOpen, onClose, title, brand, media, axes, variants, defaultSelection, onConfirm, presentation = "auto", drawerContentProps, labels, currency, locale, "data-testid": testId, }) => {
|
|
25
25
|
var _a, _b, _c;
|
|
26
26
|
const { colors } = useCustomTheme();
|
|
27
|
-
const desktop =
|
|
27
|
+
const desktop = useMatchMedia(DESKTOP_QUERY);
|
|
28
28
|
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
29
29
|
const [selection, setSelection] = useState(defaultSelection !== null && defaultSelection !== void 0 ? defaultSelection : {});
|
|
30
30
|
const [quantity, setQuantity] = useState(1);
|
|
@@ -49,4 +49,5 @@ export const CSS_RATIO = {
|
|
|
49
49
|
};
|
|
50
50
|
export const MEDIA_RADIUS = "1rem";
|
|
51
51
|
export const TITLE_LINES = 2;
|
|
52
|
-
export const
|
|
52
|
+
export const FINE_POINTER_QUERY = "(hover: hover) and (pointer: fine)";
|
|
53
|
+
export const FINE_POINTER = `@media ${FINE_POINTER_QUERY}`;
|
|
@@ -11,7 +11,7 @@ import Button from "../Button/Button.js";
|
|
|
11
11
|
import Skeletons from "../Skeletons/Skeletons.js";
|
|
12
12
|
import MediaGallery from "../MediaGallery/MediaGallery.js";
|
|
13
13
|
import { useContainerWidth } from "../Commerce/useContainerWidth.js";
|
|
14
|
-
import { useMatchMedia } from "
|
|
14
|
+
import { useMatchMedia } from "../../Hooks/useMatchMedia.js";
|
|
15
15
|
import ProductBadges from "../ProductBadges/ProductBadges.js";
|
|
16
16
|
import Rating from "../Rating/Rating.js";
|
|
17
17
|
import PriceBlock from "../PriceBlock/PriceBlock.js";
|
|
@@ -59,7 +59,7 @@ const scrollToNode = (node, reduced) => {
|
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
61
|
/** Product page that wires one selection to price, stock, gallery frames and the primary action; every region has a slot. */
|
|
62
|
-
const ProductDetails = ({ product, selection: selectionProp, defaultSelection, onSelectionChange, quantity: quantityProp, defaultQuantity = 1, onQuantityChange, maxQuantity = DEFAULT_MAX_QUANTITY, onAddToBag, onBuyNow, onNotify, onGoToBag, addedDuration = DEFAULT_ADDED_MS, wishlisted, defaultWishlisted, onWishlistChange, onShare, offers, offersProps, trustItems, pincode, sizeGuide, onGuideClick, reviews, ratingBreakdown, reviewsProps, onWriteReview, detailSectionsProps, care, breadcrumbs, layout = "auto", mediaSlot, metaSlot, infoSlot, belowFold, relatedSlot, isLoading = false, labels: labelOverrides, currency, locale, "data-testid": testId, }) => {
|
|
62
|
+
const ProductDetails = ({ product, selection: selectionProp, defaultSelection, onSelectionChange, quantity: quantityProp, defaultQuantity = 1, onQuantityChange, maxQuantity = DEFAULT_MAX_QUANTITY, onAddToBag, onBuyNow, onNotify, onGoToBag, addedDuration = DEFAULT_ADDED_MS, wishlisted, defaultWishlisted, onWishlistChange, onShare, offers, offersProps, trustItems, pincode, sizeGuide, onGuideClick, reviews, ratingBreakdown, reviewsProps, onWriteReview, detailSectionsProps, care, breadcrumbs, layout = "auto", size, mediaSlot, metaSlot, infoSlot, belowFold, relatedSlot, isLoading = false, labels: labelOverrides, currency, locale, "data-testid": testId, }) => {
|
|
63
63
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
64
64
|
const { colors } = useCustomTheme();
|
|
65
65
|
const reduced = usePrefersReducedMotion();
|
|
@@ -70,6 +70,7 @@ const ProductDetails = ({ product, selection: selectionProp, defaultSelection, o
|
|
|
70
70
|
const containerWidth = useContainerWidth(rootRef);
|
|
71
71
|
const wide = useMatchMedia(DESKTOP_QUERY, false);
|
|
72
72
|
const desktop = layout === "auto" ? (containerWidth === undefined ? wide : containerWidth >= DESKTOP_MIN_WIDTH) : layout === "desktop";
|
|
73
|
+
const controlSize = size !== null && size !== void 0 ? size : (desktop ? "md" : "lg");
|
|
73
74
|
const variants = useMemo(() => resolveVariants(product), [product]);
|
|
74
75
|
const axes = useMemo(() => withGuide(product.axes, sizeGuide), [product.axes, sizeGuide]);
|
|
75
76
|
const [innerSelection, setInnerSelection] = useState(defaultSelection !== null && defaultSelection !== void 0 ? defaultSelection : {});
|
|
@@ -201,11 +202,11 @@ const ProductDetails = ({ product, selection: selectionProp, defaultSelection, o
|
|
|
201
202
|
const showReviews = reviews !== undefined || product.ratingCount !== undefined;
|
|
202
203
|
const sectionGap = desktop ? 18 : 10;
|
|
203
204
|
const gutter = desktop ? 6 : 4;
|
|
204
|
-
const wishlist = (_jsx(WishlistToggle, { selected: wishlisted, defaultSelected: defaultWishlisted, onSelectedChange: onWishlistChange, size: desktop ?
|
|
205
|
+
const wishlist = (_jsx(WishlistToggle, { selected: wishlisted, defaultSelected: defaultWishlisted, onSelectedChange: onWishlistChange, size: desktop ? controlSize : "md", variant: desktop ? "soft" : "glass", label: labels.wishlistSave }));
|
|
205
206
|
const badgeSlot = product.badges && product.badges.length > 0 ? _jsx(ProductBadges, { badges: product.badges, max: 2, variant: "glass", size: "md" }) : undefined;
|
|
206
207
|
const gallery = mediaSlot !== null && mediaSlot !== void 0 ? mediaSlot : (_jsx(MediaGallery, { media: product.media, layout: desktop ? "thumbs-left" : "swipe", aspectRatio: "4/5", activeMediaIds: activeIds, sticky: desktop, stickyTop: 24, badges: badgeSlot, actions: desktop ? undefined : wishlist, isLoading: isLoading, "aria-label": product.title }));
|
|
207
|
-
const primaryButton = (_jsx(Button, { size:
|
|
208
|
-
const buyPanel = isLoading ? (_jsxs(Flex, { direction: "column", gap: 4, "aria-busy": "true", children: [_jsx(Skeletons, { type: "skeleton", height: "1rem", width: "30%" }), _jsx(Skeletons, { type: "skeleton", height: "2rem", width: "90%" }), _jsx(Skeletons, { type: "skeleton", height: "2.5rem", width: "45%" }), _jsx(Skeletons, { type: "skeleton", height: "3rem", width: "100%" }), _jsx(Skeletons, { type: "skeleton", height: "3rem", width: "100%" })] })) : (_jsxs(Flex, { direction: "column", gap: 6, children: [_jsxs(Flex, { direction: "column", gap: 2, children: [_jsxs(Flex, { align: "center", justify: "space-between", gap: 3, minH: product.brand || onShare ? "2.75rem" : undefined, children: [product.brand ? (_jsx(Text, { as: "span", fontSize: "sm", fontWeight: 600, color: colors.textMuted, children: product.brand })) : (_jsx("span", {})), onShare ? (_jsx(Box, { as: "button", type: "button", "aria-label": labels.share, onClick: onShare, display: "inline-flex", alignItems: "center", justifyContent: "center", boxSize: "2.75rem", mr: "-0.625rem", borderRadius: "full", color: colors.header[900], transition: transitionFor(["background-color"]), _hover: { bg: colors.background[100] }, sx: { ...focusVisibleStyles(colors), ...reducedMotionSx() }, children: _jsx(Share2, { size: 20, "aria-hidden": true }) })) : null] }), _jsx(Text, { as: "h1", fontSize: desktop ? "1.75rem" : "1.375rem", lineHeight: 1.25, fontWeight: 700, letterSpacing: "-0.02em", color: colors.header[900], children: product.title }), product.rating !== undefined && showReviews ? (_jsx(Box, { as: "button", type: "button", onClick: jumpToReviews, "aria-label": labels.jumpToReviews(ratingText), alignSelf: "flex-start", display: "inline-flex", alignItems: "center", minH: "2.75rem", borderRadius: "md", sx: { ...focusVisibleStyles(colors) }, children: _jsx(Rating, { value: product.rating, count: product.ratingCount, size: "sm", "aria-label": ratingText, locale: locale }) })) : product.rating !== undefined ? (_jsx(Rating, { value: product.rating, count: product.ratingCount, size: "sm", locale: locale })) : null] }), _jsxs(Flex, { direction: "column", gap: 2, children: [_jsx(PriceBlock, { size: "lg", price: price, mrp: mrp, priceRange: showRange ? range : undefined, showSavings: true, taxNote: labels.taxNote, unitPrice: packUnitPrice(product, selection, price), ...fmt }), state ? _jsx(StockStatus, { status: state, count: variant === null || variant === void 0 ? void 0 : variant.stock }) : null, metaSlot] }), offers && offers.length > 0 ? _jsx(OffersList, { offers: offers, ...offersProps }) : null, axes.length > 0 ? (_jsx(Box, { ref: selectorRef, children: _jsx(VariantSelector, { axes: axes, variants: variants, value: selection, onValueChange: handleSelection, error: showSelectError && !complete ? selectPrompt : undefined, showSelectedLabel: true, showLowStock: true, onGuideClick: onGuideClick }) })) : null, _jsxs(Flex, { direction: "column", gap: 3, ref: ctaRef, children: [_jsxs(Flex, { align: "center", gap: 3, children: [showStepper ? (_jsx(QuantityStepper, { size:
|
|
208
|
+
const primaryButton = (_jsx(Button, { size: controlSize, width: "100%", variant: notifyMode ? "outline" : "solid", label: primaryLabel, leftIcon: bag === "added" ? _jsx(Check, { size: 18, "aria-hidden": true }) : notifyMode ? _jsx(BellRing, { size: 18, "aria-hidden": true }) : undefined, isLoading: bag === "adding", onClick: notifyMode ? handleNotify : handleAdd, fontWeight: 600 }));
|
|
209
|
+
const buyPanel = isLoading ? (_jsxs(Flex, { direction: "column", gap: 4, "aria-busy": "true", children: [_jsx(Skeletons, { type: "skeleton", height: "1rem", width: "30%" }), _jsx(Skeletons, { type: "skeleton", height: "2rem", width: "90%" }), _jsx(Skeletons, { type: "skeleton", height: "2.5rem", width: "45%" }), _jsx(Skeletons, { type: "skeleton", height: "3rem", width: "100%" }), _jsx(Skeletons, { type: "skeleton", height: "3rem", width: "100%" })] })) : (_jsxs(Flex, { direction: "column", gap: 6, children: [_jsxs(Flex, { direction: "column", gap: 2, children: [_jsxs(Flex, { align: "center", justify: "space-between", gap: 3, minH: product.brand || onShare ? "2.75rem" : undefined, children: [product.brand ? (_jsx(Text, { as: "span", fontSize: "sm", fontWeight: 600, color: colors.textMuted, children: product.brand })) : (_jsx("span", {})), onShare ? (_jsx(Box, { as: "button", type: "button", "aria-label": labels.share, onClick: onShare, display: "inline-flex", alignItems: "center", justifyContent: "center", boxSize: "2.75rem", mr: "-0.625rem", borderRadius: "full", color: colors.header[900], transition: transitionFor(["background-color"]), _hover: { bg: colors.background[100] }, sx: { ...focusVisibleStyles(colors), ...reducedMotionSx() }, children: _jsx(Share2, { size: 20, "aria-hidden": true }) })) : null] }), _jsx(Text, { as: "h1", fontSize: desktop ? "1.75rem" : "1.375rem", lineHeight: 1.25, fontWeight: 700, letterSpacing: "-0.02em", color: colors.header[900], children: product.title }), product.rating !== undefined && showReviews ? (_jsx(Box, { as: "button", type: "button", onClick: jumpToReviews, "aria-label": labels.jumpToReviews(ratingText), alignSelf: "flex-start", display: "inline-flex", alignItems: "center", minH: "2.75rem", borderRadius: "md", sx: { ...focusVisibleStyles(colors) }, children: _jsx(Rating, { value: product.rating, count: product.ratingCount, size: "sm", "aria-label": ratingText, locale: locale }) })) : product.rating !== undefined ? (_jsx(Rating, { value: product.rating, count: product.ratingCount, size: "sm", locale: locale })) : null] }), _jsxs(Flex, { direction: "column", gap: 2, children: [_jsx(PriceBlock, { size: "lg", price: price, mrp: mrp, priceRange: showRange ? range : undefined, showSavings: true, taxNote: labels.taxNote, unitPrice: packUnitPrice(product, selection, price), ...fmt }), state ? _jsx(StockStatus, { status: state, count: variant === null || variant === void 0 ? void 0 : variant.stock }) : null, metaSlot] }), offers && offers.length > 0 ? _jsx(OffersList, { offers: offers, ...offersProps }) : null, axes.length > 0 ? (_jsx(Box, { ref: selectorRef, children: _jsx(VariantSelector, { axes: axes, variants: variants, value: selection, onValueChange: handleSelection, error: showSelectError && !complete ? selectPrompt : undefined, showSelectedLabel: true, showLowStock: true, onGuideClick: onGuideClick }) })) : null, _jsxs(Flex, { direction: "column", gap: 3, ref: ctaRef, children: [_jsxs(Flex, { align: "center", gap: 3, children: [showStepper ? (_jsx(QuantityStepper, { size: controlSize, value: quantity, onValueChange: handleQuantity, min: 1, max: ceiling, itemLabel: product.title })) : null, _jsx(Box, { flex: 1, minW: 0, children: primaryButton }), desktop ? wishlist : null] }), showBuyNow ? _jsx(Button, { size: controlSize, width: "100%", variant: "outline", label: labels.buyNow, onClick: handleBuyNow }) : null, addFailed ? (_jsxs(Flex, { role: "alert", align: "center", gap: 1.5, color: colors.semanticText.error, fontSize: "sm", fontWeight: 600, children: [_jsx(CircleAlert, { size: 14, "aria-hidden": true }), _jsx(Text, { as: "span", children: labels.addFailed })] })) : null] }), pincode ? _jsx(PincodeChecker, { ...pincode }) : null, trustItems && trustItems.length > 0 ? _jsx(TrustBadges, { items: trustItems, layout: desktop ? "grid" : "row" }) : null, infoSlot] }));
|
|
209
210
|
return (_jsxs(Box, { ref: rootRef, "data-testid": testId, "data-layout": desktop ? "desktop" : "mobile", w: "100%", maxW: "82rem", mx: "auto", px: desktop ? 6 : 0, children: [_jsx(VisuallyHidden, { role: "status", "aria-live": "polite", "aria-atomic": "true", children: announcement }), breadcrumbs ? _jsx(Box, { px: desktop ? 0 : gutter, py: 3, children: breadcrumbs }) : null, _jsxs(Flex, { direction: desktop ? "row" : "column", align: desktop ? "flex-start" : "stretch", gap: desktop ? 12 : 5, children: [_jsx(Box, { flex: desktop ? "1.63 1 0" : undefined, minW: 0, w: desktop ? undefined : "100%", children: gallery }), _jsx(Box, { flex: desktop ? "1 1 0" : undefined, minW: 0, px: desktop ? 0 : gutter, children: buyPanel })] }), !isLoading ? (_jsxs(Flex, { direction: "column", gap: sectionGap, mt: sectionGap, px: desktop ? 0 : gutter, children: [_jsx(DetailSections, { description: product.description, highlights: product.highlights, specs: product.specs, care: care, shipping: product.returnsNote, legal: hasLegal
|
|
210
211
|
? {
|
|
211
212
|
netQuantity: product.netQuantity,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useMemo, useSyncExternalStore } from "react";
|
|
2
|
+
const entries = new Map();
|
|
3
|
+
const supported = () => typeof window !== "undefined" && typeof window.matchMedia === "function";
|
|
4
|
+
const subscriberFor = (query) => (notify) => {
|
|
5
|
+
if (!supported())
|
|
6
|
+
return () => undefined;
|
|
7
|
+
let entry = entries.get(query);
|
|
8
|
+
if (!entry) {
|
|
9
|
+
const list = window.matchMedia(query);
|
|
10
|
+
const created = { list, listeners: new Set(), onChange: () => created.listeners.forEach((fn) => fn()) };
|
|
11
|
+
list.addEventListener("change", created.onChange);
|
|
12
|
+
entries.set(query, created);
|
|
13
|
+
entry = created;
|
|
14
|
+
}
|
|
15
|
+
entry.listeners.add(notify);
|
|
16
|
+
return () => {
|
|
17
|
+
const current = entries.get(query);
|
|
18
|
+
if (!current)
|
|
19
|
+
return;
|
|
20
|
+
current.listeners.delete(notify);
|
|
21
|
+
if (current.listeners.size === 0) {
|
|
22
|
+
current.list.removeEventListener("change", current.onChange);
|
|
23
|
+
entries.delete(query);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
/** `matches` for a media query, sharing one listener per query; `fallback` where matchMedia is missing (SSR, jsdom). */
|
|
28
|
+
export const useMatchMedia = (query, fallback = false) => {
|
|
29
|
+
const subscribe = useMemo(() => subscriberFor(query), [query]);
|
|
30
|
+
const read = () => { var _a, _b; return (_b = (_a = entries.get(query)) === null || _a === void 0 ? void 0 : _a.list.matches) !== null && _b !== void 0 ? _b : (supported() ? window.matchMedia(query).matches : fallback); };
|
|
31
|
+
return useSyncExternalStore(subscribe, read, () => fallback);
|
|
32
|
+
};
|
|
@@ -1,21 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMatchMedia } from "../../Hooks/useMatchMedia.js";
|
|
2
2
|
export const REDUCED_MOTION_QUERY = "@media (prefers-reduced-motion: reduce)";
|
|
3
3
|
const MEDIA = "(prefers-reduced-motion: reduce)";
|
|
4
|
-
|
|
5
|
-
const hasMatchMedia = () => typeof window !== "undefined" && typeof window.matchMedia === "function";
|
|
6
|
-
const readPreference = () => hasMatchMedia() && window.matchMedia(MEDIA).matches;
|
|
7
|
-
export const usePrefersReducedMotion = () => {
|
|
8
|
-
const [reduced, setReduced] = useState(readPreference);
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
if (!hasMatchMedia())
|
|
11
|
-
return;
|
|
12
|
-
const query = window.matchMedia(MEDIA);
|
|
13
|
-
const onChange = () => setReduced(query.matches);
|
|
14
|
-
query.addEventListener("change", onChange);
|
|
15
|
-
return () => query.removeEventListener("change", onChange);
|
|
16
|
-
}, []);
|
|
17
|
-
return reduced;
|
|
18
|
-
};
|
|
4
|
+
export const usePrefersReducedMotion = () => useMatchMedia(MEDIA, false);
|
|
19
5
|
/** Spread into `sx`: drops transitions and animations when the user asks for less motion. */
|
|
20
6
|
export const reducedMotionSx = (overrides = {}) => ({
|
|
21
7
|
[REDUCED_MOTION_QUERY]: { transition: "none", animation: "none", ...overrides },
|
package/dist/esm/Utils/price.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
export const DEFAULT_CURRENCY = "INR";
|
|
2
2
|
export const DEFAULT_LOCALE = "en-IN";
|
|
3
|
+
const formatters = new Map();
|
|
4
|
+
const formatterFor = (locale, currency, minimum, maximum) => {
|
|
5
|
+
const key = `${locale}|${currency}|${minimum}|${maximum}`;
|
|
6
|
+
let formatter = formatters.get(key);
|
|
7
|
+
if (!formatter) {
|
|
8
|
+
formatter = new Intl.NumberFormat(locale, { style: "currency", currency, minimumFractionDigits: minimum, maximumFractionDigits: maximum });
|
|
9
|
+
formatters.set(key, formatter);
|
|
10
|
+
}
|
|
11
|
+
return formatter;
|
|
12
|
+
};
|
|
3
13
|
export function formatPrice(value, options = {}) {
|
|
4
14
|
const { currency = DEFAULT_CURRENCY, locale = DEFAULT_LOCALE, maximumFractionDigits } = options;
|
|
5
15
|
const whole = Number.isInteger(value);
|
|
6
|
-
return
|
|
7
|
-
style: "currency",
|
|
8
|
-
currency,
|
|
9
|
-
minimumFractionDigits: whole ? 0 : 2,
|
|
10
|
-
maximumFractionDigits: maximumFractionDigits !== null && maximumFractionDigits !== void 0 ? maximumFractionDigits : (whole ? 0 : 2),
|
|
11
|
-
}).format(value);
|
|
16
|
+
return formatterFor(locale, currency, whole ? 0 : 2, maximumFractionDigits !== null && maximumFractionDigits !== void 0 ? maximumFractionDigits : (whole ? 0 : 2)).format(value);
|
|
12
17
|
}
|
|
13
18
|
export function discountPercent(price, mrp) {
|
|
14
19
|
if (mrp === undefined || mrp <= price || mrp <= 0)
|
package/package.json
CHANGED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useMatchMedia = void 0;
|
|
4
|
-
const react_1 = require("react");
|
|
5
|
-
const supported = () => typeof window !== "undefined" && typeof window.matchMedia === "function";
|
|
6
|
-
/** `matches` for a media query, or `fallback` where matchMedia does not exist (SSR, jsdom). */
|
|
7
|
-
const useMatchMedia = (query, fallback) => {
|
|
8
|
-
const [matches, setMatches] = (0, react_1.useState)(() => (supported() ? window.matchMedia(query).matches : fallback));
|
|
9
|
-
(0, react_1.useEffect)(() => {
|
|
10
|
-
if (!supported())
|
|
11
|
-
return undefined;
|
|
12
|
-
const list = window.matchMedia(query);
|
|
13
|
-
const onChange = () => setMatches(list.matches);
|
|
14
|
-
onChange();
|
|
15
|
-
list.addEventListener("change", onChange);
|
|
16
|
-
return () => list.removeEventListener("change", onChange);
|
|
17
|
-
}, [query]);
|
|
18
|
-
return matches;
|
|
19
|
-
};
|
|
20
|
-
exports.useMatchMedia = useMatchMedia;
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useMinWidth = void 0;
|
|
4
|
-
const react_1 = require("react");
|
|
5
|
-
const supported = () => typeof window !== "undefined" && typeof window.matchMedia === "function";
|
|
6
|
-
/** `matches` for a media query; false where matchMedia does not exist (SSR, jsdom). */
|
|
7
|
-
const useMinWidth = (query) => {
|
|
8
|
-
const [matches, setMatches] = (0, react_1.useState)(() => (supported() ? window.matchMedia(query).matches : false));
|
|
9
|
-
(0, react_1.useEffect)(() => {
|
|
10
|
-
if (!supported())
|
|
11
|
-
return undefined;
|
|
12
|
-
const list = window.matchMedia(query);
|
|
13
|
-
const onChange = () => setMatches(list.matches);
|
|
14
|
-
onChange();
|
|
15
|
-
list.addEventListener("change", onChange);
|
|
16
|
-
return () => list.removeEventListener("change", onChange);
|
|
17
|
-
}, [query]);
|
|
18
|
-
return matches;
|
|
19
|
-
};
|
|
20
|
-
exports.useMinWidth = useMinWidth;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
const supported = () => typeof window !== "undefined" && typeof window.matchMedia === "function";
|
|
3
|
-
/** `matches` for a media query, or `fallback` where matchMedia does not exist (SSR, jsdom). */
|
|
4
|
-
export const useMatchMedia = (query, fallback) => {
|
|
5
|
-
const [matches, setMatches] = useState(() => (supported() ? window.matchMedia(query).matches : fallback));
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
if (!supported())
|
|
8
|
-
return undefined;
|
|
9
|
-
const list = window.matchMedia(query);
|
|
10
|
-
const onChange = () => setMatches(list.matches);
|
|
11
|
-
onChange();
|
|
12
|
-
list.addEventListener("change", onChange);
|
|
13
|
-
return () => list.removeEventListener("change", onChange);
|
|
14
|
-
}, [query]);
|
|
15
|
-
return matches;
|
|
16
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
const supported = () => typeof window !== "undefined" && typeof window.matchMedia === "function";
|
|
3
|
-
/** `matches` for a media query; false where matchMedia does not exist (SSR, jsdom). */
|
|
4
|
-
export const useMinWidth = (query) => {
|
|
5
|
-
const [matches, setMatches] = useState(() => (supported() ? window.matchMedia(query).matches : false));
|
|
6
|
-
useEffect(() => {
|
|
7
|
-
if (!supported())
|
|
8
|
-
return undefined;
|
|
9
|
-
const list = window.matchMedia(query);
|
|
10
|
-
const onChange = () => setMatches(list.matches);
|
|
11
|
-
onChange();
|
|
12
|
-
list.addEventListener("change", onChange);
|
|
13
|
-
return () => list.removeEventListener("change", onChange);
|
|
14
|
-
}, [query]);
|
|
15
|
-
return matches;
|
|
16
|
-
};
|