1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
E-commerce ecommerceshopcardproductadd-to-cartwishlist

Store Product Card

Tarjeta de producto con ilustración o foto, insignia, valoración, colores, precio anterior, favoritos y botón de añadir al carrito.

Vista Previa en Vivo

Uso

Copia y pega este código directamente en tu página de Astro:

astro
---
import { StoreProductCard } from 'astro-component-kit/ecommerce';
---

<StoreProductCard
  href="/product/aurora-headphones/"
  categoryLabel="Audio"
  product={{
    slug: 'aurora-headphones',
    name: 'Aurora Wireless Headphones',
    price: 189,
    compareAt: 229,
    category: 'audio',
    rating: 4.8,
    reviews: 312,
    badge: '-18%',
    icon: 'headphones',
    hue: 38,
  }}
/>
--- import { StoreProductCard } from 'astro-component-kit/ecommerce'; --- <StoreProductCard href="/product/aurora-headphones/" categoryLabel="Audio" product={{ slug: 'aurora-headphones', name: 'Aurora Wireless Headphones', price: 189, compareAt: 229, category: 'audio', rating: 4.8, reviews: 312, badge: '-18%', icon: 'headphones', hue: 38, }} />

Instalación Manual

Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/lib/StoreProductCard.astro:

astro
---
/**
 * StoreProductCard — Shop product tile with media, badge, rating, colour swatches,
 * price (with compare-at), wishlist toggle and an add-to-cart button wired to the cart store.
 *
 * @param {Product} product - Product data (see ecommerce/types.ts). A demo product is used by default.
 * @param {string} href - Product page URL. Default '#'.
 * @param {string} categoryLabel - Small label above the title (e.g. category name).
 * @param {string} currency - ISO currency. Default 'USD'.
 * @param {string} locale - Number format locale. Default 'en-US'.
 * @param {string} ctaLabel - Add-to-cart label. Default 'Add to cart'.
 * @param {string} addedLabel - Label shown briefly after adding. Default 'Added'.
 * @param {string} cartKey - Cart storage key (several stores on one domain). Default 'ack-cart'.
 * @param {boolean} openCart - Open the CartDrawer after adding; false fires a CartToast instead. Default true.
 * @param {string} variant - Visual style (glass|minimal|bordered). Default 'glass'.
 * @param {string} size - Card density (sm|md|lg). Default 'md'.
 * @param {string} headingLevel - Title element (h2|h3|h4). Default 'h3'.
 * @param {string} className - Extra class.
 *
 * Accessibility: the title link covers the whole card (stretched link) so there are no
 * nested interactive elements; wishlist uses aria-pressed; out-of-stock disables the button.
 */
import ProductMedia from './ProductMedia.astro';
import ProductRating from './ProductRating.astro';
import Badge from '../feedback/Badge.astro';
import { formatMoney } from './cart-store';
import type { Product } from './types';

interface Props {
  product?: Product;
  href?: string;
  categoryLabel?: string;
  currency?: string;
  locale?: string;
  ctaLabel?: string;
  addedLabel?: string;
  cartKey?: string;
  openCart?: boolean;
  variant?: 'glass' | 'minimal' | 'bordered';
  size?: 'sm' | 'md' | 'lg';
  headingLevel?: 'h2' | 'h3' | 'h4';
  className?: string;
}

const {
  product = {
    slug: 'aurora-headphones',
    name: 'Aurora Wireless Headphones',
    price: 189,
    compareAt: 229,
    category: 'audio',
    rating: 4.8,
    reviews: 312,
    badge: '-18%',
    badgeVariant: 'warning',
    colors: [{ name: 'Graphite', hex: '#2b2b2f' }, { name: 'Sand', hex: '#d8c3a5' }, { name: 'Ocean', hex: '#3d6f9e' }],
    icon: 'headphones',
    hue: 38,
    inStock: true,
  },
  href = '#',
  categoryLabel = 'Audio',
  currency = 'USD',
  locale = 'en-US',
  ctaLabel = 'Add to cart',
  addedLabel = 'Added',
  cartKey = 'ack-cart',
  openCart = true,
  variant = 'glass',
  size = 'md',
  headingLevel = 'h3',
  className = '',
} = Astro.props;

