import type { ComponentPropsWithoutRef, ElementType, MouseEvent, ReactNode } from "react"; import type { OptionAxis, PriceFormat, ProductBadge, ProductMedia, ProductVariant } from "../Commerce/commerceTypes"; import type { UnitPrice } from "../../Utils/price"; import type { QuickAddPresentation, QuickAddSheetProps } from "./QuickAddSheetProps"; /** grid fills its column, list is a horizontal row with the action in the body, rail is a fixed-width scroll-snap item. */ export type ProductCardLayout = "grid" | "list" | "rail"; /** Type, insets and the add affordance scale together: compact is the 173 px phone 2-up, spacious the desktop 4-up. */ export type ProductCardDensity = "compact" | "comfortable" | "spacious"; /** Ratio of the media well; images cover it, so one ratio per grid keeps rows level. */ export type ProductCardAspectRatio = "4/5" | "1/1" | "3/4"; /** What the primary control does: choose options in the sheet, add at once, or ask to be notified. */ export type ProductCardAction = "options" | "add" | "notify"; /** Fires when the card (title link or button) is pressed anywhere outside its own controls. */ export type ProductCardPressHandler = (event: MouseEvent) => void; /** Visible and screen-reader copy of the card's own controls; override every string for another language. */ export interface ProductCardLabels { /** Wishlist button name, given the product title. */ save: (title: string) => string; /** Name of the control that opens the option sheet, given the product title. */ chooseOptions: (title: string) => string; /** Name of the control that adds a product with no options, given the product title. */ addToBag: (title: string) => string; /** Visible text of the notify control. */ notifyMe: string; /** Name of the notify control, given the product title. */ notifyMeFor: (title: string) => string; /** Visible text on the hover bar of a spacious card whose action opens options. */ chooseOptionsText: string; /** Visible text on the hover bar of a spacious card that adds directly. */ addToBagText: string; /** Status text of an unavailable product. */ soldOut: string; /** Screen-reader text added to a selected card. */ selected: string; /** Screen-reader text after the "+N" swatch count, given N. */ moreColours: (count: number) => string; /** Image fallback text. */ imageUnavailable: string; } /** Product card for listings, rails and search: media, brand, title, rating, price, swatches and one action; owns no product vocabulary beyond `axes`. */ export interface ProductCardProps extends PriceFormat { /** Product name; the title link or button text, and the card's accessible name. */ title: string; /** Muted line above the title; own-brand stores omit it. */ brand?: string; /** First frame is the primary image, the second is the hover frame; every frame is also the pool `mediaIdsByOption` points into. */ media: ProductMedia[]; /** Alt text for the primary image when the media item has none; defaults to the title. */ alt?: string; /** Selling price; a range replaces it when `variants` differ in price. */ price: number; /** List price; the strike and discount show only when above `price` and the product is in stock. */ mrp?: number; /** A `unitPrice()` result or a ready string, shown under the price. */ unitPrice?: UnitPrice | string | null; /** 0 to 5; the row is hidden when missing or 0, never rendered as "0 reviews". */ rating?: number; /** Review count shown beside the stars in compact form; has no effect when `rating` is missing or 0. */ ratingCount?: number; /** Badges in priority order; at most two are drawn, and none on a sold-out card. */ badges?: ProductBadge[]; /** Option axes; any axis with more than one value makes the primary action open the quick-add sheet. */ axes?: OptionAxis[]; /** Variants the axes resolve to; drive availability, the price range and the sheet's confirm button. */ variants?: ProductVariant[]; /** Maps `":"` to the media ids shown for that value, so a swatch swaps the card image. */ mediaIdsByOption?: Record; /** Most colour dots shown before "+N". */ swatchLimit?: number; /** Units left for a product without variants; shown as "Only N left" at or under the low-stock threshold. */ stock?: number; /** Forces the sold-out state; derived from `variants` or `stock` when omitted. */ outOfStock?: boolean; /** Renders the title as a link and makes the whole card its target. */ href?: string; /** Component for the title link, e.g. a router Link; receives `href` and `linkProps`. */ linkAs?: ElementType; /** Extra props for the title link, e.g. `{ to: "/p/jacket" }` for a router Link. */ linkProps?: Omit, "href" | "onClick">; /** Makes the title a stretched button when there is no `href`, or runs beside navigation when there is. */ onPress?: ProductCardPressHandler; /** Card arrangement; defaults to `grid`. */ layout?: ProductCardLayout; /** Type, inset and control scale; defaults to `comfortable`. */ density?: ProductCardDensity; /** Media well ratio; defaults to `4/5`. */ aspectRatio?: ProductCardAspectRatio; /** Overrides the action derived from the data (notify when sold out, options when an axis has choices, else add). */ action?: ProductCardAction; /** Units in the bag; above 0 the add control becomes a stepper. Omit to let the card track it after a direct add. */ quantity?: number; /** Fires from the stepper; 0 means the shopper removed the product. */ onQuantityChange?: (quantity: number) => void; /** How the quick-add sheet opens; `auto` follows the window width, so pass `drawer` inside a phone-width frame. */ quickAddPresentation?: QuickAddPresentation; /** Forwarded to the quick-add drawer panel; the drawer portals to the window; see `QuickAddSheetProps.drawerContentProps` for capping its width. */ quickAddDrawerProps?: QuickAddSheetProps["drawerContentProps"]; /** Fires on a direct add (quantity 1) and from the quick-add sheet; `variantId` is the chosen variant when one is known. */ onAdd?: (variantId: string | undefined, quantity: number) => void; /** Fires when the stepper's minus is pressed at 1, after `onQuantityChange(0)`. */ onRemove?: () => void; /** Fires from the notify control of a sold-out card. */ onNotify?: () => void; /** Controlled wishlist state. */ wishlisted?: boolean; /** Fires with the new wishlist state when the heart is toggled. */ onWishlistChange?: (wishlisted: boolean) => void; /** Marks the card as chosen (compare, bulk select): a ring on the media and a check, never colour alone. */ selected?: boolean; /** Renders the skeleton with the same box model, so nothing shifts on load. */ isLoading?: boolean; /** Glass content laid over the media well, clear of the badge, wishlist and action corners. */ mediaOverlay?: ReactNode; /** Replaces the default meta line (low-stock status) under the price; sold-out status stays. */ meta?: ReactNode; /** Replaces the derived add control, keeping its footprint. */ actions?: ReactNode; /** Overrides for the card's own control copy; unspecified strings keep the English defaults. */ labels?: Partial; "data-testid"?: string; } /** Skeleton with the card's box model; pass the same layout, density and ratio as the loaded cards. */ export interface ProductCardSkeletonProps { /** Card arrangement to mirror; defaults to `grid`. */ layout?: ProductCardLayout; /** Density to mirror; defaults to `compact`, unlike ProductCard's `comfortable`. */ density?: ProductCardDensity; /** Media well ratio to mirror; defaults to `4/5`. */ aspectRatio?: ProductCardAspectRatio; "data-testid"?: string; } /** Column counts per breakpoint of a card grid. */ export type ProductCardGridColumns = Partial>; /** Responsive grid for cards: 2 columns on a phone up to 5 on a wide screen, equal-height rows, one gap scale. */ export interface ProductCardGridProps { /** One card per child; each is wrapped in a list item that stretches it to the row height. */ children: ReactNode; /** Columns per breakpoint; defaults to 2, 3 from md, 4 from lg and 5 from xl. */ columns?: ProductCardGridColumns; /** Gap on both axes, overriding the window-based gaps; a number is in Chakra space units, a string any CSS length. */ gap?: string | number; /** Column gap only; wins over `gap`. */ columnGap?: string | number; /** Row gap only; wins over `gap`. */ rowGap?: string | number; /** Accessible name of the list, e.g. "Jackets". */ "aria-label"?: string; "data-testid"?: string; }