@thorprovider/blocks 3.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -21
- package/dist/blocks/designs/checkout-commerce-v1/block.d.ts +49 -0
- package/dist/blocks/designs/checkout-commerce-v1/block.d.ts.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/block.js +2487 -0
- package/dist/blocks/designs/checkout-commerce-v1/block.js.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/block.mjs +2467 -0
- package/dist/blocks/designs/checkout-commerce-v1/block.mjs.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/data-context.d.ts +8 -0
- package/dist/blocks/designs/checkout-commerce-v1/data-context.d.ts.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/data-context.js +44 -0
- package/dist/blocks/designs/checkout-commerce-v1/data-context.js.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/data-context.mjs +19 -0
- package/dist/blocks/designs/checkout-commerce-v1/data-context.mjs.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/index.d.ts +3 -0
- package/dist/blocks/designs/checkout-commerce-v1/index.d.ts.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/index.js +36 -0
- package/dist/blocks/designs/checkout-commerce-v1/index.js.map +1 -0
- package/dist/blocks/designs/checkout-commerce-v1/index.mjs +6 -0
- package/dist/blocks/designs/checkout-commerce-v1/index.mjs.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/block.d.ts +12 -0
- package/dist/blocks/designs/marketplace-product-card/block.d.ts.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/block.js +243 -0
- package/dist/blocks/designs/marketplace-product-card/block.js.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/block.mjs +233 -0
- package/dist/blocks/designs/marketplace-product-card/block.mjs.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/index.d.ts +3 -0
- package/dist/blocks/designs/marketplace-product-card/index.d.ts.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/index.js +36 -0
- package/dist/blocks/designs/marketplace-product-card/index.js.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/index.mjs +6 -0
- package/dist/blocks/designs/marketplace-product-card/index.mjs.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.d.ts +13 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.d.ts.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.js +36 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.js.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.mjs +12 -0
- package/dist/blocks/designs/marketplace-product-card/normalize.mjs.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/variants.d.ts +72 -0
- package/dist/blocks/designs/marketplace-product-card/variants.d.ts.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/variants.js +123 -0
- package/dist/blocks/designs/marketplace-product-card/variants.js.map +1 -0
- package/dist/blocks/designs/marketplace-product-card/variants.mjs +89 -0
- package/dist/blocks/designs/marketplace-product-card/variants.mjs.map +1 -0
- package/dist/blocks/e-commerce/account/account-frame.d.ts +38 -0
- package/dist/blocks/e-commerce/account/account-frame.d.ts.map +1 -0
- package/dist/blocks/e-commerce/account/account-frame.js +102 -0
- package/dist/blocks/e-commerce/account/account-frame.js.map +1 -0
- package/dist/blocks/e-commerce/account/account-frame.mjs +78 -0
- package/dist/blocks/e-commerce/account/account-frame.mjs.map +1 -0
- package/dist/blocks/e-commerce/account/address-book.d.ts +79 -0
- package/dist/blocks/e-commerce/account/address-book.d.ts.map +1 -0
- package/dist/blocks/e-commerce/account/address-book.js +250 -0
- package/dist/blocks/e-commerce/account/address-book.js.map +1 -0
- package/dist/blocks/e-commerce/account/address-book.mjs +225 -0
- package/dist/blocks/e-commerce/account/address-book.mjs.map +1 -0
- package/dist/blocks/e-commerce/account/profile-form.d.ts +35 -0
- package/dist/blocks/e-commerce/account/profile-form.d.ts.map +1 -0
- package/dist/blocks/e-commerce/account/profile-form.js +100 -0
- package/dist/blocks/e-commerce/account/profile-form.js.map +1 -0
- package/dist/blocks/e-commerce/account/profile-form.mjs +76 -0
- package/dist/blocks/e-commerce/account/profile-form.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkbox-check.js +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkbox-check.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkbox-check.mjs +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkbox-check.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.d.ts +11 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.js +46 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.mjs +22 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-equality.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.d.ts +28 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.js +278 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.mjs +260 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-address-fields.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.d.ts +12 -9
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.js +49 -71
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.mjs +50 -72
- package/dist/blocks/e-commerce/checkout-forms/checkout-contact-step.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.js +6 -3
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.mjs +6 -3
- package/dist/blocks/e-commerce/checkout-forms/checkout-country-field.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.d.ts +7 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.js +41 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.mjs +16 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-id-context.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.d.ts +24 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.js +75 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.mjs +58 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-location-select.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.d.ts +3 -2
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.js +9 -2
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.mjs +9 -2
- package/dist/blocks/e-commerce/checkout-forms/checkout-order-summary.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.d.ts +23 -8
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.js +136 -31
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.mjs +143 -32
- package/dist/blocks/e-commerce/checkout-forms/checkout-payment-step.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.d.ts +13 -7
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.js +179 -28
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.mjs +180 -29
- package/dist/blocks/e-commerce/checkout-forms/checkout-review-step.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.d.ts +20 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.js +199 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.mjs +175 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-saved-addresses.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.d.ts +14 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.js +136 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.mjs +109 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-section-nav.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.d.ts +32 -10
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.js +265 -147
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.mjs +272 -148
- package/dist/blocks/e-commerce/checkout-forms/checkout-shipping-step.mjs.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.d.ts +13 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.js +74 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.mjs +50 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-header.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.d.ts +7 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.d.ts.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.js +91 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.js.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.mjs +65 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout-step-summary.mjs.map +1 -0
- package/dist/blocks/e-commerce/checkout-forms/checkout.d.ts +174 -44
- package/dist/blocks/e-commerce/checkout-forms/checkout.d.ts.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout.js +532 -108
- package/dist/blocks/e-commerce/checkout-forms/checkout.js.map +1 -1
- package/dist/blocks/e-commerce/checkout-forms/checkout.mjs +542 -109
- package/dist/blocks/e-commerce/checkout-forms/checkout.mjs.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.d.ts.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.js +44 -20
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.js.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.mjs +44 -20
- package/dist/blocks/e-commerce/navigation/storefront-navbar-cart-sheet.mjs.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar.d.ts.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar.js +21 -10
- package/dist/blocks/e-commerce/navigation/storefront-navbar.js.map +1 -1
- package/dist/blocks/e-commerce/navigation/storefront-navbar.mjs +21 -10
- package/dist/blocks/e-commerce/navigation/storefront-navbar.mjs.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-card.d.ts.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-card.js +36 -9
- package/dist/blocks/e-commerce/product-list/product-card.js.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-card.mjs +36 -9
- package/dist/blocks/e-commerce/product-list/product-card.mjs.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-showcase-1.d.ts.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-showcase-1.js +7 -2
- package/dist/blocks/e-commerce/product-list/product-showcase-1.js.map +1 -1
- package/dist/blocks/e-commerce/product-list/product-showcase-1.mjs +7 -2
- package/dist/blocks/e-commerce/product-list/product-showcase-1.mjs.map +1 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.d.ts.map +1 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.js +5 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.js.map +1 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.mjs +5 -1
- package/dist/blocks/e-commerce/product-overview/product-quick-view-dialog.mjs.map +1 -1
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.d.ts.map +1 -1
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.js +71 -51
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.js.map +1 -1
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.mjs +71 -51
- package/dist/blocks/e-commerce/storefront-hero/storefront-hero.mjs.map +1 -1
- package/dist/blocks/marketing/banners/banner-1.d.ts.map +1 -1
- package/dist/blocks/marketing/banners/banner-1.js +15 -27
- package/dist/blocks/marketing/banners/banner-1.js.map +1 -1
- package/dist/blocks/marketing/banners/banner-1.mjs +16 -28
- package/dist/blocks/marketing/banners/banner-1.mjs.map +1 -1
- package/dist/index.d.ts +16 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13 -0
- package/dist/index.mjs.map +1 -1
- package/dist/lib/contracts/checkout-commerce-v1.d.ts +2350 -0
- package/dist/lib/contracts/checkout-commerce-v1.d.ts.map +1 -0
- package/dist/lib/contracts/checkout-commerce-v1.js +417 -0
- package/dist/lib/contracts/checkout-commerce-v1.js.map +1 -0
- package/dist/lib/contracts/checkout-commerce-v1.mjs +392 -0
- package/dist/lib/contracts/checkout-commerce-v1.mjs.map +1 -0
- package/dist/lib/contracts/marketplace-product-card.d.ts +324 -0
- package/dist/lib/contracts/marketplace-product-card.d.ts.map +1 -0
- package/dist/lib/contracts/marketplace-product-card.js +85 -0
- package/dist/lib/contracts/marketplace-product-card.js.map +1 -0
- package/dist/lib/contracts/marketplace-product-card.mjs +61 -0
- package/dist/lib/contracts/marketplace-product-card.mjs.map +1 -0
- package/dist/lib/contracts.d.ts +2289 -178
- package/dist/lib/contracts.d.ts.map +1 -1
- package/dist/lib/contracts.js +332 -25
- package/dist/lib/contracts.js.map +1 -1
- package/dist/lib/contracts.mjs +278 -5
- package/dist/lib/contracts.mjs.map +1 -1
- package/dist/lib/money-schema.d.ts +19 -0
- package/dist/lib/money-schema.d.ts.map +1 -0
- package/dist/lib/money-schema.js +33 -0
- package/dist/lib/money-schema.js.map +1 -0
- package/dist/lib/money-schema.mjs +9 -0
- package/dist/lib/money-schema.mjs.map +1 -0
- package/dist/lib/motion.d.ts +29 -0
- package/dist/lib/motion.d.ts.map +1 -0
- package/dist/lib/motion.js +65 -0
- package/dist/lib/motion.js.map +1 -0
- package/dist/lib/motion.mjs +41 -0
- package/dist/lib/motion.mjs.map +1 -0
- package/dist/ui/accordion.d.ts +3 -1
- package/dist/ui/accordion.d.ts.map +1 -1
- package/dist/ui/accordion.js +21 -5
- package/dist/ui/accordion.js.map +1 -1
- package/dist/ui/accordion.mjs +21 -5
- package/dist/ui/accordion.mjs.map +1 -1
- package/dist/ui/input-group.d.ts +1 -1
- package/package.json +18 -3
- package/dist/.tsbuildinfo +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/blocks/e-commerce/product-list/product-showcase-1.tsx"],"sourcesContent":["'use client'\n\nimport { useMemo, useState } from 'react'\nimport { ChevronLeft, ChevronRight, Star } from 'lucide-react'\nimport { motion, useReducedMotion } from 'motion/react'\n\nimport { Button } from '../../../ui/button'\nimport { Card, CardContent, CardFooter } from '../../../ui/card'\nimport { formatMoney } from '../../../lib/money'\nimport type { ProductPresentation } from '../../../lib/contracts'\n\nexport interface ProductShowcase1Props {\n products: ProductPresentation[]\n heading?: string\n description?: string\n pageSize?: number\n prevLabel?: string\n nextLabel?: string\n /** Label for the current-page status, e.g. \"Página\". */\n pageLabel?: string\n unavailablePriceLabel?: string\n emptyLabel?: string\n addToCartLabel?: string\n onAddToCart?: (product: ProductPresentation) => void\n onProductClick?: (product: ProductPresentation) => void\n}\n\nconst DEFAULT_PAGE_SIZE = 6\nconst SWIPE_THRESHOLD = 48\n\nfunction chunk<T>(items: readonly T[], size: number): T[][] {\n const pages: T[][] = []\n for (let index = 0; index < items.length; index += size) {\n pages.push(items.slice(index, index + size))\n }\n return pages\n}\n\nfunction compareAtText(product: ProductPresentation): string | undefined {\n const compareAt = product.compareAtPrice\n if (!compareAt) return undefined\n const formatted = formatMoney(compareAt)\n const price = formatMoney(product.price)\n if (!formatted || !price) return undefined\n if (Number(compareAt.amount) <= Number(product.price.amount)) return undefined\n return formatted\n}\n\n/**\n * Combined grid-and-carousel product presentation: fixed-size grid pages\n * navigated manually (prev/next) with a visible current-page indication. There\n * is no autoplay, no auto-advance, and no infinite loop. Controls are real\n * buttons (keyboard operable and visible at every breakpoint); touch swipe is an\n * addition, never the only way to navigate. Below-fold images are lazy.\n *\n * Data access lives in the host: this block only renders the props it receives\n * and reports interactions through callbacks (no backend/router/measurement\n * imports).\n */\nexport function ProductShowcase1({\n products,\n heading,\n description,\n pageSize = DEFAULT_PAGE_SIZE,\n prevLabel = 'Anterior',\n nextLabel = 'Siguiente',\n pageLabel = 'Página',\n unavailablePriceLabel = 'Precio no disponible',\n emptyLabel = 'No hay productos para mostrar.',\n addToCartLabel = 'Agregar al carrito',\n onAddToCart,\n onProductClick,\n}: ProductShowcase1Props) {\n const reduceMotion = useReducedMotion()\n const size = Number.isInteger(pageSize) && pageSize > 0 ? pageSize : DEFAULT_PAGE_SIZE\n const pages = useMemo(() => chunk(products, size), [products, size])\n const pageCount = pages.length\n const [page, setPage] = useState(0)\n const [touchStart, setTouchStart] = useState<number | null>(null)\n const safePage = pageCount === 0 ? 0 : Math.min(page, pageCount - 1)\n\n const goTo = (next: number) => {\n if (pageCount === 0) return\n if (next < 0 || next >= pageCount) return\n setPage(next)\n }\n\n if (pageCount === 0) {\n return (\n <section className='py-12'>\n <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>\n {heading && (\n <h2 className='mb-4 text-center text-2xl font-bold text-balance md:text-3xl'>\n {heading}\n </h2>\n )}\n <p className='text-muted-foreground text-center'>{emptyLabel}</p>\n </div>\n </section>\n )\n }\n\n const visible = pages[safePage]\n\n return (\n <section\n className='py-12'\n aria-roledescription='carousel'\n onKeyDown={(event) => {\n if (event.key === 'ArrowLeft') {\n event.preventDefault()\n goTo(safePage - 1)\n } else if (event.key === 'ArrowRight') {\n event.preventDefault()\n goTo(safePage + 1)\n }\n }}\n >\n <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>\n {(heading || description) && (\n <div className='mb-8 text-center'>\n {heading && (\n <h2 className='text-2xl font-bold text-balance md:text-3xl'>{heading}</h2>\n )}\n {description && <p className='text-muted-foreground mt-2 text-balance'>{description}</p>}\n </div>\n )}\n\n <div\n onTouchStart={(event) => setTouchStart(event.touches[0]?.clientX ?? null)}\n onTouchEnd={(event) => {\n if (touchStart === null) return\n const delta = (event.changedTouches[0]?.clientX ?? touchStart) - touchStart\n if (delta <= -SWIPE_THRESHOLD) goTo(safePage + 1)\n else if (delta >= SWIPE_THRESHOLD) goTo(safePage - 1)\n setTouchStart(null)\n }}\n >\n <motion.div\n key={safePage}\n initial={reduceMotion ? false : { opacity: 0 }}\n animate={{ opacity: 1 }}\n transition={{ duration: 0.3, ease: 'easeOut' }}\n className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:gap-6'\n >\n {visible.map((product) => {\n const compareAt = compareAtText(product)\n return (\n <Card\n key={product.id}\n className={`flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow duration-300 hover:shadow-md ${\n onProductClick ? 'cursor-pointer' : ''\n }`}\n onClick={onProductClick ? () => onProductClick(product) : undefined}\n >\n <CardContent className='flex flex-1 flex-col gap-4 px-4'>\n <div className='relative aspect-square overflow-hidden rounded-md'>\n {product.badge && (\n <span className='bg-destructive text-destructive-foreground absolute top-2 left-2 z-10 rounded-full px-2 py-0.5 text-xs font-semibold'>\n {product.badge}\n </span>\n )}\n <img\n src={product.image || undefined}\n alt={product.imageAlt ?? product.title}\n className='size-full rounded-md object-contain'\n loading='lazy'\n width={400}\n height={400}\n />\n </div>\n\n <div className='flex flex-1 flex-col'>\n <h3 className='mb-1 font-medium text-balance'>\n {onProductClick ? (\n <button\n type='button'\n onClick={(event) => {\n event.stopPropagation()\n onProductClick(product)\n }}\n className='hover:text-primary cursor-pointer text-left transition-colors'\n >\n {product.title}\n </button>\n ) : (\n product.title\n )}\n </h3>\n {typeof product.rating === 'number' && Number.isFinite(product.rating) && (\n <div className='text-muted-foreground mb-1 flex items-center gap-1 text-xs'>\n <Star className='size-3.5 fill-current' aria-hidden='true' />\n <span>{product.rating.toFixed(1)}</span>\n </div>\n )}\n <div className='mt-auto flex items-baseline gap-2'>\n <p className='font-semibold'>\n {formatMoney(product.price) ?? unavailablePriceLabel}\n </p>\n {compareAt && (\n <p className='text-muted-foreground text-sm line-through'>{compareAt}</p>\n )}\n </div>\n </div>\n </CardContent>\n\n <CardFooter className='bg-transparent border-t-0 px-3 pt-0 md:px-4'>\n <Button\n variant='outline'\n size='sm'\n className='h-8 w-full cursor-pointer px-3 text-sm'\n onClick={(event) => {\n event.stopPropagation()\n onAddToCart?.(product)\n }}\n >\n {addToCartLabel}\n </Button>\n </CardFooter>\n </Card>\n )\n })}\n </motion.div>\n </div>\n\n {pageCount > 1 && (\n <div className='mt-6 flex items-center justify-center gap-3'>\n <Button\n type='button'\n variant='outline'\n size='icon-sm'\n className='cursor-pointer'\n aria-label={prevLabel}\n disabled={safePage === 0}\n onClick={() => goTo(safePage - 1)}\n >\n <ChevronLeft className='size-4' aria-hidden='true' />\n </Button>\n\n <div className='flex items-center gap-2'>\n {pages.map((_, index) => (\n <button\n key={index}\n type='button'\n aria-label={`${pageLabel} ${index + 1}`}\n aria-current={index === safePage ? 'true' : undefined}\n className={`size-2 rounded-full transition-colors ${\n index === safePage ? 'bg-primary' : 'bg-muted-foreground/40'\n }`}\n onClick={() => goTo(index)}\n />\n ))}\n </div>\n\n <Button\n type='button'\n variant='outline'\n size='icon-sm'\n className='cursor-pointer'\n aria-label={nextLabel}\n disabled={safePage === pageCount - 1}\n onClick={() => goTo(safePage + 1)}\n >\n <ChevronRight className='size-4' aria-hidden='true' />\n </Button>\n </div>\n )}\n\n <p aria-live='polite' className='sr-only'>\n {pageLabel} {safePage + 1} / {pageCount}\n </p>\n </div>\n </section>\n )\n}\n\nexport default ProductShowcase1\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/blocks/e-commerce/product-list/product-showcase-1.tsx"],"sourcesContent":["'use client'\n\nimport { useMemo, useState } from 'react'\nimport { ChevronLeft, ChevronRight, Star } from 'lucide-react'\nimport { motion, useReducedMotion } from 'motion/react'\n\nimport { Button } from '../../../ui/button'\nimport { Card, CardContent, CardFooter } from '../../../ui/card'\nimport { MOTION } from '../../../lib/motion'\nimport { formatMoney } from '../../../lib/money'\nimport type { ProductPresentation } from '../../../lib/contracts'\n\nexport interface ProductShowcase1Props {\n products: ProductPresentation[]\n heading?: string\n description?: string\n pageSize?: number\n prevLabel?: string\n nextLabel?: string\n /** Label for the current-page status, e.g. \"Página\". */\n pageLabel?: string\n unavailablePriceLabel?: string\n emptyLabel?: string\n addToCartLabel?: string\n onAddToCart?: (product: ProductPresentation) => void\n onProductClick?: (product: ProductPresentation) => void\n}\n\nconst DEFAULT_PAGE_SIZE = 6\nconst SWIPE_THRESHOLD = 48\n\nfunction chunk<T>(items: readonly T[], size: number): T[][] {\n const pages: T[][] = []\n for (let index = 0; index < items.length; index += size) {\n pages.push(items.slice(index, index + size))\n }\n return pages\n}\n\nfunction compareAtText(product: ProductPresentation): string | undefined {\n const compareAt = product.compareAtPrice\n if (!compareAt) return undefined\n const formatted = formatMoney(compareAt)\n const price = formatMoney(product.price)\n if (!formatted || !price) return undefined\n if (Number(compareAt.amount) <= Number(product.price.amount)) return undefined\n return formatted\n}\n\n/**\n * Combined grid-and-carousel product presentation: fixed-size grid pages\n * navigated manually (prev/next) with a visible current-page indication. There\n * is no autoplay, no auto-advance, and no infinite loop. Controls are real\n * buttons (keyboard operable and visible at every breakpoint); touch swipe is an\n * addition, never the only way to navigate. Below-fold images are lazy.\n *\n * Data access lives in the host: this block only renders the props it receives\n * and reports interactions through callbacks (no backend/router/measurement\n * imports).\n */\nexport function ProductShowcase1({\n products,\n heading,\n description,\n pageSize = DEFAULT_PAGE_SIZE,\n prevLabel = 'Anterior',\n nextLabel = 'Siguiente',\n pageLabel = 'Página',\n unavailablePriceLabel = 'Precio no disponible',\n emptyLabel = 'No hay productos para mostrar.',\n addToCartLabel = 'Agregar al carrito',\n onAddToCart,\n onProductClick,\n}: ProductShowcase1Props) {\n const reduceMotion = useReducedMotion()\n const size = Number.isInteger(pageSize) && pageSize > 0 ? pageSize : DEFAULT_PAGE_SIZE\n const pages = useMemo(() => chunk(products, size), [products, size])\n const pageCount = pages.length\n const [page, setPage] = useState(0)\n const [touchStart, setTouchStart] = useState<number | null>(null)\n const safePage = pageCount === 0 ? 0 : Math.min(page, pageCount - 1)\n\n const goTo = (next: number) => {\n if (pageCount === 0) return\n if (next < 0 || next >= pageCount) return\n setPage(next)\n }\n\n if (pageCount === 0) {\n return (\n <section className='py-12'>\n <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>\n {heading && (\n <h2 className='mb-4 text-center text-2xl font-bold text-balance md:text-3xl'>\n {heading}\n </h2>\n )}\n <p className='text-muted-foreground text-center'>{emptyLabel}</p>\n </div>\n </section>\n )\n }\n\n const visible = pages[safePage]\n\n return (\n <section\n className='py-12'\n aria-roledescription='carousel'\n onKeyDown={(event) => {\n if (event.key === 'ArrowLeft') {\n event.preventDefault()\n goTo(safePage - 1)\n } else if (event.key === 'ArrowRight') {\n event.preventDefault()\n goTo(safePage + 1)\n }\n }}\n >\n <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>\n {(heading || description) && (\n <div className='mb-8 text-center'>\n {heading && (\n <h2 className='text-2xl font-bold text-balance md:text-3xl'>{heading}</h2>\n )}\n {description && <p className='text-muted-foreground mt-2 text-balance'>{description}</p>}\n </div>\n )}\n\n <div\n onTouchStart={(event) => setTouchStart(event.touches[0]?.clientX ?? null)}\n onTouchEnd={(event) => {\n if (touchStart === null) return\n const delta = (event.changedTouches[0]?.clientX ?? touchStart) - touchStart\n if (delta <= -SWIPE_THRESHOLD) goTo(safePage + 1)\n else if (delta >= SWIPE_THRESHOLD) goTo(safePage - 1)\n setTouchStart(null)\n }}\n >\n <motion.div\n key={safePage}\n initial={reduceMotion ? false : { opacity: 0 }}\n animate={{ opacity: 1 }}\n transition={{ duration: MOTION.durationSeconds.state, ease: MOTION.easing.standard }}\n className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:gap-6'\n >\n {visible.map((product) => {\n const compareAt = compareAtText(product)\n return (\n <Card\n key={product.id}\n className={`flex h-full flex-col gap-4 overflow-hidden rounded-lg py-4 shadow-none transition-shadow hover:shadow-md ${\n onProductClick ? 'cursor-pointer' : ''\n }`}\n style={{\n transitionDuration: `${MOTION.durationMilliseconds.feedback}ms`,\n transitionTimingFunction: MOTION.cssEasing.standard,\n }}\n onClick={onProductClick ? () => onProductClick(product) : undefined}\n >\n <CardContent className='flex flex-1 flex-col gap-4 px-4'>\n <div className='relative aspect-square overflow-hidden rounded-md'>\n {product.badge && (\n <span className='bg-destructive text-destructive-foreground absolute top-2 left-2 z-10 rounded-full px-2 py-0.5 text-xs font-semibold'>\n {product.badge}\n </span>\n )}\n <img\n src={product.image || undefined}\n alt={product.imageAlt ?? product.title}\n className='size-full rounded-md object-contain'\n loading='lazy'\n width={400}\n height={400}\n />\n </div>\n\n <div className='flex flex-1 flex-col'>\n <h3 className='mb-1 font-medium text-balance'>\n {onProductClick ? (\n <button\n type='button'\n onClick={(event) => {\n event.stopPropagation()\n onProductClick(product)\n }}\n className='hover:text-primary cursor-pointer text-left transition-colors'\n >\n {product.title}\n </button>\n ) : (\n product.title\n )}\n </h3>\n {typeof product.rating === 'number' && Number.isFinite(product.rating) && (\n <div className='text-muted-foreground mb-1 flex items-center gap-1 text-xs'>\n <Star className='size-3.5 fill-current' aria-hidden='true' />\n <span>{product.rating.toFixed(1)}</span>\n </div>\n )}\n <div className='mt-auto flex items-baseline gap-2'>\n <p className='font-semibold'>\n {formatMoney(product.price) ?? unavailablePriceLabel}\n </p>\n {compareAt && (\n <p className='text-muted-foreground text-sm line-through'>{compareAt}</p>\n )}\n </div>\n </div>\n </CardContent>\n\n <CardFooter className='bg-transparent border-t-0 px-3 pt-0 md:px-4'>\n <Button\n variant='outline'\n size='sm'\n className='h-8 w-full cursor-pointer px-3 text-sm'\n onClick={(event) => {\n event.stopPropagation()\n onAddToCart?.(product)\n }}\n >\n {addToCartLabel}\n </Button>\n </CardFooter>\n </Card>\n )\n })}\n </motion.div>\n </div>\n\n {pageCount > 1 && (\n <div className='mt-6 flex items-center justify-center gap-3'>\n <Button\n type='button'\n variant='outline'\n size='icon-sm'\n className='cursor-pointer'\n aria-label={prevLabel}\n disabled={safePage === 0}\n onClick={() => goTo(safePage - 1)}\n >\n <ChevronLeft className='size-4' aria-hidden='true' />\n </Button>\n\n <div className='flex items-center gap-2'>\n {pages.map((_, index) => (\n <button\n key={index}\n type='button'\n aria-label={`${pageLabel} ${index + 1}`}\n aria-current={index === safePage ? 'true' : undefined}\n className={`size-2 rounded-full transition-colors ${\n index === safePage ? 'bg-primary' : 'bg-muted-foreground/40'\n }`}\n onClick={() => goTo(index)}\n />\n ))}\n </div>\n\n <Button\n type='button'\n variant='outline'\n size='icon-sm'\n className='cursor-pointer'\n aria-label={nextLabel}\n disabled={safePage === pageCount - 1}\n onClick={() => goTo(safePage + 1)}\n >\n <ChevronRight className='size-4' aria-hidden='true' />\n </Button>\n </div>\n )}\n\n <p aria-live='polite' className='sr-only'>\n {pageLabel} {safePage + 1} / {pageCount}\n </p>\n </div>\n </section>\n )\n}\n\nexport default ProductShowcase1\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"product-quick-view-dialog.d.ts","sourceRoot":"","sources":["../../../../src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"product-quick-view-dialog.d.ts","sourceRoot":"","sources":["../../../../src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAyB5D;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,OAAO,EACP,WAAW,GACZ,EAAE;IACD,QAAQ,EAAE,gBAAgB,EAAE,CAAA;IAC5B,kEAAkE;IAClE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,mBAAmB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC5C,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,WAAW,CAAC,EAAE,CACZ,OAAO,EAAE,gBAAgB,EACzB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC9B,IAAI,CAAA;CACV,sCAmUA;AAED,eAAe,sBAAsB,CAAA"}
|
|
@@ -33,6 +33,7 @@ var import_button_group = require("../../../ui/button-group.js");
|
|
|
33
33
|
var import_dialog = require("../../../ui/dialog.js");
|
|
34
34
|
var import_separator = require("../../../ui/separator.js");
|
|
35
35
|
var import_toggle_group = require("../../../ui/toggle-group.js");
|
|
36
|
+
var import_motion = require("../../../lib/motion.js");
|
|
36
37
|
const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 });
|
|
37
38
|
const confirmationMs = 2400;
|
|
38
39
|
function defaultSelection(product) {
|
|
@@ -100,7 +101,10 @@ function ProductQuickViewDialog({
|
|
|
100
101
|
initial: reduceMotion ? false : { opacity: 0 },
|
|
101
102
|
animate: { opacity: 1 },
|
|
102
103
|
exit: reduceMotion ? void 0 : { opacity: 0 },
|
|
103
|
-
transition: {
|
|
104
|
+
transition: {
|
|
105
|
+
duration: import_motion.MOTION.durationSeconds.state,
|
|
106
|
+
ease: import_motion.MOTION.easing.standard
|
|
107
|
+
},
|
|
104
108
|
decoding: "async"
|
|
105
109
|
},
|
|
106
110
|
product.id
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx"],"sourcesContent":["'use client'\n\nimport { useEffect, useMemo, useState } from 'react'\nimport { AnimatePresence, motion, useReducedMotion } from 'motion/react'\nimport { Check, Minus, Plus, Star } from 'lucide-react'\n\nimport { Badge } from '../../../ui/badge'\nimport { Button } from '../../../ui/button'\nimport { ButtonGroup, ButtonGroupText } from '../../../ui/button-group'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '../../../ui/dialog'\nimport { Separator } from '../../../ui/separator'\nimport { ToggleGroup, ToggleGroupItem } from '../../../ui/toggle-group'\n\nimport type { QuickViewProduct } from './product-quick-view'\n\nconst money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 })\n\n/** How long the added-to-cart confirmation stays up before the button returns to its label. */\nconst confirmationMs = 2400\n\n/**\n * What a product opens on: its stated default where it has one, and otherwise the first option\n * that is actually in stock — never a sold-out one, which would put the panel in a state whose\n * add-to-cart button cannot honestly be pressed.\n */\nfunction defaultSelection(product: QuickViewProduct): Record<string, string> {\n return Object.fromEntries(\n product.variants.map(variant => {\n const stated = variant.options.find(\n option => option.value === variant.defaultValue && option.available\n )\n const fallback = variant.options.find(option => option.available) ?? variant.options[0]\n\n return [variant.key, (stated ?? fallback).value]\n })\n )\n}\n\n/**\n * The panel a quick view opens, and the reason the pattern exists.\n *\n * A quick view is not a small product page. It is a promise that the reader will not lose the\n * grid, so this one keeps the whole catalogue reachable from inside: the strip under the picture\n * is the other products, not other angles of this one. Choosing from it re-dresses the panel\n * around a new product, the grid behind marks where the reader has got to, and closing returns\n * them to a grid they never actually left.\n */\nexport function ProductQuickViewDialog({\n products,\n activeIndex,\n onActiveIndexChange,\n onClose,\n onAddToCart,\n}: {\n products: QuickViewProduct[]\n /** Which product is open, or `null` when the dialog is closed. */\n activeIndex: number | null\n onActiveIndexChange: (index: number) => void\n onClose: () => void\n onAddToCart?: (\n product: QuickViewProduct,\n quantity: number,\n selection: Record<string, string>\n ) => void\n}) {\n const reduceMotion = useReducedMotion()\n const product = activeIndex === null ? null : products[activeIndex]\n\n const [selection, setSelection] = useState<Record<string, string>>({})\n const [quantity, setQuantity] = useState(1)\n const [addedCount, setAddedCount] = useState(0)\n\n /*\n One effect keyed on the product's identity rather than three scattered resets. Carrying a\n \"brushed brass\" across from a pendant to a sofa is worse than starting again, and a quantity\n of four floor lamps should not become four sofas because the reader stepped sideways.\n */\n useEffect(() => {\n if (!product) {\n return\n }\n\n setSelection(defaultSelection(product))\n setQuantity(1)\n setAddedCount(0)\n }, [product?.id]) // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n if (addedCount === 0) {\n return\n }\n\n const timer = setTimeout(() => setAddedCount(0), confirmationMs)\n\n return () => clearTimeout(timer)\n }, [addedCount])\n\n /*\n The chosen options in words, for the live region and the stock line. Built from the variant\n order rather than from the selection object, so it reads the way the controls are stacked\n instead of in whatever order the keys happen to sit in.\n */\n const selectionSummary = useMemo(() => {\n if (!product) {\n return ''\n }\n\n return product.variants\n .map(variant => variant.options.find(option => option.value === selection[variant.key])?.label)\n .filter(Boolean)\n .join(' · ')\n }, [product, selection])\n\n if (!product || activeIndex === null) {\n return null\n }\n\n const discount = product.compareAt ? Math.round((1 - product.price / product.compareAt) * 100) : null\n\n return (\n <Dialog open onOpenChange={next => !next && onClose()}>\n {/*\n `max-h` plus `overflow-y-auto` is what makes this usable below a laptop. The popup is\n fixed and centred with a translate, so without a bound it grew past both edges of a phone\n and the add-to-cart button ended up off the bottom of the screen with no way to reach it\n — the page behind a fixed element cannot scroll to it either.\n */}\n <DialogContent className='max-h-[92dvh] w-full overflow-y-auto p-0 sm:max-w-4xl md:h-[38rem] md:max-h-[92dvh] md:overflow-hidden'>\n <DialogHeader className='sr-only'>\n <DialogTitle>{product.name}</DialogTitle>\n <DialogDescription>{product.description}</DialogDescription>\n </DialogHeader>\n\n {/*\n A fixed height from `md`, and the detail column scrolls inside it.\n\n The popup is centred with a -50% translate, so its height decides where its top edge\n lands. Every product carries a different length of description and a different number\n of variant rows, so stepping through the strip re-measured the panel and slid the whole\n dialog up and down under the reader's cursor - the thumbnail they wanted next was no\n longer where they were pointing. Holding the height still makes the strip a control\n rather than a moving target. Below `md` the columns stack and the whole popup scrolls,\n where a fixed height would be the wrong answer.\n */}\n <div className='grid gap-0 md:h-full md:min-h-0 md:grid-cols-[minmax(0,0.95fr)_minmax(0,1fr)]'>\n <div className='bg-muted/40 flex flex-col gap-3 p-4 sm:p-5 md:min-h-0 md:justify-center md:overflow-y-auto'>\n {/*\n The picture cross-fades rather than cutting. At this size a hard swap reads as a\n flicker, and the fade is what ties the thumbnail that was pressed to the picture\n that arrives.\n */}\n <div className='bg-muted relative aspect-square w-full overflow-hidden rounded-lg'>\n <AnimatePresence initial={false} mode='popLayout'>\n <motion.img\n key={product.id}\n src={product.image || undefined}\n alt={product.imageAlt}\n className='absolute inset-0 size-full object-cover'\n initial={reduceMotion ? false : { opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={reduceMotion ? undefined : { opacity: 0 }}\n transition={{ duration: 0.24, ease: 'easeOut' }}\n decoding='async'\n />\n </AnimatePresence>\n\n <Badge variant='secondary' className='absolute top-3 left-3 rounded-full'>\n {product.category}\n </Badge>\n </div>\n\n {/*\n `spacing={2}` rather than a `gap-2` class. A ToggleGroup at the default spacing of\n zero is a segmented control: it strips the item radii, collapses the shared borders\n and adds `px-2` to every item through a `group-data-[spacing=0]` selector that no\n plain `p-0` utility can outrank. That padding around a small thumbnail was the odd\n width and the odd gaps; asking for real spacing turns the whole rule off.\n\n A ToggleGroup rather than a row of buttons for the same reason it is used for the\n variants: single selection, roving focus and arrow-key movement, none of which a\n loop of buttons gets without being told.\n */}\n <ToggleGroup\n type='multiple'\n spacing={2}\n value={[String(activeIndex)]}\n onValueChange={value => value[0] && onActiveIndexChange(Number(value[0]))}\n aria-label='Elegir producto'\n className='grid w-full grid-cols-4'\n >\n {products.map((entry, index) => (\n <ToggleGroupItem\n key={entry.id}\n value={String(index)}\n aria-label={entry.name}\n className='data-[state=on]:ring-primary size-auto min-w-0 overflow-hidden rounded-md p-0 ring-2 ring-transparent transition-shadow data-[state=on]:bg-transparent'\n >\n <img\n src={entry.image || undefined}\n alt=''\n className='bg-muted aspect-square w-full object-cover'\n loading='lazy'\n decoding='async'\n />\n </ToggleGroupItem>\n ))}\n </ToggleGroup>\n\n <p className='text-muted-foreground text-center font-mono text-xs tabular-nums'>\n {`${activeIndex + 1} / ${products.length}`}\n </p>\n </div>\n\n <div className='flex flex-col p-4 pt-5 sm:p-6 md:min-h-0 md:overflow-y-auto'>\n <p className='text-muted-foreground text-xs tracking-[0.16em] uppercase'>{product.tagline}</p>\n\n <h2 className='mt-2 text-2xl font-semibold tracking-tight text-balance'>{product.name}</h2>\n\n <div className='mt-3 flex flex-wrap items-center gap-x-3 gap-y-2'>\n <p className='text-xl font-semibold tabular-nums'>{money.format(product.price)}</p>\n {product.compareAt ? (\n <p className='text-muted-foreground text-sm line-through tabular-nums'>\n {money.format(product.compareAt)}\n </p>\n ) : null}\n {discount ? (\n <Badge variant='secondary' className='rounded-full font-normal'>{`${discount}% de descuento`}</Badge>\n ) : null}\n <p className='text-muted-foreground flex items-center gap-1.5 text-sm'>\n {/*\n Rating stars use the foreground token so they inherit theme contrast,\n matching storefront-hero-1 and the canonical review card. The rating number sits\n right beside the star, so the star is never the only carrier.\n */}\n <Star className='size-3.5 fill-foreground text-foreground' aria-hidden='true' />\n <span className='text-foreground font-medium tabular-nums'>{product.rating}</span>\n <span className='tabular-nums'>({product.reviewCount})</span>\n </p>\n </div>\n\n <p className='text-muted-foreground mt-4 text-sm leading-6 text-pretty'>{product.description}</p>\n\n <div className='mt-5 flex flex-col gap-5'>\n {product.variants.map(variant => (\n /*\n A fieldset per variant, and a ToggleGroup inside it rather than a row of\n Buttons with a hand-rolled `aria-pressed`. A loop of buttons has to fake single\n selection and roving focus, and gets the focus order wrong the moment one of\n them is disabled — which, with a sold-out finish, is most of them.\n */\n <fieldset key={variant.key}>\n <legend className='flex w-full items-baseline gap-2 text-sm font-medium'>\n {variant.label}\n <span className='text-muted-foreground font-normal'>\n {variant.options.find(option => option.value === selection[variant.key])?.label}\n </span>\n </legend>\n\n <ToggleGroup\n type='multiple'\n spacing={2}\n variant='outline'\n value={[selection[variant.key] ?? '']}\n onValueChange={value =>\n value[0] && setSelection(current => ({ ...current, [variant.key]: value[0] }))\n }\n aria-label={variant.label}\n className='mt-2 flex-wrap'\n >\n {variant.options.map(option => (\n <ToggleGroupItem\n key={option.value}\n value={option.value}\n disabled={!option.available}\n // Sold out is a word, not just a dimmed control.\n aria-label={option.available ? option.label : `${option.label}, agotado`}\n className='data-[state=on]:border-primary data-[state=on]:ring-primary/30 gap-2 data-[state=on]:ring-2'\n >\n {option.swatch ? (\n <span\n aria-hidden='true'\n className='ring-border size-3 rounded-full ring-1'\n style={{ backgroundColor: option.swatch }}\n />\n ) : null}\n {option.label}\n </ToggleGroupItem>\n ))}\n </ToggleGroup>\n </fieldset>\n ))}\n </div>\n\n <div className='mt-6 flex flex-wrap items-center gap-3'>\n {/*\n A ButtonGroup, so the stepper's shared border, its corner radii and its focus\n stacking come from the component rather than from `rounded-r-none` by hand.\n\n The readout is a `ButtonGroupText`, not a bare div. The group styles its children\n through `[&>[data-slot]…]` selectors, so an element without a `data-slot` is\n invisible to every one of them: it kept its own radius, lost no border where the\n group expected a seam, and threw off which child counted as last - which is the\n detached middle box with a stray rule under it.\n */}\n <ButtonGroup aria-label='Cantidad'>\n <Button\n variant='outline'\n size='icon'\n aria-label='Reducir cantidad'\n disabled={quantity <= 1}\n onClick={() => setQuantity(current => Math.max(1, current - 1))}\n >\n <Minus />\n </Button>\n {/*\n `aria-live` rather than a label: the number changes under the reader's own\n button presses, and announcing it is the only way they hear the result.\n */}\n <ButtonGroupText aria-live='polite' className='bg-background min-w-10 justify-center tabular-nums'>\n {quantity}\n </ButtonGroupText>\n <Button\n variant='outline'\n size='icon'\n aria-label='Aumentar cantidad'\n onClick={() => setQuantity(current => current + 1)}\n >\n <Plus />\n </Button>\n </ButtonGroup>\n\n <Button\n className='flex-1'\n onClick={() => {\n setAddedCount(quantity)\n onAddToCart?.(product, quantity, selection)\n }}\n >\n {addedCount > 0 ? (\n <>\n <Check aria-hidden='true' />\n Añadido al carrito\n </>\n ) : (\n `Añadir al carrito · ${money.format(product.price * quantity)}`\n )}\n </Button>\n </div>\n\n {product.unitsLeft ? (\n <p className='text-muted-foreground mt-3 text-xs'>\n {`Solo quedan ${product.unitsLeft} en ${selectionSummary || 'esta opción'}.`}\n </p>\n ) : null}\n\n {/* The one announcement, off-screen, that the visible confirmation stands in for. */}\n <p role='status' aria-live='polite' className='sr-only'>\n {addedCount > 0 ? `${addedCount} × ${product.name}, ${selectionSummary}, añadido a tu carrito` : ''}\n </p>\n\n <Separator className='mt-6' />\n\n <ul className='text-muted-foreground mt-4 grid gap-1.5 text-sm sm:grid-cols-2'>\n {product.details.map(detail => (\n <li key={detail} className='flex items-start gap-2'>\n <Check className='text-primary mt-0.5 size-3.5 shrink-0' aria-hidden='true' />\n {detail}\n </li>\n ))}\n </ul>\n\n {product.href ? (\n <a\n href={product.href}\n className='text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 mt-5 w-fit rounded-sm text-sm underline underline-offset-4 outline-none focus-visible:ring-[3px]'\n >\n Ver la página completa del producto\n </a>\n ) : null}\n </div>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n\nexport default ProductQuickViewDialog"],"mappings":"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","names":["import_react"]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx"],"sourcesContent":["'use client'\n\nimport { useEffect, useMemo, useState } from 'react'\nimport { AnimatePresence, motion, useReducedMotion } from 'motion/react'\nimport { Check, Minus, Plus, Star } from 'lucide-react'\n\nimport { Badge } from '../../../ui/badge'\nimport { Button } from '../../../ui/button'\nimport { ButtonGroup, ButtonGroupText } from '../../../ui/button-group'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '../../../ui/dialog'\nimport { Separator } from '../../../ui/separator'\nimport { ToggleGroup, ToggleGroupItem } from '../../../ui/toggle-group'\nimport { MOTION } from '../../../lib/motion'\n\nimport type { QuickViewProduct } from './product-quick-view'\n\nconst money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 })\n\n/** How long the added-to-cart confirmation stays up before the button returns to its label. */\nconst confirmationMs = 2400\n\n/**\n * What a product opens on: its stated default where it has one, and otherwise the first option\n * that is actually in stock — never a sold-out one, which would put the panel in a state whose\n * add-to-cart button cannot honestly be pressed.\n */\nfunction defaultSelection(product: QuickViewProduct): Record<string, string> {\n return Object.fromEntries(\n product.variants.map(variant => {\n const stated = variant.options.find(\n option => option.value === variant.defaultValue && option.available\n )\n const fallback = variant.options.find(option => option.available) ?? variant.options[0]\n\n return [variant.key, (stated ?? fallback).value]\n })\n )\n}\n\n/**\n * The panel a quick view opens, and the reason the pattern exists.\n *\n * A quick view is not a small product page. It is a promise that the reader will not lose the\n * grid, so this one keeps the whole catalogue reachable from inside: the strip under the picture\n * is the other products, not other angles of this one. Choosing from it re-dresses the panel\n * around a new product, the grid behind marks where the reader has got to, and closing returns\n * them to a grid they never actually left.\n */\nexport function ProductQuickViewDialog({\n products,\n activeIndex,\n onActiveIndexChange,\n onClose,\n onAddToCart,\n}: {\n products: QuickViewProduct[]\n /** Which product is open, or `null` when the dialog is closed. */\n activeIndex: number | null\n onActiveIndexChange: (index: number) => void\n onClose: () => void\n onAddToCart?: (\n product: QuickViewProduct,\n quantity: number,\n selection: Record<string, string>\n ) => void\n}) {\n const reduceMotion = useReducedMotion()\n const product = activeIndex === null ? null : products[activeIndex]\n\n const [selection, setSelection] = useState<Record<string, string>>({})\n const [quantity, setQuantity] = useState(1)\n const [addedCount, setAddedCount] = useState(0)\n\n /*\n One effect keyed on the product's identity rather than three scattered resets. Carrying a\n \"brushed brass\" across from a pendant to a sofa is worse than starting again, and a quantity\n of four floor lamps should not become four sofas because the reader stepped sideways.\n */\n useEffect(() => {\n if (!product) {\n return\n }\n\n setSelection(defaultSelection(product))\n setQuantity(1)\n setAddedCount(0)\n }, [product?.id]) // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n if (addedCount === 0) {\n return\n }\n\n const timer = setTimeout(() => setAddedCount(0), confirmationMs)\n\n return () => clearTimeout(timer)\n }, [addedCount])\n\n /*\n The chosen options in words, for the live region and the stock line. Built from the variant\n order rather than from the selection object, so it reads the way the controls are stacked\n instead of in whatever order the keys happen to sit in.\n */\n const selectionSummary = useMemo(() => {\n if (!product) {\n return ''\n }\n\n return product.variants\n .map(variant => variant.options.find(option => option.value === selection[variant.key])?.label)\n .filter(Boolean)\n .join(' · ')\n }, [product, selection])\n\n if (!product || activeIndex === null) {\n return null\n }\n\n const discount = product.compareAt ? Math.round((1 - product.price / product.compareAt) * 100) : null\n\n return (\n <Dialog open onOpenChange={next => !next && onClose()}>\n {/*\n `max-h` plus `overflow-y-auto` is what makes this usable below a laptop. The popup is\n fixed and centred with a translate, so without a bound it grew past both edges of a phone\n and the add-to-cart button ended up off the bottom of the screen with no way to reach it\n — the page behind a fixed element cannot scroll to it either.\n */}\n <DialogContent className='max-h-[92dvh] w-full overflow-y-auto p-0 sm:max-w-4xl md:h-[38rem] md:max-h-[92dvh] md:overflow-hidden'>\n <DialogHeader className='sr-only'>\n <DialogTitle>{product.name}</DialogTitle>\n <DialogDescription>{product.description}</DialogDescription>\n </DialogHeader>\n\n {/*\n A fixed height from `md`, and the detail column scrolls inside it.\n\n The popup is centred with a -50% translate, so its height decides where its top edge\n lands. Every product carries a different length of description and a different number\n of variant rows, so stepping through the strip re-measured the panel and slid the whole\n dialog up and down under the reader's cursor - the thumbnail they wanted next was no\n longer where they were pointing. Holding the height still makes the strip a control\n rather than a moving target. Below `md` the columns stack and the whole popup scrolls,\n where a fixed height would be the wrong answer.\n */}\n <div className='grid gap-0 md:h-full md:min-h-0 md:grid-cols-[minmax(0,0.95fr)_minmax(0,1fr)]'>\n <div className='bg-muted/40 flex flex-col gap-3 p-4 sm:p-5 md:min-h-0 md:justify-center md:overflow-y-auto'>\n {/*\n The picture cross-fades rather than cutting. At this size a hard swap reads as a\n flicker, and the fade is what ties the thumbnail that was pressed to the picture\n that arrives.\n */}\n <div className='bg-muted relative aspect-square w-full overflow-hidden rounded-lg'>\n <AnimatePresence initial={false} mode='popLayout'>\n <motion.img\n key={product.id}\n src={product.image || undefined}\n alt={product.imageAlt}\n className='absolute inset-0 size-full object-cover'\n initial={reduceMotion ? false : { opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={reduceMotion ? undefined : { opacity: 0 }}\n transition={{\n duration: MOTION.durationSeconds.state,\n ease: MOTION.easing.standard,\n }}\n decoding='async'\n />\n </AnimatePresence>\n\n <Badge variant='secondary' className='absolute top-3 left-3 rounded-full'>\n {product.category}\n </Badge>\n </div>\n\n {/*\n `spacing={2}` rather than a `gap-2` class. A ToggleGroup at the default spacing of\n zero is a segmented control: it strips the item radii, collapses the shared borders\n and adds `px-2` to every item through a `group-data-[spacing=0]` selector that no\n plain `p-0` utility can outrank. That padding around a small thumbnail was the odd\n width and the odd gaps; asking for real spacing turns the whole rule off.\n\n A ToggleGroup rather than a row of buttons for the same reason it is used for the\n variants: single selection, roving focus and arrow-key movement, none of which a\n loop of buttons gets without being told.\n */}\n <ToggleGroup\n type='multiple'\n spacing={2}\n value={[String(activeIndex)]}\n onValueChange={value => value[0] && onActiveIndexChange(Number(value[0]))}\n aria-label='Elegir producto'\n className='grid w-full grid-cols-4'\n >\n {products.map((entry, index) => (\n <ToggleGroupItem\n key={entry.id}\n value={String(index)}\n aria-label={entry.name}\n className='data-[state=on]:ring-primary size-auto min-w-0 overflow-hidden rounded-md p-0 ring-2 ring-transparent transition-shadow data-[state=on]:bg-transparent'\n >\n <img\n src={entry.image || undefined}\n alt=''\n className='bg-muted aspect-square w-full object-cover'\n loading='lazy'\n decoding='async'\n />\n </ToggleGroupItem>\n ))}\n </ToggleGroup>\n\n <p className='text-muted-foreground text-center font-mono text-xs tabular-nums'>\n {`${activeIndex + 1} / ${products.length}`}\n </p>\n </div>\n\n <div className='flex flex-col p-4 pt-5 sm:p-6 md:min-h-0 md:overflow-y-auto'>\n <p className='text-muted-foreground text-xs tracking-[0.16em] uppercase'>{product.tagline}</p>\n\n <h2 className='mt-2 text-2xl font-semibold tracking-tight text-balance'>{product.name}</h2>\n\n <div className='mt-3 flex flex-wrap items-center gap-x-3 gap-y-2'>\n <p className='text-xl font-semibold tabular-nums'>{money.format(product.price)}</p>\n {product.compareAt ? (\n <p className='text-muted-foreground text-sm line-through tabular-nums'>\n {money.format(product.compareAt)}\n </p>\n ) : null}\n {discount ? (\n <Badge variant='secondary' className='rounded-full font-normal'>{`${discount}% de descuento`}</Badge>\n ) : null}\n <p className='text-muted-foreground flex items-center gap-1.5 text-sm'>\n {/*\n Rating stars use the foreground token so they inherit theme contrast,\n matching storefront-hero-1 and the canonical review card. The rating number sits\n right beside the star, so the star is never the only carrier.\n */}\n <Star className='size-3.5 fill-foreground text-foreground' aria-hidden='true' />\n <span className='text-foreground font-medium tabular-nums'>{product.rating}</span>\n <span className='tabular-nums'>({product.reviewCount})</span>\n </p>\n </div>\n\n <p className='text-muted-foreground mt-4 text-sm leading-6 text-pretty'>{product.description}</p>\n\n <div className='mt-5 flex flex-col gap-5'>\n {product.variants.map(variant => (\n /*\n A fieldset per variant, and a ToggleGroup inside it rather than a row of\n Buttons with a hand-rolled `aria-pressed`. A loop of buttons has to fake single\n selection and roving focus, and gets the focus order wrong the moment one of\n them is disabled — which, with a sold-out finish, is most of them.\n */\n <fieldset key={variant.key}>\n <legend className='flex w-full items-baseline gap-2 text-sm font-medium'>\n {variant.label}\n <span className='text-muted-foreground font-normal'>\n {variant.options.find(option => option.value === selection[variant.key])?.label}\n </span>\n </legend>\n\n <ToggleGroup\n type='multiple'\n spacing={2}\n variant='outline'\n value={[selection[variant.key] ?? '']}\n onValueChange={value =>\n value[0] && setSelection(current => ({ ...current, [variant.key]: value[0] }))\n }\n aria-label={variant.label}\n className='mt-2 flex-wrap'\n >\n {variant.options.map(option => (\n <ToggleGroupItem\n key={option.value}\n value={option.value}\n disabled={!option.available}\n // Sold out is a word, not just a dimmed control.\n aria-label={option.available ? option.label : `${option.label}, agotado`}\n className='data-[state=on]:border-primary data-[state=on]:ring-primary/30 gap-2 data-[state=on]:ring-2'\n >\n {option.swatch ? (\n <span\n aria-hidden='true'\n className='ring-border size-3 rounded-full ring-1'\n style={{ backgroundColor: option.swatch }}\n />\n ) : null}\n {option.label}\n </ToggleGroupItem>\n ))}\n </ToggleGroup>\n </fieldset>\n ))}\n </div>\n\n <div className='mt-6 flex flex-wrap items-center gap-3'>\n {/*\n A ButtonGroup, so the stepper's shared border, its corner radii and its focus\n stacking come from the component rather than from `rounded-r-none` by hand.\n\n The readout is a `ButtonGroupText`, not a bare div. The group styles its children\n through `[&>[data-slot]…]` selectors, so an element without a `data-slot` is\n invisible to every one of them: it kept its own radius, lost no border where the\n group expected a seam, and threw off which child counted as last - which is the\n detached middle box with a stray rule under it.\n */}\n <ButtonGroup aria-label='Cantidad'>\n <Button\n variant='outline'\n size='icon'\n aria-label='Reducir cantidad'\n disabled={quantity <= 1}\n onClick={() => setQuantity(current => Math.max(1, current - 1))}\n >\n <Minus />\n </Button>\n {/*\n `aria-live` rather than a label: the number changes under the reader's own\n button presses, and announcing it is the only way they hear the result.\n */}\n <ButtonGroupText aria-live='polite' className='bg-background min-w-10 justify-center tabular-nums'>\n {quantity}\n </ButtonGroupText>\n <Button\n variant='outline'\n size='icon'\n aria-label='Aumentar cantidad'\n onClick={() => setQuantity(current => current + 1)}\n >\n <Plus />\n </Button>\n </ButtonGroup>\n\n <Button\n className='flex-1'\n onClick={() => {\n setAddedCount(quantity)\n onAddToCart?.(product, quantity, selection)\n }}\n >\n {addedCount > 0 ? (\n <>\n <Check aria-hidden='true' />\n Añadido al carrito\n </>\n ) : (\n `Añadir al carrito · ${money.format(product.price * quantity)}`\n )}\n </Button>\n </div>\n\n {product.unitsLeft ? (\n <p className='text-muted-foreground mt-3 text-xs'>\n {`Solo quedan ${product.unitsLeft} en ${selectionSummary || 'esta opción'}.`}\n </p>\n ) : null}\n\n {/* The one announcement, off-screen, that the visible confirmation stands in for. */}\n <p role='status' aria-live='polite' className='sr-only'>\n {addedCount > 0 ? `${addedCount} × ${product.name}, ${selectionSummary}, añadido a tu carrito` : ''}\n </p>\n\n <Separator className='mt-6' />\n\n <ul className='text-muted-foreground mt-4 grid gap-1.5 text-sm sm:grid-cols-2'>\n {product.details.map(detail => (\n <li key={detail} className='flex items-start gap-2'>\n <Check className='text-primary mt-0.5 size-3.5 shrink-0' aria-hidden='true' />\n {detail}\n </li>\n ))}\n </ul>\n\n {product.href ? (\n <a\n href={product.href}\n className='text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 mt-5 w-fit rounded-sm text-sm underline underline-offset-4 outline-none focus-visible:ring-[3px]'\n >\n Ver la página completa del producto\n </a>\n ) : null}\n </div>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n\nexport default ProductQuickViewDialog\n"],"mappings":"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","names":["import_react"]}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "../../../ui/dialog.mjs";
|
|
16
16
|
import { Separator } from "../../../ui/separator.mjs";
|
|
17
17
|
import { ToggleGroup, ToggleGroupItem } from "../../../ui/toggle-group.mjs";
|
|
18
|
+
import { MOTION } from "../../../lib/motion.mjs";
|
|
18
19
|
const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 });
|
|
19
20
|
const confirmationMs = 2400;
|
|
20
21
|
function defaultSelection(product) {
|
|
@@ -82,7 +83,10 @@ function ProductQuickViewDialog({
|
|
|
82
83
|
initial: reduceMotion ? false : { opacity: 0 },
|
|
83
84
|
animate: { opacity: 1 },
|
|
84
85
|
exit: reduceMotion ? void 0 : { opacity: 0 },
|
|
85
|
-
transition: {
|
|
86
|
+
transition: {
|
|
87
|
+
duration: MOTION.durationSeconds.state,
|
|
88
|
+
ease: MOTION.easing.standard
|
|
89
|
+
},
|
|
86
90
|
decoding: "async"
|
|
87
91
|
},
|
|
88
92
|
product.id
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx"],"sourcesContent":["'use client'\n\nimport { useEffect, useMemo, useState } from 'react'\nimport { AnimatePresence, motion, useReducedMotion } from 'motion/react'\nimport { Check, Minus, Plus, Star } from 'lucide-react'\n\nimport { Badge } from '../../../ui/badge'\nimport { Button } from '../../../ui/button'\nimport { ButtonGroup, ButtonGroupText } from '../../../ui/button-group'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '../../../ui/dialog'\nimport { Separator } from '../../../ui/separator'\nimport { ToggleGroup, ToggleGroupItem } from '../../../ui/toggle-group'\n\nimport type { QuickViewProduct } from './product-quick-view'\n\nconst money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 })\n\n/** How long the added-to-cart confirmation stays up before the button returns to its label. */\nconst confirmationMs = 2400\n\n/**\n * What a product opens on: its stated default where it has one, and otherwise the first option\n * that is actually in stock — never a sold-out one, which would put the panel in a state whose\n * add-to-cart button cannot honestly be pressed.\n */\nfunction defaultSelection(product: QuickViewProduct): Record<string, string> {\n return Object.fromEntries(\n product.variants.map(variant => {\n const stated = variant.options.find(\n option => option.value === variant.defaultValue && option.available\n )\n const fallback = variant.options.find(option => option.available) ?? variant.options[0]\n\n return [variant.key, (stated ?? fallback).value]\n })\n )\n}\n\n/**\n * The panel a quick view opens, and the reason the pattern exists.\n *\n * A quick view is not a small product page. It is a promise that the reader will not lose the\n * grid, so this one keeps the whole catalogue reachable from inside: the strip under the picture\n * is the other products, not other angles of this one. Choosing from it re-dresses the panel\n * around a new product, the grid behind marks where the reader has got to, and closing returns\n * them to a grid they never actually left.\n */\nexport function ProductQuickViewDialog({\n products,\n activeIndex,\n onActiveIndexChange,\n onClose,\n onAddToCart,\n}: {\n products: QuickViewProduct[]\n /** Which product is open, or `null` when the dialog is closed. */\n activeIndex: number | null\n onActiveIndexChange: (index: number) => void\n onClose: () => void\n onAddToCart?: (\n product: QuickViewProduct,\n quantity: number,\n selection: Record<string, string>\n ) => void\n}) {\n const reduceMotion = useReducedMotion()\n const product = activeIndex === null ? null : products[activeIndex]\n\n const [selection, setSelection] = useState<Record<string, string>>({})\n const [quantity, setQuantity] = useState(1)\n const [addedCount, setAddedCount] = useState(0)\n\n /*\n One effect keyed on the product's identity rather than three scattered resets. Carrying a\n \"brushed brass\" across from a pendant to a sofa is worse than starting again, and a quantity\n of four floor lamps should not become four sofas because the reader stepped sideways.\n */\n useEffect(() => {\n if (!product) {\n return\n }\n\n setSelection(defaultSelection(product))\n setQuantity(1)\n setAddedCount(0)\n }, [product?.id]) // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n if (addedCount === 0) {\n return\n }\n\n const timer = setTimeout(() => setAddedCount(0), confirmationMs)\n\n return () => clearTimeout(timer)\n }, [addedCount])\n\n /*\n The chosen options in words, for the live region and the stock line. Built from the variant\n order rather than from the selection object, so it reads the way the controls are stacked\n instead of in whatever order the keys happen to sit in.\n */\n const selectionSummary = useMemo(() => {\n if (!product) {\n return ''\n }\n\n return product.variants\n .map(variant => variant.options.find(option => option.value === selection[variant.key])?.label)\n .filter(Boolean)\n .join(' · ')\n }, [product, selection])\n\n if (!product || activeIndex === null) {\n return null\n }\n\n const discount = product.compareAt ? Math.round((1 - product.price / product.compareAt) * 100) : null\n\n return (\n <Dialog open onOpenChange={next => !next && onClose()}>\n {/*\n `max-h` plus `overflow-y-auto` is what makes this usable below a laptop. The popup is\n fixed and centred with a translate, so without a bound it grew past both edges of a phone\n and the add-to-cart button ended up off the bottom of the screen with no way to reach it\n — the page behind a fixed element cannot scroll to it either.\n */}\n <DialogContent className='max-h-[92dvh] w-full overflow-y-auto p-0 sm:max-w-4xl md:h-[38rem] md:max-h-[92dvh] md:overflow-hidden'>\n <DialogHeader className='sr-only'>\n <DialogTitle>{product.name}</DialogTitle>\n <DialogDescription>{product.description}</DialogDescription>\n </DialogHeader>\n\n {/*\n A fixed height from `md`, and the detail column scrolls inside it.\n\n The popup is centred with a -50% translate, so its height decides where its top edge\n lands. Every product carries a different length of description and a different number\n of variant rows, so stepping through the strip re-measured the panel and slid the whole\n dialog up and down under the reader's cursor - the thumbnail they wanted next was no\n longer where they were pointing. Holding the height still makes the strip a control\n rather than a moving target. Below `md` the columns stack and the whole popup scrolls,\n where a fixed height would be the wrong answer.\n */}\n <div className='grid gap-0 md:h-full md:min-h-0 md:grid-cols-[minmax(0,0.95fr)_minmax(0,1fr)]'>\n <div className='bg-muted/40 flex flex-col gap-3 p-4 sm:p-5 md:min-h-0 md:justify-center md:overflow-y-auto'>\n {/*\n The picture cross-fades rather than cutting. At this size a hard swap reads as a\n flicker, and the fade is what ties the thumbnail that was pressed to the picture\n that arrives.\n */}\n <div className='bg-muted relative aspect-square w-full overflow-hidden rounded-lg'>\n <AnimatePresence initial={false} mode='popLayout'>\n <motion.img\n key={product.id}\n src={product.image || undefined}\n alt={product.imageAlt}\n className='absolute inset-0 size-full object-cover'\n initial={reduceMotion ? false : { opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={reduceMotion ? undefined : { opacity: 0 }}\n transition={{ duration: 0.24, ease: 'easeOut' }}\n decoding='async'\n />\n </AnimatePresence>\n\n <Badge variant='secondary' className='absolute top-3 left-3 rounded-full'>\n {product.category}\n </Badge>\n </div>\n\n {/*\n `spacing={2}` rather than a `gap-2` class. A ToggleGroup at the default spacing of\n zero is a segmented control: it strips the item radii, collapses the shared borders\n and adds `px-2` to every item through a `group-data-[spacing=0]` selector that no\n plain `p-0` utility can outrank. That padding around a small thumbnail was the odd\n width and the odd gaps; asking for real spacing turns the whole rule off.\n\n A ToggleGroup rather than a row of buttons for the same reason it is used for the\n variants: single selection, roving focus and arrow-key movement, none of which a\n loop of buttons gets without being told.\n */}\n <ToggleGroup\n type='multiple'\n spacing={2}\n value={[String(activeIndex)]}\n onValueChange={value => value[0] && onActiveIndexChange(Number(value[0]))}\n aria-label='Elegir producto'\n className='grid w-full grid-cols-4'\n >\n {products.map((entry, index) => (\n <ToggleGroupItem\n key={entry.id}\n value={String(index)}\n aria-label={entry.name}\n className='data-[state=on]:ring-primary size-auto min-w-0 overflow-hidden rounded-md p-0 ring-2 ring-transparent transition-shadow data-[state=on]:bg-transparent'\n >\n <img\n src={entry.image || undefined}\n alt=''\n className='bg-muted aspect-square w-full object-cover'\n loading='lazy'\n decoding='async'\n />\n </ToggleGroupItem>\n ))}\n </ToggleGroup>\n\n <p className='text-muted-foreground text-center font-mono text-xs tabular-nums'>\n {`${activeIndex + 1} / ${products.length}`}\n </p>\n </div>\n\n <div className='flex flex-col p-4 pt-5 sm:p-6 md:min-h-0 md:overflow-y-auto'>\n <p className='text-muted-foreground text-xs tracking-[0.16em] uppercase'>{product.tagline}</p>\n\n <h2 className='mt-2 text-2xl font-semibold tracking-tight text-balance'>{product.name}</h2>\n\n <div className='mt-3 flex flex-wrap items-center gap-x-3 gap-y-2'>\n <p className='text-xl font-semibold tabular-nums'>{money.format(product.price)}</p>\n {product.compareAt ? (\n <p className='text-muted-foreground text-sm line-through tabular-nums'>\n {money.format(product.compareAt)}\n </p>\n ) : null}\n {discount ? (\n <Badge variant='secondary' className='rounded-full font-normal'>{`${discount}% de descuento`}</Badge>\n ) : null}\n <p className='text-muted-foreground flex items-center gap-1.5 text-sm'>\n {/*\n Rating stars use the foreground token so they inherit theme contrast,\n matching storefront-hero-1 and the canonical review card. The rating number sits\n right beside the star, so the star is never the only carrier.\n */}\n <Star className='size-3.5 fill-foreground text-foreground' aria-hidden='true' />\n <span className='text-foreground font-medium tabular-nums'>{product.rating}</span>\n <span className='tabular-nums'>({product.reviewCount})</span>\n </p>\n </div>\n\n <p className='text-muted-foreground mt-4 text-sm leading-6 text-pretty'>{product.description}</p>\n\n <div className='mt-5 flex flex-col gap-5'>\n {product.variants.map(variant => (\n /*\n A fieldset per variant, and a ToggleGroup inside it rather than a row of\n Buttons with a hand-rolled `aria-pressed`. A loop of buttons has to fake single\n selection and roving focus, and gets the focus order wrong the moment one of\n them is disabled — which, with a sold-out finish, is most of them.\n */\n <fieldset key={variant.key}>\n <legend className='flex w-full items-baseline gap-2 text-sm font-medium'>\n {variant.label}\n <span className='text-muted-foreground font-normal'>\n {variant.options.find(option => option.value === selection[variant.key])?.label}\n </span>\n </legend>\n\n <ToggleGroup\n type='multiple'\n spacing={2}\n variant='outline'\n value={[selection[variant.key] ?? '']}\n onValueChange={value =>\n value[0] && setSelection(current => ({ ...current, [variant.key]: value[0] }))\n }\n aria-label={variant.label}\n className='mt-2 flex-wrap'\n >\n {variant.options.map(option => (\n <ToggleGroupItem\n key={option.value}\n value={option.value}\n disabled={!option.available}\n // Sold out is a word, not just a dimmed control.\n aria-label={option.available ? option.label : `${option.label}, agotado`}\n className='data-[state=on]:border-primary data-[state=on]:ring-primary/30 gap-2 data-[state=on]:ring-2'\n >\n {option.swatch ? (\n <span\n aria-hidden='true'\n className='ring-border size-3 rounded-full ring-1'\n style={{ backgroundColor: option.swatch }}\n />\n ) : null}\n {option.label}\n </ToggleGroupItem>\n ))}\n </ToggleGroup>\n </fieldset>\n ))}\n </div>\n\n <div className='mt-6 flex flex-wrap items-center gap-3'>\n {/*\n A ButtonGroup, so the stepper's shared border, its corner radii and its focus\n stacking come from the component rather than from `rounded-r-none` by hand.\n\n The readout is a `ButtonGroupText`, not a bare div. The group styles its children\n through `[&>[data-slot]…]` selectors, so an element without a `data-slot` is\n invisible to every one of them: it kept its own radius, lost no border where the\n group expected a seam, and threw off which child counted as last - which is the\n detached middle box with a stray rule under it.\n */}\n <ButtonGroup aria-label='Cantidad'>\n <Button\n variant='outline'\n size='icon'\n aria-label='Reducir cantidad'\n disabled={quantity <= 1}\n onClick={() => setQuantity(current => Math.max(1, current - 1))}\n >\n <Minus />\n </Button>\n {/*\n `aria-live` rather than a label: the number changes under the reader's own\n button presses, and announcing it is the only way they hear the result.\n */}\n <ButtonGroupText aria-live='polite' className='bg-background min-w-10 justify-center tabular-nums'>\n {quantity}\n </ButtonGroupText>\n <Button\n variant='outline'\n size='icon'\n aria-label='Aumentar cantidad'\n onClick={() => setQuantity(current => current + 1)}\n >\n <Plus />\n </Button>\n </ButtonGroup>\n\n <Button\n className='flex-1'\n onClick={() => {\n setAddedCount(quantity)\n onAddToCart?.(product, quantity, selection)\n }}\n >\n {addedCount > 0 ? (\n <>\n <Check aria-hidden='true' />\n Añadido al carrito\n </>\n ) : (\n `Añadir al carrito · ${money.format(product.price * quantity)}`\n )}\n </Button>\n </div>\n\n {product.unitsLeft ? (\n <p className='text-muted-foreground mt-3 text-xs'>\n {`Solo quedan ${product.unitsLeft} en ${selectionSummary || 'esta opción'}.`}\n </p>\n ) : null}\n\n {/* The one announcement, off-screen, that the visible confirmation stands in for. */}\n <p role='status' aria-live='polite' className='sr-only'>\n {addedCount > 0 ? `${addedCount} × ${product.name}, ${selectionSummary}, añadido a tu carrito` : ''}\n </p>\n\n <Separator className='mt-6' />\n\n <ul className='text-muted-foreground mt-4 grid gap-1.5 text-sm sm:grid-cols-2'>\n {product.details.map(detail => (\n <li key={detail} className='flex items-start gap-2'>\n <Check className='text-primary mt-0.5 size-3.5 shrink-0' aria-hidden='true' />\n {detail}\n </li>\n ))}\n </ul>\n\n {product.href ? (\n <a\n href={product.href}\n className='text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 mt-5 w-fit rounded-sm text-sm underline underline-offset-4 outline-none focus-visible:ring-[3px]'\n >\n Ver la página completa del producto\n </a>\n ) : null}\n </div>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n\nexport default ProductQuickViewDialog"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx"],"sourcesContent":["'use client'\n\nimport { useEffect, useMemo, useState } from 'react'\nimport { AnimatePresence, motion, useReducedMotion } from 'motion/react'\nimport { Check, Minus, Plus, Star } from 'lucide-react'\n\nimport { Badge } from '../../../ui/badge'\nimport { Button } from '../../../ui/button'\nimport { ButtonGroup, ButtonGroupText } from '../../../ui/button-group'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from '../../../ui/dialog'\nimport { Separator } from '../../../ui/separator'\nimport { ToggleGroup, ToggleGroupItem } from '../../../ui/toggle-group'\nimport { MOTION } from '../../../lib/motion'\n\nimport type { QuickViewProduct } from './product-quick-view'\n\nconst money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 })\n\n/** How long the added-to-cart confirmation stays up before the button returns to its label. */\nconst confirmationMs = 2400\n\n/**\n * What a product opens on: its stated default where it has one, and otherwise the first option\n * that is actually in stock — never a sold-out one, which would put the panel in a state whose\n * add-to-cart button cannot honestly be pressed.\n */\nfunction defaultSelection(product: QuickViewProduct): Record<string, string> {\n return Object.fromEntries(\n product.variants.map(variant => {\n const stated = variant.options.find(\n option => option.value === variant.defaultValue && option.available\n )\n const fallback = variant.options.find(option => option.available) ?? variant.options[0]\n\n return [variant.key, (stated ?? fallback).value]\n })\n )\n}\n\n/**\n * The panel a quick view opens, and the reason the pattern exists.\n *\n * A quick view is not a small product page. It is a promise that the reader will not lose the\n * grid, so this one keeps the whole catalogue reachable from inside: the strip under the picture\n * is the other products, not other angles of this one. Choosing from it re-dresses the panel\n * around a new product, the grid behind marks where the reader has got to, and closing returns\n * them to a grid they never actually left.\n */\nexport function ProductQuickViewDialog({\n products,\n activeIndex,\n onActiveIndexChange,\n onClose,\n onAddToCart,\n}: {\n products: QuickViewProduct[]\n /** Which product is open, or `null` when the dialog is closed. */\n activeIndex: number | null\n onActiveIndexChange: (index: number) => void\n onClose: () => void\n onAddToCart?: (\n product: QuickViewProduct,\n quantity: number,\n selection: Record<string, string>\n ) => void\n}) {\n const reduceMotion = useReducedMotion()\n const product = activeIndex === null ? null : products[activeIndex]\n\n const [selection, setSelection] = useState<Record<string, string>>({})\n const [quantity, setQuantity] = useState(1)\n const [addedCount, setAddedCount] = useState(0)\n\n /*\n One effect keyed on the product's identity rather than three scattered resets. Carrying a\n \"brushed brass\" across from a pendant to a sofa is worse than starting again, and a quantity\n of four floor lamps should not become four sofas because the reader stepped sideways.\n */\n useEffect(() => {\n if (!product) {\n return\n }\n\n setSelection(defaultSelection(product))\n setQuantity(1)\n setAddedCount(0)\n }, [product?.id]) // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => {\n if (addedCount === 0) {\n return\n }\n\n const timer = setTimeout(() => setAddedCount(0), confirmationMs)\n\n return () => clearTimeout(timer)\n }, [addedCount])\n\n /*\n The chosen options in words, for the live region and the stock line. Built from the variant\n order rather than from the selection object, so it reads the way the controls are stacked\n instead of in whatever order the keys happen to sit in.\n */\n const selectionSummary = useMemo(() => {\n if (!product) {\n return ''\n }\n\n return product.variants\n .map(variant => variant.options.find(option => option.value === selection[variant.key])?.label)\n .filter(Boolean)\n .join(' · ')\n }, [product, selection])\n\n if (!product || activeIndex === null) {\n return null\n }\n\n const discount = product.compareAt ? Math.round((1 - product.price / product.compareAt) * 100) : null\n\n return (\n <Dialog open onOpenChange={next => !next && onClose()}>\n {/*\n `max-h` plus `overflow-y-auto` is what makes this usable below a laptop. The popup is\n fixed and centred with a translate, so without a bound it grew past both edges of a phone\n and the add-to-cart button ended up off the bottom of the screen with no way to reach it\n — the page behind a fixed element cannot scroll to it either.\n */}\n <DialogContent className='max-h-[92dvh] w-full overflow-y-auto p-0 sm:max-w-4xl md:h-[38rem] md:max-h-[92dvh] md:overflow-hidden'>\n <DialogHeader className='sr-only'>\n <DialogTitle>{product.name}</DialogTitle>\n <DialogDescription>{product.description}</DialogDescription>\n </DialogHeader>\n\n {/*\n A fixed height from `md`, and the detail column scrolls inside it.\n\n The popup is centred with a -50% translate, so its height decides where its top edge\n lands. Every product carries a different length of description and a different number\n of variant rows, so stepping through the strip re-measured the panel and slid the whole\n dialog up and down under the reader's cursor - the thumbnail they wanted next was no\n longer where they were pointing. Holding the height still makes the strip a control\n rather than a moving target. Below `md` the columns stack and the whole popup scrolls,\n where a fixed height would be the wrong answer.\n */}\n <div className='grid gap-0 md:h-full md:min-h-0 md:grid-cols-[minmax(0,0.95fr)_minmax(0,1fr)]'>\n <div className='bg-muted/40 flex flex-col gap-3 p-4 sm:p-5 md:min-h-0 md:justify-center md:overflow-y-auto'>\n {/*\n The picture cross-fades rather than cutting. At this size a hard swap reads as a\n flicker, and the fade is what ties the thumbnail that was pressed to the picture\n that arrives.\n */}\n <div className='bg-muted relative aspect-square w-full overflow-hidden rounded-lg'>\n <AnimatePresence initial={false} mode='popLayout'>\n <motion.img\n key={product.id}\n src={product.image || undefined}\n alt={product.imageAlt}\n className='absolute inset-0 size-full object-cover'\n initial={reduceMotion ? false : { opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={reduceMotion ? undefined : { opacity: 0 }}\n transition={{\n duration: MOTION.durationSeconds.state,\n ease: MOTION.easing.standard,\n }}\n decoding='async'\n />\n </AnimatePresence>\n\n <Badge variant='secondary' className='absolute top-3 left-3 rounded-full'>\n {product.category}\n </Badge>\n </div>\n\n {/*\n `spacing={2}` rather than a `gap-2` class. A ToggleGroup at the default spacing of\n zero is a segmented control: it strips the item radii, collapses the shared borders\n and adds `px-2` to every item through a `group-data-[spacing=0]` selector that no\n plain `p-0` utility can outrank. That padding around a small thumbnail was the odd\n width and the odd gaps; asking for real spacing turns the whole rule off.\n\n A ToggleGroup rather than a row of buttons for the same reason it is used for the\n variants: single selection, roving focus and arrow-key movement, none of which a\n loop of buttons gets without being told.\n */}\n <ToggleGroup\n type='multiple'\n spacing={2}\n value={[String(activeIndex)]}\n onValueChange={value => value[0] && onActiveIndexChange(Number(value[0]))}\n aria-label='Elegir producto'\n className='grid w-full grid-cols-4'\n >\n {products.map((entry, index) => (\n <ToggleGroupItem\n key={entry.id}\n value={String(index)}\n aria-label={entry.name}\n className='data-[state=on]:ring-primary size-auto min-w-0 overflow-hidden rounded-md p-0 ring-2 ring-transparent transition-shadow data-[state=on]:bg-transparent'\n >\n <img\n src={entry.image || undefined}\n alt=''\n className='bg-muted aspect-square w-full object-cover'\n loading='lazy'\n decoding='async'\n />\n </ToggleGroupItem>\n ))}\n </ToggleGroup>\n\n <p className='text-muted-foreground text-center font-mono text-xs tabular-nums'>\n {`${activeIndex + 1} / ${products.length}`}\n </p>\n </div>\n\n <div className='flex flex-col p-4 pt-5 sm:p-6 md:min-h-0 md:overflow-y-auto'>\n <p className='text-muted-foreground text-xs tracking-[0.16em] uppercase'>{product.tagline}</p>\n\n <h2 className='mt-2 text-2xl font-semibold tracking-tight text-balance'>{product.name}</h2>\n\n <div className='mt-3 flex flex-wrap items-center gap-x-3 gap-y-2'>\n <p className='text-xl font-semibold tabular-nums'>{money.format(product.price)}</p>\n {product.compareAt ? (\n <p className='text-muted-foreground text-sm line-through tabular-nums'>\n {money.format(product.compareAt)}\n </p>\n ) : null}\n {discount ? (\n <Badge variant='secondary' className='rounded-full font-normal'>{`${discount}% de descuento`}</Badge>\n ) : null}\n <p className='text-muted-foreground flex items-center gap-1.5 text-sm'>\n {/*\n Rating stars use the foreground token so they inherit theme contrast,\n matching storefront-hero-1 and the canonical review card. The rating number sits\n right beside the star, so the star is never the only carrier.\n */}\n <Star className='size-3.5 fill-foreground text-foreground' aria-hidden='true' />\n <span className='text-foreground font-medium tabular-nums'>{product.rating}</span>\n <span className='tabular-nums'>({product.reviewCount})</span>\n </p>\n </div>\n\n <p className='text-muted-foreground mt-4 text-sm leading-6 text-pretty'>{product.description}</p>\n\n <div className='mt-5 flex flex-col gap-5'>\n {product.variants.map(variant => (\n /*\n A fieldset per variant, and a ToggleGroup inside it rather than a row of\n Buttons with a hand-rolled `aria-pressed`. A loop of buttons has to fake single\n selection and roving focus, and gets the focus order wrong the moment one of\n them is disabled — which, with a sold-out finish, is most of them.\n */\n <fieldset key={variant.key}>\n <legend className='flex w-full items-baseline gap-2 text-sm font-medium'>\n {variant.label}\n <span className='text-muted-foreground font-normal'>\n {variant.options.find(option => option.value === selection[variant.key])?.label}\n </span>\n </legend>\n\n <ToggleGroup\n type='multiple'\n spacing={2}\n variant='outline'\n value={[selection[variant.key] ?? '']}\n onValueChange={value =>\n value[0] && setSelection(current => ({ ...current, [variant.key]: value[0] }))\n }\n aria-label={variant.label}\n className='mt-2 flex-wrap'\n >\n {variant.options.map(option => (\n <ToggleGroupItem\n key={option.value}\n value={option.value}\n disabled={!option.available}\n // Sold out is a word, not just a dimmed control.\n aria-label={option.available ? option.label : `${option.label}, agotado`}\n className='data-[state=on]:border-primary data-[state=on]:ring-primary/30 gap-2 data-[state=on]:ring-2'\n >\n {option.swatch ? (\n <span\n aria-hidden='true'\n className='ring-border size-3 rounded-full ring-1'\n style={{ backgroundColor: option.swatch }}\n />\n ) : null}\n {option.label}\n </ToggleGroupItem>\n ))}\n </ToggleGroup>\n </fieldset>\n ))}\n </div>\n\n <div className='mt-6 flex flex-wrap items-center gap-3'>\n {/*\n A ButtonGroup, so the stepper's shared border, its corner radii and its focus\n stacking come from the component rather than from `rounded-r-none` by hand.\n\n The readout is a `ButtonGroupText`, not a bare div. The group styles its children\n through `[&>[data-slot]…]` selectors, so an element without a `data-slot` is\n invisible to every one of them: it kept its own radius, lost no border where the\n group expected a seam, and threw off which child counted as last - which is the\n detached middle box with a stray rule under it.\n */}\n <ButtonGroup aria-label='Cantidad'>\n <Button\n variant='outline'\n size='icon'\n aria-label='Reducir cantidad'\n disabled={quantity <= 1}\n onClick={() => setQuantity(current => Math.max(1, current - 1))}\n >\n <Minus />\n </Button>\n {/*\n `aria-live` rather than a label: the number changes under the reader's own\n button presses, and announcing it is the only way they hear the result.\n */}\n <ButtonGroupText aria-live='polite' className='bg-background min-w-10 justify-center tabular-nums'>\n {quantity}\n </ButtonGroupText>\n <Button\n variant='outline'\n size='icon'\n aria-label='Aumentar cantidad'\n onClick={() => setQuantity(current => current + 1)}\n >\n <Plus />\n </Button>\n </ButtonGroup>\n\n <Button\n className='flex-1'\n onClick={() => {\n setAddedCount(quantity)\n onAddToCart?.(product, quantity, selection)\n }}\n >\n {addedCount > 0 ? (\n <>\n <Check aria-hidden='true' />\n Añadido al carrito\n </>\n ) : (\n `Añadir al carrito · ${money.format(product.price * quantity)}`\n )}\n </Button>\n </div>\n\n {product.unitsLeft ? (\n <p className='text-muted-foreground mt-3 text-xs'>\n {`Solo quedan ${product.unitsLeft} en ${selectionSummary || 'esta opción'}.`}\n </p>\n ) : null}\n\n {/* The one announcement, off-screen, that the visible confirmation stands in for. */}\n <p role='status' aria-live='polite' className='sr-only'>\n {addedCount > 0 ? `${addedCount} × ${product.name}, ${selectionSummary}, añadido a tu carrito` : ''}\n </p>\n\n <Separator className='mt-6' />\n\n <ul className='text-muted-foreground mt-4 grid gap-1.5 text-sm sm:grid-cols-2'>\n {product.details.map(detail => (\n <li key={detail} className='flex items-start gap-2'>\n <Check className='text-primary mt-0.5 size-3.5 shrink-0' aria-hidden='true' />\n {detail}\n </li>\n ))}\n </ul>\n\n {product.href ? (\n <a\n href={product.href}\n className='text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 mt-5 w-fit rounded-sm text-sm underline underline-offset-4 outline-none focus-visible:ring-[3px]'\n >\n Ver la página completa del producto\n </a>\n ) : null}\n </div>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n\nexport default ProductQuickViewDialog\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storefront-hero.d.ts","sourceRoot":"","sources":["../../../../src/blocks/e-commerce/storefront-hero/storefront-hero.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"storefront-hero.d.ts","sourceRoot":"","sources":["../../../../src/blocks/e-commerce/storefront-hero/storefront-hero.tsx"],"names":[],"mappings":"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"}
|
|
@@ -32,6 +32,7 @@ var import_button = require("../../../ui/button.js");
|
|
|
32
32
|
var import_card = require("../../../ui/card.js");
|
|
33
33
|
var import_carousel = require("../../../ui/carousel.js");
|
|
34
34
|
var import_input = require("../../../ui/input.js");
|
|
35
|
+
var import_motion = require("../../../lib/motion.js");
|
|
35
36
|
var import_utils = require("../../../lib/utils.js");
|
|
36
37
|
function hasRealDiscount(item) {
|
|
37
38
|
return typeof item.originalPrice === "number" && Number.isFinite(item.originalPrice) && item.originalPrice > item.price && item.price >= 0;
|
|
@@ -199,9 +200,9 @@ function CompactPresentation({
|
|
|
199
200
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
200
201
|
import_react2.motion.div,
|
|
201
202
|
{
|
|
202
|
-
initial: reduceMotion ? false : { opacity: 0, y:
|
|
203
|
+
initial: reduceMotion ? false : { opacity: 0, y: import_motion.MOTION.distancePx.entrance },
|
|
203
204
|
animate: { opacity: 1, y: 0 },
|
|
204
|
-
transition: { duration:
|
|
205
|
+
transition: { duration: import_motion.MOTION.durationSeconds.entrance, ease: import_motion.MOTION.easing.standard },
|
|
205
206
|
className: "mx-auto max-w-xl text-center lg:mx-0 xl:text-left",
|
|
206
207
|
children: [
|
|
207
208
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(HeroHeading, { title, titleAccent, badge }),
|
|
@@ -233,13 +234,13 @@ function CompactPresentation({
|
|
|
233
234
|
products.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-full max-w-2xl lg:mt-0", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid gap-6 sm:grid-cols-2", children: products.map((product, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
234
235
|
import_react2.motion.div,
|
|
235
236
|
{
|
|
236
|
-
initial: reduceMotion ? false : { opacity: 0, y:
|
|
237
|
+
initial: reduceMotion ? false : { opacity: 0, y: import_motion.MOTION.distancePx.entrance },
|
|
237
238
|
whileInView: { opacity: 1, y: 0 },
|
|
238
239
|
viewport: { once: true, margin: "-40px" },
|
|
239
240
|
transition: {
|
|
240
|
-
duration:
|
|
241
|
-
delay: reduceMotion ? 0 : index
|
|
242
|
-
ease:
|
|
241
|
+
duration: import_motion.MOTION.durationSeconds.entrance,
|
|
242
|
+
delay: reduceMotion ? 0 : import_motion.MOTION.stagger(index),
|
|
243
|
+
ease: import_motion.MOTION.easing.standard
|
|
243
244
|
},
|
|
244
245
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
245
246
|
CompactProductCard,
|
|
@@ -403,9 +404,13 @@ function ShowcasePresentation({
|
|
|
403
404
|
setCurrentSlide(index);
|
|
404
405
|
},
|
|
405
406
|
className: (0, import_utils.cn)(
|
|
406
|
-
"relative size-3 rounded-full transition-
|
|
407
|
+
"relative size-3 rounded-full transition-colors",
|
|
407
408
|
currentSlide === index ? "bg-primary" : "bg-foreground/20 hover:bg-foreground/40"
|
|
408
409
|
),
|
|
410
|
+
style: {
|
|
411
|
+
transitionDuration: `${import_motion.MOTION.durationMilliseconds.feedback}ms`,
|
|
412
|
+
transitionTimingFunction: import_motion.MOTION.cssEasing.standard
|
|
413
|
+
},
|
|
409
414
|
"aria-label": `Ir a la diapositiva ${index + 1}`,
|
|
410
415
|
"aria-current": currentSlide === index ? "step" : void 0
|
|
411
416
|
},
|
|
@@ -621,50 +626,65 @@ function PromotionalPresentation({
|
|
|
621
626
|
] }),
|
|
622
627
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-muted-foreground mx-auto mt-4 max-w-2xl text-lg", children: description })
|
|
623
628
|
] }),
|
|
624
|
-
categories.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-4", children: categories.map((category) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
{
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
629
|
+
categories.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-4", children: categories.map((category) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
630
|
+
import_card.Card,
|
|
631
|
+
{
|
|
632
|
+
className: "group relative overflow-hidden py-0 transition-shadow",
|
|
633
|
+
style: {
|
|
634
|
+
transitionDuration: `${import_motion.MOTION.durationMilliseconds.feedback}ms`,
|
|
635
|
+
transitionTimingFunction: import_motion.MOTION.cssEasing.standard
|
|
636
|
+
},
|
|
637
|
+
children: [
|
|
638
|
+
category.image && category.image.trim().length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
639
|
+
"img",
|
|
640
|
+
{
|
|
641
|
+
src: category.image,
|
|
642
|
+
alt: category.imageAlt ?? category.name,
|
|
643
|
+
className: "aspect-4/5 size-full object-cover grayscale transition-[filter] hover:grayscale-0",
|
|
644
|
+
width: 400,
|
|
645
|
+
height: 500,
|
|
646
|
+
loading: "lazy",
|
|
647
|
+
style: {
|
|
648
|
+
transitionDuration: `${import_motion.MOTION.durationMilliseconds.feedback}ms`,
|
|
649
|
+
transitionTimingFunction: import_motion.MOTION.cssEasing.standard
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "bg-muted aspect-4/5 size-full" }),
|
|
653
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_card.CardContent, { className: "absolute inset-x-0 bottom-0 p-4", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mt-1 flex items-center justify-between", children: [
|
|
654
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
655
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "text-background dark:text-foreground text-lg font-medium", children: category.name }),
|
|
656
|
+
category.count && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-background/80 dark:text-foreground/80 text-sm", children: [
|
|
657
|
+
category.count,
|
|
658
|
+
" ",
|
|
659
|
+
labels.productsLabel
|
|
660
|
+
] })
|
|
661
|
+
] }),
|
|
662
|
+
category.href ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
663
|
+
import_button.Button,
|
|
664
|
+
{
|
|
665
|
+
variant: "secondary",
|
|
666
|
+
size: "icon",
|
|
667
|
+
className: "h-9 w-9 cursor-pointer rounded-full backdrop-blur-xs",
|
|
668
|
+
"aria-label": `${labels.viewLabel} ${category.name}`,
|
|
669
|
+
asChild: true,
|
|
670
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: category.href, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowRight, {}) })
|
|
671
|
+
}
|
|
672
|
+
) : onCategoryClick ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
673
|
+
import_button.Button,
|
|
674
|
+
{
|
|
675
|
+
variant: "secondary",
|
|
676
|
+
size: "icon",
|
|
677
|
+
className: "h-9 w-9 cursor-pointer rounded-full backdrop-blur-xs",
|
|
678
|
+
"aria-label": `${labels.viewLabel} ${category.name}`,
|
|
679
|
+
onClick: () => onCategoryClick(category),
|
|
680
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowRight, {})
|
|
681
|
+
}
|
|
682
|
+
) : null
|
|
683
|
+
] }) })
|
|
684
|
+
]
|
|
685
|
+
},
|
|
686
|
+
category.id
|
|
687
|
+
)) })
|
|
668
688
|
] })
|
|
669
689
|
] });
|
|
670
690
|
}
|