const Heading = headingLevel;
const money = { currency, locale };
const inStock = product.inStock !== false;
const cartPayload = JSON.stringify({
  id: product.slug,
  name: product.name,
  price: product.price,
  icon: product.icon,
  hue: product.hue,
  image: product.image,
  variant: product.colors?.[0]?.name,
});
const titleId = `spc-${product.slug}`;
---

<article
  class:list={['sp-card', `sp-card--${variant}`, `sp-card--${size}`, 'is-hoverable', className]}
  aria-labelledby={titleId}
  data-product
  data-name={product.name.toLowerCase()}
  data-category={product.category}
  data-price={product.price}
  data-rating={product.rating ?? 0}
  data-colors={(product.colors ?? []).map((c) => c.name.toLowerCase()).join(',')}
  data-stock={inStock ? 'in' : 'out'}
>
  <div class="sp-card__media">
    <ProductMedia image={product.image} alt={product.name} icon={product.icon} hue={product.hue} />
    {product.badge && (
      <div class="sp-card__badge"><Badge variant={product.badgeVariant ?? 'default'}>{product.badge}</Badge></div>
    )}
    <button class="sp-card__wish" type="button" aria-pressed="false" aria-label={`Save ${product.name} to wishlist`} data-wishlist>
      <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1 1.1L12 21l7.8-7.5 1-1.1a5.5 5.5 0 0 0 0-7.8z"/></svg>
    </button>
  </div>

  <div class="sp-card__body">
    {categoryLabel && <p class="sp-card__cat">{categoryLabel}</p>}
    <Heading class="sp-card__title" id={titleId}>
      <a class="sp-card__link" href={href}>{product.name}</a>
    </Heading>
    {product.rating !== undefined && <ProductRating value={product.rating} count={product.reviews} />}

    {product.colors && product.colors.length > 0 && (
      <ul class="sp-card__swatches" aria-label={`Colours: ${product.colors.map((c) => c.name).join(', ')}`}>
        {product.colors.slice(0, 4).map((c) => (
          <li class="sp-card__swatch" style={`--sw: ${c.hex}`} title={c.name} />
        ))}
        {product.colors.length > 4 && <li class="sp-card__swatch-more">+{product.colors.length - 4}</li>}
      </ul>
    )}

    <div class="sp-card__foot">
      <p class="sp-card__price">
        <span class="sp-card__now">{formatMoney(product.price, money)}</span>
        {product.compareAt && (
          <s class="sp-card__was"><span class="sr-only">Was </span>{formatMoney(product.compareAt, money)}</s>
        )}
      </p>
      <button
        class="sp-card__add"
        type="button"
        data-add-to-cart={cartPayload}
        data-cart-key={cartKey}
        data-open-cart={openCart ? undefined : 'false'}
        data-added-label={addedLabel}
        disabled={!inStock}
        aria-label={inStock ? `${ctaLabel}: ${product.name}` : `${product.name} is sold out`}
      >
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 6h15l-1.5 9h-12z"/><path d="M6 6 5 3H2"/><circle cx="9" cy="20" r="1.4"/><circle cx="18" cy="20" r="1.4"/></svg>
        <span data-label>{inStock ? ctaLabel : 'Sold out'}</span>
      </button>
    </div>
  </div>
</article>

<script>
  import { bindAddToCart } from './cart-store';

  function init() {
    bindAddToCart(document);
    document.querySelectorAll<HTMLButtonElement>('[data-wishlist]:not([data-wish-bound])').forEach((btn) => {
      btn.dataset.wishBound = '';
      btn.addEventListener('click', () => {
        btn.setAttribute('aria-pressed', String(btn.getAttribute('aria-pressed') !== 'true'));
      });
    });
  }
  init();
  document.addEventListener('astro:page-load', init);
</script>

