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