1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
E-commerceecommerceshopimageillustrationplaceholder

Product Media

Imagen de producto: una foto real o un fondo tintado con una de 16 ilustraciones de producto a línea.

Vista Previa en Vivo

Uso

Copia y pega este código directamente en tu componente React:

tsx
import { ProductMedia } from 'react-component-kit-ui/ecommerce';

<ProductMedia icon="sneaker" hue={0} ratio="4 / 3" />

Instalación Manual

Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/ProductMedia.tsx + ProductMedia.css:

ProductMedia.tsx
'use client';

import type { CSSProperties, ReactNode } from 'react';
import { productIcon } from './product-icons';
import './ProductMedia.css';

/** class:list → className */
function cx(...args: unknown[]): string {
  const out: string[] = [];
  const walk = (v: unknown) => {
    if (!v) return;
    if (typeof v === 'string' || typeof v === 'number') out.push(String(v));
    else if (Array.isArray(v)) v.forEach(walk);
    else if (typeof v === 'object') for (const [k, on] of Object.entries(v as Record<string, unknown>)) if (on) out.push(k);
  };
  args.forEach(walk);
  return out.join(' ');
}

/** "a: b; --c: d" → style object */
function st(css: string | undefined): CSSProperties {
  const o: Record<string, string> = {};
  for (const decl of String(css ?? '').split(';')) {
    const i = decl.indexOf(':');
    if (i < 0) continue;
    const k = decl.slice(0, i).trim();
    if (k) o[k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = decl.slice(i + 1).trim();
  }
  return o as CSSProperties;
}

/**
 * ProductMedia — Product visual: a real photo, or a generated backdrop with a
 * line-art illustration so demo catalogs look finished without external images.
 *
 * @param {string} image - Product photo URL. When set, replaces the illustration.
 * @param {string} alt - Alternative text. Required for photos; illustrations are decorative.
 * @param {string} icon - Built-in illustration (headphones|watch|sneaker|backpack|camera|lamp|keyboard|sunglasses|bottle|speaker|jacket|mug|plant|shirt|bag|phone).
 * @param {number} hue - 0–360 hue of the generated backdrop. Default 40 (gold).
 * @param {string} ratio - CSS aspect-ratio. Default '1 / 1'.
 * @param {string} size - Illustration scale (sm|md|lg). Default 'md'.
 * @param {string} variant - Backdrop style (glow|soft|flat). Default 'glow'.
 * @param {string} className - Extra class.
 *
 * Accessibility: illustrations are aria-hidden; photos need `alt`.
 */
export interface ProductMediaProps {
  image?: string;
  alt?: string;
  icon?: string;
  hue?: number;
  ratio?: string;
  size?: 'sm' | 'md' | 'lg';
  variant?: 'glow' | 'soft' | 'flat';
  className?: string;
  children?: ReactNode;
}

export function ProductMedia({
  image,
  alt = '',
  icon = 'bag',
  hue = 40,
  ratio = '1 / 1',
  size = 'md',
  variant = 'glow',
  className = '',
  children
}: ProductMediaProps) {
  return (
    <>
      <div className={cx(['p-media', `p-media--${size}`, `p-media--${variant}`, className])} style={st(`--pm-hue: ${hue}; --pm-ratio: ${ratio};`)} data-rk-082ece="">
        {image ? (
          <img className="p-media__img" src={image} alt={alt} loading="lazy" decoding="async" data-rk-082ece="" />
        ) : (
          <svg className="p-media__art" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" data-rk-082ece=""><g dangerouslySetInnerHTML={{ __html: productIcon(icon) }} data-rk-082ece="" /></svg>
        )}
        {children}
      </div>
    </>
  );
}

export default ProductMedia;
ProductMedia.css
/* ProductMedia — ported from astro-component-kit (ecommerce/ProductMedia.astro) */

.p-media:where([data-rk-082ece]) {
  --pm-bg-1: hsl(var(--pm-hue) 70% 50% / 0.28);
  --pm-bg-2: hsl(var(--pm-hue) 60% 30% / 0.14);
  --pm-ink: hsl(var(--pm-hue) var(--pm-ink-s, 80%) var(--pm-ink-l, 80%));
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: var(--pm-ratio);
  width: 100%;
  overflow: hidden;
  border-radius: inherit;
  background:
    radial-gradient(circle at 50% 42%, var(--pm-bg-1), transparent 62%),
    linear-gradient(160deg, var(--pm-bg-2), var(--c-bg-1, #0a0a0a) 75%);
  isolation: isolate;
}
.p-media--soft:where([data-rk-082ece]) {
  background: linear-gradient(160deg, hsl(var(--pm-hue) var(--pm-soft-s, 40%) var(--pm-soft-l, 20%) / var(--pm-soft-a, 0.5)), var(--c-bg-1, #0a0a0a));
}
.p-media--flat:where([data-rk-082ece]) { background: var(--c-bg-2, #111); }

.p-media:where([data-rk-082ece])::after {
  content: '';
  position: absolute;
  inset: auto 22% 14% 22%;
  height: 10%;
  border-radius: 50%;
  background: radial-gradient(ellipse, rgba(0, 0, 0, 0.55), transparent 70%);
  z-index: -1;
}
.p-media--flat:where([data-rk-082ece])::after { display: none; }

.p-media__art:where([data-rk-082ece]) {
  width: 46%;
  height: auto;
  color: var(--pm-ink);
  filter: drop-shadow(0 6px 18px hsl(var(--pm-hue) 80% 50% / 0.35));
  transition: transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}
.p-media--sm:where([data-rk-082ece]) .p-media__art:where([data-rk-082ece]) { width: 58%; stroke-width: 2.4; }
.p-media--lg:where([data-rk-082ece]) .p-media__art:where([data-rk-082ece]) { width: 40%; stroke-width: 1.4; }

.p-media__img:where([data-rk-082ece]) {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.is-hoverable:hover .p-media__art:where([data-rk-082ece]),.is-hoverable:focus-within .p-media__art:where([data-rk-082ece]) {
  transform: translateY(-4%) rotate(-4deg) scale(1.04);
}

@media (prefers-reduced-motion: reduce) {
  .p-media__art:where([data-rk-082ece]) { transition: none; }
}

Información Rápida

Categoría
E-commerce
Nombre de archivo
ProductMedia.tsx
Dependencias
Ninguna — React + CSS puro
Etiquetas
ecommerceshopimageillustrationplaceholder