<style>
  .sp-card {
    container-type: inline-size;
    position: relative;
    display: flex;
    flex-direction: column;
    border-radius: var(--r-lg, 20px);
    overflow: hidden;
    background: var(--c-surface, rgb(var(--ink-rgb, 255 255 255) / 0.02));
    border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05));
    transition: border-color 0.25s ease, transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s ease;
  }
  .sp-card--glass {
    background: linear-gradient(180deg, rgb(var(--ink-rgb, 255 255 255) / 0.035), rgb(var(--ink-rgb, 255 255 255) / 0.01));
    backdrop-filter: blur(10px);
  }
  .sp-card--minimal { background: transparent; border-color: transparent; }
  .sp-card--bordered { border-color: rgb(var(--ink-rgb, 255 255 255) / 0.12); }
  .sp-card:hover {
    border-color: rgba(212, 160, 23, 0.35);
    transform: translateY(-3px);
    box-shadow: 0 18px 40px -22px rgba(212, 160, 23, 0.45);
  }
  .sp-card:focus-within {
    outline: 2px solid var(--c-primary-light, #d4a017);
    outline-offset: 2px;
  }

  .sp-card__media { position: relative; border-radius: var(--r-lg, 20px) var(--r-lg, 20px) 0 0; }
  .sp-card--minimal .sp-card__media { border-radius: var(--r-lg, 20px); }
  .sp-card__badge { position: absolute; top: var(--sp-3, 0.75rem); left: var(--sp-3, 0.75rem); z-index: 3; }
  .sp-card__wish {
    position: absolute;
    top: var(--sp-2, 0.5rem);
    right: var(--sp-2, 0.5rem);
    z-index: 3;
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    border-radius: var(--r-full, 9999px);
    border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.1);
    background: rgb(var(--paper-rgb, 5 5 5) / 0.55);
    backdrop-filter: blur(8px);
    color: var(--c-text-1, #fff);
    cursor: pointer;
    transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
  }
  .sp-card__wish:hover { transform: scale(1.06); }
  .sp-card__wish[aria-pressed='true'] { color: var(--c-danger, #f87171); }
  .sp-card__wish[aria-pressed='true'] svg { fill: currentColor; }
  .sp-card__wish:focus-visible,
  .sp-card__add:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }

  .sp-card__body {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2, 0.5rem);
    padding: var(--sp-4, 1rem) var(--sp-4, 1rem) var(--sp-5, 1.25rem);
    flex: 1;
  }
  .sp-card--sm .sp-card__body { padding: var(--sp-3, 0.75rem); }
  .sp-card--lg .sp-card__body { padding: var(--sp-6, 1.5rem); }
  .sp-card--minimal .sp-card__body { padding-inline: 0; }

  .sp-card__cat {
    margin: 0;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--c-primary-light, #d4a017);
  }
  .sp-card__title { margin: 0; font-size: 1rem; font-weight: 700; line-height: 1.3; }
  .sp-card--lg .sp-card__title { font-size: 1.15rem; }
  .sp-card__link { color: var(--c-text-1, #fff); text-decoration: none; }
  .sp-card__link:focus-visible { outline: none; }
  .sp-card__link::after { content: ''; position: absolute; inset: 0; z-index: 2; }

  .sp-card__swatches { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; padding: 0; list-style: none; }
  .sp-card__swatch {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--sw);
    box-shadow: 0 0 0 1px rgb(var(--ink-rgb, 255 255 255) / 0.25);
  }
  .sp-card__swatch-more { font-size: 0.72rem; color: var(--c-text-2, #a3a3a3); }

  .sp-card__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3, 0.75rem);
    margin-top: auto;
    padding-top: var(--sp-2, 0.5rem);
  }
  .sp-card__price { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 var(--sp-2, 0.5rem); margin: 0; }
  .sp-card__now { font-size: 1.1rem; font-weight: 800; color: var(--c-text-1, #fff); }
  .sp-card__was { font-size: 0.82rem; color: var(--c-text-2, #a3a3a3); }

  .sp-card__add {
    position: relative;
    z-index: 3;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    min-height: 44px;
    padding: 0 var(--sp-4, 1rem);
    border: 0;
    border-radius: var(--r-full, 9999px);
    background: var(--c-primary-light, #d4a017);
    color: var(--c-on-primary, #050505);
    font: inherit;
    font-size: 0.82rem;
    font-weight: 700;
    white-space: nowrap;
    cursor: pointer;
    transition: transform 0.2s ease, filter 0.2s ease, background 0.2s ease;
  }
  .sp-card__add:hover { filter: brightness(1.08); transform: translateY(-1px); }
  .sp-card__add:disabled { background: rgb(var(--ink-rgb, 255 255 255) / 0.08); color: var(--c-text-2, #a3a3a3); cursor: not-allowed; transform: none; }
  .sp-card__add.is-added { background: var(--c-success, #34d399); }
  .sp-card--sm .sp-card__add span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .sp-card--sm .sp-card__add { width: 44px; padding: 0; justify-content: center; }

  .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

  /* Narrow cards (2-up on phones): stack price above a full-width button */
  @container (max-width: 250px) {
    .sp-card__body { padding: var(--sp-3, 0.75rem) var(--sp-3, 0.75rem) var(--sp-4, 1rem); }
    .sp-card__foot { flex-direction: column; align-items: stretch; gap: var(--sp-2, 0.5rem); }
    .sp-card__add { justify-content: center; }
    .sp-card__title { font-size: 0.92rem; }
  }

  @media (prefers-reduced-motion: reduce) {
    .sp-card, .sp-card__add, .sp-card__wish { transition: none; }
    .sp-card:hover { transform: none; }
  }
</style>
--- /** * StoreProductCard — Shop product tile with media, badge, rating, colour swatches, * price (with compare-at), wishlist toggle and an add-to-cart button wired to the cart store. * * @param {Product} product - Product data (see ecommerce/types.ts). A demo product is used by default. * @param {string} href - Product page URL. Default '#'. * @param {string} categoryLabel - Small label above the title (e.g. category name). * @param {string} currency - ISO currency. Default 'USD'. * @param {string} locale - Number format locale. Default 'en-US'. * @param {string} ctaLabel - Add-to-cart label. Default 'Add to cart'. * @param {string} addedLabel - Label shown briefly after adding. Default 'Added'. * @param {string} cartKey - Cart storage key (several stores on one domain). Default 'ack-cart'. * @param {boolean} openCart - Open the CartDrawer after adding; false fires a CartToast instead. Default true. * @param {string} variant - Visual style (glass|minimal|bordered). Default 'glass'. * @param {string} size - Card density (sm|md|lg). Default 'md'. * @param {string} headingLevel - Title element (h2|h3|h4). Default 'h3'. * @param {string} className - Extra class. * * Accessibility: the title link covers the whole card (stretched link) so there are no * nested interactive elements; wishlist uses aria-pressed; out-of-stock disables the button. */ import ProductMedia from './ProductMedia.astro'; import ProductRating from './ProductRating.astro'; import Badge from '../feedback/Badge.astro'; import { formatMoney } from './cart-store'; import type { Product } from './types'; interface Props { product?: Product; href?: string; categoryLabel?: string; currency?: string; locale?: string; ctaLabel?: string; addedLabel?: string; cartKey?: string; openCart?: boolean; variant?: 'glass' | 'minimal' | 'bordered'; size?: 'sm' | 'md' | 'lg'; headingLevel?: 'h2' | 'h3' | 'h4'; className?: string; } const { product = { slug: 'aurora-headphones', name: 'Aurora Wireless Headphones', price: 189, compareAt: 229, category: 'audio', rating: 4.8, reviews: 312, badge: '-18%', badgeVariant: 'warning', colors: [{ name: 'Graphite', hex: '#2b2b2f' }, { name: 'Sand', hex: '#d8c3a5' }, { name: 'Ocean', hex: '#3d6f9e' }], icon: 'headphones', hue: 38, inStock: true, }, href = '#', categoryLabel = 'Audio', currency = 'USD', locale = 'en-US', ctaLabel = 'Add to cart', addedLabel = 'Added', cartKey = 'ack-cart', openCart = true, variant = 'glass', size = 'md', headingLevel = 'h3', className = '', } = Astro.props; const Heading = headingLevel; const money = { currency, locale }; const inStock = product.inStock !== false; const cartPayload = JSON.stringify({ id: product.slug, name: product.name, price: product.price, icon: product.icon, hue: product.hue, image: product.image, variant: product.colors?.[0]?.name, }); const titleId = `spc-${product.slug}`; --- <article class:list={['sp-card', `sp-card--${variant}`, `sp-card--${size}`, 'is-hoverable', className]} aria-labelledby={titleId} data-product data-name={product.name.toLowerCase()} data-category={product.category} data-price={product.price} data-rating={product.rating ?? 0} data-colors={(product.colors ?? []).map((c) => c.name.toLowerCase()).join(',')} data-stock={inStock ? 'in' : 'out'} > <div class="sp-card__media"> <ProductMedia image={product.image} alt={product.name} icon={product.icon} hue={product.hue} /> {product.badge && ( <div class="sp-card__badge"><Badge variant={product.badgeVariant ?? 'default'}>{product.badge}</Badge></div> )} <button class="sp-card__wish" type="button" aria-pressed="false" aria-label={`Save ${product.name} to wishlist`} data-wishlist> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1 1.1L12 21l7.8-7.5 1-1.1a5.5 5.5 0 0 0 0-7.8z"/></svg> </button> </div> <div class="sp-card__body"> {categoryLabel && <p class="sp-card__cat">{categoryLabel}</p>} <Heading class="sp-card__title" id={titleId}> <a class="sp-card__link" href={href}>{product.name}</a> </Heading> {product.rating !== undefined && <ProductRating value={product.rating} count={product.reviews} />} {product.colors && product.colors.length > 0 && ( <ul class="sp-card__swatches" aria-label={`Colours: ${product.colors.map((c) => c.name).join(', ')}`}> {product.colors.slice(0, 4).map((c) => ( <li class="sp-card__swatch" style={`--sw: ${c.hex}`} title={c.name} /> ))} {product.colors.length > 4 && <li class="sp-card__swatch-more">+{product.colors.length - 4}</li>} </ul> )} <div class="sp-card__foot"> <p class="sp-card__price"> <span class="sp-card__now">{formatMoney(product.price, money)}</span> {product.compareAt && ( <s class="sp-card__was"><span class="sr-only">Was </span>{formatMoney(product.compareAt, money)}</s> )} </p> <button class="sp-card__add" type="button" data-add-to-cart={cartPayload} data-cart-key={cartKey} data-open-cart={openCart ? undefined : 'false'} data-added-label={addedLabel} disabled={!inStock} aria-label={inStock ? `${ctaLabel}: ${product.name}` : `${product.name} is sold out`} > <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 6h15l-1.5 9h-12z"/><path d="M6 6 5 3H2"/><circle cx="9" cy="20" r="1.4"/><circle cx="18" cy="20" r="1.4"/></svg> <span data-label>{inStock ? ctaLabel : 'Sold out'}</span> </button> </div> </div> </article> <script> import { bindAddToCart } from './cart-store'; function init() { bindAddToCart(document); document.querySelectorAll<HTMLButtonElement>('[data-wishlist]:not([data-wish-bound])').forEach((btn) => { btn.dataset.wishBound = ''; btn.addEventListener('click', () => { btn.setAttribute('aria-pressed', String(btn.getAttribute('aria-pressed') !== 'true')); }); }); } init(); document.addEventListener('astro:page-load', init); </script> <style> .sp-card { container-type: inline-size; position: relative; display: flex; flex-direction: column; border-radius: var(--r-lg, 20px); overflow: hidden; background: var(--c-surface, rgb(var(--ink-rgb, 255 255 255) / 0.02)); border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05)); transition: border-color 0.25s ease, transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s ease; } .sp-card--glass { background: linear-gradient(180deg, rgb(var(--ink-rgb, 255 255 255) / 0.035), rgb(var(--ink-rgb, 255 255 255) / 0.01)); backdrop-filter: blur(10px); } .sp-card--minimal { background: transparent; border-color: transparent; } .sp-card--bordered { border-color: rgb(var(--ink-rgb, 255 255 255) / 0.12); } .sp-card:hover { border-color: rgba(212, 160, 23, 0.35); transform: translateY(-3px); box-shadow: 0 18px 40px -22px rgba(212, 160, 23, 0.45); } .sp-card:focus-within { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; } .sp-card__media { position: relative; border-radius: var(--r-lg, 20px) var(--r-lg, 20px) 0 0; } .sp-card--minimal .sp-card__media { border-radius: var(--r-lg, 20px); } .sp-card__badge { position: absolute; top: var(--sp-3, 0.75rem); left: var(--sp-3, 0.75rem); z-index: 3; } .sp-card__wish { position: absolute; top: var(--sp-2, 0.5rem); right: var(--sp-2, 0.5rem); z-index: 3; display: grid; place-items: center; width: 44px; height: 44px; border-radius: var(--r-full, 9999px); border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.1); background: rgb(var(--paper-rgb, 5 5 5) / 0.55); backdrop-filter: blur(8px); color: var(--c-text-1, #fff); cursor: pointer; transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease; } .sp-card__wish:hover { transform: scale(1.06); } .sp-card__wish[aria-pressed='true'] { color: var(--c-danger, #f87171); } .sp-card__wish[aria-pressed='true'] svg { fill: currentColor; } .sp-card__wish:focus-visible, .sp-card__add:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; } .sp-card__body { display: flex; flex-direction: column; gap: var(--sp-2, 0.5rem); padding: var(--sp-4, 1rem) var(--sp-4, 1rem) var(--sp-5, 1.25rem); flex: 1; } .sp-card--sm .sp-card__body { padding: var(--sp-3, 0.75rem); } .sp-card--lg .sp-card__body { padding: var(--sp-6, 1.5rem); } .sp-card--minimal .sp-card__body { padding-inline: 0; } .sp-card__cat { margin: 0; font-size: 0.7rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--c-primary-light, #d4a017); } .sp-card__title { margin: 0; font-size: 1rem; font-weight: 700; line-height: 1.3; } .sp-card--lg .sp-card__title { font-size: 1.15rem; } .sp-card__link { color: var(--c-text-1, #fff); text-decoration: none; } .sp-card__link:focus-visible { outline: none; } .sp-card__link::after { content: ''; position: absolute; inset: 0; z-index: 2; } .sp-card__swatches { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; padding: 0; list-style: none; } .sp-card__swatch { width: 14px; height: 14px; border-radius: 50%; background: var(--sw); box-shadow: 0 0 0 1px rgb(var(--ink-rgb, 255 255 255) / 0.25); } .sp-card__swatch-more { font-size: 0.72rem; color: var(--c-text-2, #a3a3a3); } .sp-card__foot { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3, 0.75rem); margin-top: auto; padding-top: var(--sp-2, 0.5rem); } .sp-card__price { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 var(--sp-2, 0.5rem); margin: 0; } .sp-card__now { font-size: 1.1rem; font-weight: 800; color: var(--c-text-1, #fff); } .sp-card__was { font-size: 0.82rem; color: var(--c-text-2, #a3a3a3); } .sp-card__add { position: relative; z-index: 3; display: inline-flex; align-items: center; gap: 0.4rem; min-height: 44px; padding: 0 var(--sp-4, 1rem); border: 0; border-radius: var(--r-full, 9999px); background: var(--c-primary-light, #d4a017); color: var(--c-on-primary, #050505); font: inherit; font-size: 0.82rem; font-weight: 700; white-space: nowrap; cursor: pointer; transition: transform 0.2s ease, filter 0.2s ease, background 0.2s ease; } .sp-card__add:hover { filter: brightness(1.08); transform: translateY(-1px); } .sp-card__add:disabled { background: rgb(var(--ink-rgb, 255 255 255) / 0.08); color: var(--c-text-2, #a3a3a3); cursor: not-allowed; transform: none; } .sp-card__add.is-added { background: var(--c-success, #34d399); } .sp-card--sm .sp-card__add span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); } .sp-card--sm .sp-card__add { width: 44px; padding: 0; justify-content: center; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } /* Narrow cards (2-up on phones): stack price above a full-width button */ @container (max-width: 250px) { .sp-card__body { padding: var(--sp-3, 0.75rem) var(--sp-3, 0.75rem) var(--sp-4, 1rem); } .sp-card__foot { flex-direction: column; align-items: stretch; gap: var(--sp-2, 0.5rem); } .sp-card__add { justify-content: center; } .sp-card__title { font-size: 0.92rem; } } @media (prefers-reduced-motion: reduce) { .sp-card, .sp-card__add, .sp-card__wish { transition: none; } .sp-card:hover { transform: none; } } </style>

Información Rápida

Categoría
E-commerce
Nombre de archivo
StoreProductCard.astro
Dependencias
Ninguna — Astro nativo + CSS puro
Etiquetas
ecommerceshopcardproductadd-to-cartwishlist