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.
Files changed (32) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/DESIGN-GUIDE.md +1 -1
  3. package/dist/Components/Commerce/commerceTypes.d.ts +5 -0
  4. package/dist/Components/MediaGallery/MediaGallery.js +1 -1
  5. package/dist/Components/ProductCard/ProductCard.js +8 -3
  6. package/dist/Components/ProductCard/ProductCardMedia.js +3 -3
  7. package/dist/Components/ProductCard/QuickAddSheet.js +2 -2
  8. package/dist/Components/ProductCard/cardMetrics.d.ts +1 -0
  9. package/dist/Components/ProductCard/cardMetrics.js +3 -2
  10. package/dist/Components/ProductDetails/ProductDetails.d.ts +1 -1
  11. package/dist/Components/ProductDetails/ProductDetails.js +6 -5
  12. package/dist/Components/ProductDetails/ProductDetailsProps.d.ts +2 -0
  13. package/dist/Hooks/useMatchMedia.d.ts +2 -0
  14. package/dist/Hooks/useMatchMedia.js +36 -0
  15. package/dist/Theme/chakra/motion.js +2 -16
  16. package/dist/Utils/price.js +11 -6
  17. package/dist/esm/Components/MediaGallery/MediaGallery.js +1 -1
  18. package/dist/esm/Components/ProductCard/ProductCard.js +9 -4
  19. package/dist/esm/Components/ProductCard/ProductCardMedia.js +3 -3
  20. package/dist/esm/Components/ProductCard/QuickAddSheet.js +2 -2
  21. package/dist/esm/Components/ProductCard/cardMetrics.js +2 -1
  22. package/dist/esm/Components/ProductDetails/ProductDetails.js +6 -5
  23. package/dist/esm/Hooks/useMatchMedia.js +32 -0
  24. package/dist/esm/Theme/chakra/motion.js +2 -16
  25. package/dist/esm/Utils/price.js +11 -6
  26. package/package.json +1 -1
  27. package/dist/Components/MediaGallery/useMatchMedia.d.ts +0 -2
  28. package/dist/Components/MediaGallery/useMatchMedia.js +0 -20
  29. package/dist/Components/ProductCard/useMinWidth.d.ts +0 -2
  30. package/dist/Components/ProductCard/useMinWidth.js +0 -20
  31. package/dist/esm/Components/MediaGallery/useMatchMedia.js +0 -16
  32. 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("./useMatchMedia");
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 hoverFrame = previewed === null ? frames[1] : undefined;
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 useMinWidth_1 = require("./useMinWidth");
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, useMinWidth_1.useMinWidth)(DESKTOP_QUERY);
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.FINE_POINTER = "@media (hover: hover) and (pointer: fine)";
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("../MediaGallery/useMatchMedia");
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 ? "lg" : "md", variant: desktop ? "soft" : "glass", label: labels.wishlistSave }));
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: desktop ? "md" : "lg", 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 }));
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: desktop ? "md" : "lg", 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: desktop ? "md" : "lg", 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] }));
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,2 @@
1
+ /** `matches` for a media query, sharing one listener per query; `fallback` where matchMedia is missing (SSR, jsdom). */
2
+ export declare const useMatchMedia: (query: string, fallback?: boolean) => boolean;
@@ -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 react_1 = require("react");
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
- // NOTE: jsdom has no matchMedia; Chakra's hook throws there and breaks consumer test suites.
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 = {}) => ({
@@ -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 new Intl.NumberFormat(locale, {
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 "./useMatchMedia.js";
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 { DENSITY, FINE_POINTER, MEDIA_RADIUS, TITLE_LINES } from "./cardMetrics.js";
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 hoverFrame = previewed === null ? frames[1] : undefined;
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 { useMinWidth } from "./useMinWidth.js";
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 = useMinWidth(DESKTOP_QUERY);
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 FINE_POINTER = "@media (hover: hover) and (pointer: fine)";
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 "../MediaGallery/useMatchMedia.js";
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 ? "lg" : "md", variant: desktop ? "soft" : "glass", label: labels.wishlistSave }));
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: desktop ? "md" : "lg", 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 }));
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: desktop ? "md" : "lg", 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: desktop ? "md" : "lg", 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] }));
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 { useEffect, useState } from "react";
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
- // NOTE: jsdom has no matchMedia; Chakra's hook throws there and breaks consumer test suites.
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 },
@@ -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 new Intl.NumberFormat(locale, {
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,6 +1,6 @@
1
1
  {
2
2
  "name": "pixelize-design-library",
3
- "version": "2.4.2-beta.57",
3
+ "version": "2.4.2-beta.58",
4
4
  "description": "React component library for Pixelize apps: themeable Chakra-based components, design tokens and light/dark brand theming.",
5
5
  "keywords": [
6
6
  "react",
@@ -1,2 +0,0 @@
1
- /** `matches` for a media query, or `fallback` where matchMedia does not exist (SSR, jsdom). */
2
- export declare const useMatchMedia: (query: string, fallback: boolean) => boolean;
@@ -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,2 +0,0 @@
1
- /** `matches` for a media query; false where matchMedia does not exist (SSR, jsdom). */
2
- export declare const useMinWidth: (query: string) => boolean;
@@ -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
- };