1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
E-commerce

Shop Collection Mosaic

Cuadrícula asimétrica de colecciones con una pieza destacada, numeración, recuento y enlaces subrayados.

collectionsgridecommercecategories

Uso

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

tsx
import { ShopCollectionMosaic } from 'react-component-kit-ui/sections';

<ShopCollectionMosaic title="Shop by *intention*" items={[
  { title: 'The wardrobe', href: '/shop/?category=clothing', count: 24, icon: 'jacket', hue: 22 },
  { title: 'At the table', href: '/shop/?category=objects', count: 12, icon: 'mug', hue: 14 },
  { title: 'To carry', href: '/shop/?category=bags', count: 8, icon: 'bag', hue: 34 },
]} />

Instalación Manual

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

ShopCollectionMosaic.tsx
'use client';

import ProductMedia from '../ecommerce/ProductMedia';
import './ShopCollectionMosaic.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(' ');
}

interface Tile { title: string; text?: string; href: string; count?: number; icon?: string; hue?: number; image?: string }

/**
 * ShopCollectionMosaic — Asymmetric collection grid: one tall feature tile and smaller tiles,
 * each with an index number, title, item count and an underline link.
 *
 * @param {string} eyebrow - Small label. Default 'Collections'.
 * @param {string} title - Section heading. Default 'Shop by *intention*'.
 * @param {Array<{title: string, text?: string, href: string, count?: number, icon?: string, hue?: number, image?: string}>} items - 2–5 tiles; the first one is featured.
 * @param {string} linkLabel - Tile link text. Default 'Explore'.
 * @param {string} countLabel - Suffix after counts. Default 'pieces'.
 * @param {string} variant - Layout (feature-left|feature-right). Default 'feature-left'.
 * @param {string} size - Tile height (sm|md|lg). Default 'md'.
 * @param {string} className - Extra class.
 *
 * Accessibility: each tile is one link with a heading inside; art is decorative.
 */
export interface ShopCollectionMosaicProps {
  eyebrow?: string;
  title?: string;
  items?: Tile[];
  linkLabel?: string;
  countLabel?: string;
  variant?: 'feature-left' | 'feature-right';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

export function ShopCollectionMosaic({
  eyebrow = 'Collections',
  title = 'Shop by *intention*',
  items = [
    { title: 'The wardrobe', text: 'Overshirts, knitwear and coats in natural fibres.', href: '#', count: 24, icon: 'jacket', hue: 22 },
    { title: 'At the table', text: 'Stoneware made by hand.', href: '#', count: 12, icon: 'mug', hue: 14 },
    { title: 'Light & air', text: 'Lamps and living things.', href: '#', count: 9, icon: 'plant', hue: 110 },
    { title: 'To carry', text: 'Bags that soften with age.', href: '#', count: 8, icon: 'bag', hue: 34 },
  ],
  linkLabel = 'Explore',
  countLabel = 'pieces',
  variant = 'feature-left',
  size = 'md',
  className = '',
}: ShopCollectionMosaicProps) {
  const parts = title.split(/\*(.+?)\*/g);
  return (
    <>
      <section className={cx(['scm', `scm--${variant}`, `scm--${size}`, className])} data-rk-ba6398="">
        <header className="scm__head" data-rk-ba6398="">
          {eyebrow && <p className="scm__eyebrow" data-rk-ba6398="">{eyebrow}</p>}
          <h2 className="scm__title" data-rk-ba6398="">{parts.map((p, i) => (i % 2 ? <em data-rk-ba6398="">{p}</em> : p))}</h2>
        </header>
        <ul className="scm__grid" data-rk-ba6398="">
          {items.slice(0, 5).map((t, i) => (
            <li key={i} className={cx(['scm__item', { 'scm__item--feature': i === 0 }])} data-rk-ba6398="">
              <a className="scm__tile" href={t.href} data-rk-ba6398="">
                <span className="scm__art" data-rk-ba6398=""><ProductMedia image={t.image} alt="" icon={t.icon} hue={t.hue} ratio="auto" variant="soft" size={i === 0 ? 'lg' : 'md'} /></span>
                <span className="scm__num" aria-hidden="true" data-rk-ba6398="">{String(i + 1).padStart(2, '0')}</span>
                <span className="scm__body" data-rk-ba6398="">
                  <h3 className="scm__name" data-rk-ba6398="">{t.title}</h3>
                  {t.text && <span className="scm__text" data-rk-ba6398="">{t.text}</span>}
                  <span className="scm__link" data-rk-ba6398="">{t.count !== undefined ? `${linkLabel} · ${t.count} ${countLabel}` : linkLabel}</span>
                </span>
              </a>
            </li>
          ))}
        </ul>
      </section>
    </>
  );
}

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

.scm:where([data-rk-ba6398]) { container-type: inline-size; --scm-h: 300px; color: var(--c-text-1, #fff); }
.scm--sm:where([data-rk-ba6398]) { --scm-h: 230px; }
.scm--lg:where([data-rk-ba6398]) { --scm-h: 380px; }
.scm__head:where([data-rk-ba6398]) { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-4, 1rem); margin-bottom: var(--sp-6, 1.5rem); padding-bottom: var(--sp-3, 0.75rem); border-bottom: 1px solid currentColor; }
.scm__eyebrow:where([data-rk-ba6398]) { order: 2; margin: 0; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--c-text-2, #a3a3a3); }
.scm__title:where([data-rk-ba6398]) { margin: 0; font-family: var(--font-display, Georgia, 'Times New Roman', serif); font-size: clamp(2rem, 4.5vw, 3.4rem); font-weight: 400; letter-spacing: -0.035em; line-height: 1; }
.scm__title:where([data-rk-ba6398]) em:where([data-rk-ba6398]) { font-style: italic; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }

.scm__grid:where([data-rk-ba6398]) {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-auto-rows: var(--scm-h);
  gap: var(--sp-3, 0.75rem);
  margin: 0;
  padding: 0;
  list-style: none;
}
.scm__item--feature:where([data-rk-ba6398]) { grid-column: span 2; grid-row: span 2; }
.scm--feature-right:where([data-rk-ba6398]) .scm__item--feature:where([data-rk-ba6398]) { grid-column: 3 / span 2; grid-row: 1 / span 2; }
.scm__item:nth-child(4):last-child:where([data-rk-ba6398]) { grid-column: span 2; }

.scm__tile:where([data-rk-ba6398]) {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  height: 100%;
  padding: var(--sp-5, 1.25rem);
  color: inherit;
  text-decoration: none;
  overflow: hidden;
  isolation: isolate;
  border-radius: var(--r-sm, 2px);
}
.scm__art:where([data-rk-ba6398]) { position: absolute; inset: 0; z-index: -1; }
.scm__art:where([data-rk-ba6398]) .p-media { height: 100%; aspect-ratio: auto; transition: transform 1s cubic-bezier(0.16, 1, 0.3, 1); }
.scm__art:where([data-rk-ba6398]) .p-media__art { width: auto; height: 42%; margin-bottom: 22%; }
.scm__tile:hover:where([data-rk-ba6398]) .scm__art:where([data-rk-ba6398]) .p-media { transform: scale(1.04); }
.scm__tile:where([data-rk-ba6398])::after { content: ''; position: absolute; inset: 35% 0 0; z-index: -1; background: linear-gradient(transparent, rgb(var(--paper-rgb, 5 5 5) / 0.9)); }
.scm__num:where([data-rk-ba6398]) { position: absolute; top: var(--sp-4, 1rem); left: var(--sp-5, 1.25rem); font-size: 0.75rem; font-weight: 600; letter-spacing: 0.1em; font-variant-numeric: tabular-nums; }
.scm__body:where([data-rk-ba6398]) { display: grid; gap: 4px; }
.scm__name:where([data-rk-ba6398]) { margin: 0; font-family: var(--font-display, Georgia, serif); font-size: 1.6rem; font-weight: 400; letter-spacing: -0.02em; line-height: 1.05; }
.scm__item--feature:where([data-rk-ba6398]) .scm__name:where([data-rk-ba6398]) { font-size: clamp(2rem, 3.5vw, 3rem); }
.scm__text:where([data-rk-ba6398]) { max-width: 24rem; font-size: 0.88rem; color: var(--c-text-2, #a3a3a3); }
.scm__link:where([data-rk-ba6398]) { width: fit-content; margin-top: 6px; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; border-bottom: 1px solid currentColor; padding-bottom: 2px; }
.scm__tile:hover:where([data-rk-ba6398]) .scm__link:where([data-rk-ba6398]) { color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
.scm__tile:focus-visible:where([data-rk-ba6398]) { outline: 2px solid var(--shop-accent, var(--c-primary-light, #d4a017)); outline-offset: 3px; }

@container (max-width: 900px) {
  .scm__grid:where([data-rk-ba6398]) { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: 220px; }
  .scm__item--feature:where([data-rk-ba6398]), .scm--feature-right:where([data-rk-ba6398]) .scm__item--feature:where([data-rk-ba6398]) { grid-column: 1 / -1; grid-row: span 2; }
  .scm__item:nth-child(4):last-child:where([data-rk-ba6398]) { grid-column: auto; }
}
@container (max-width: 480px) {
  .scm__grid:where([data-rk-ba6398]) { grid-template-columns: 1fr; }
  .scm__item--feature:where([data-rk-ba6398]), .scm--feature-right:where([data-rk-ba6398]) .scm__item--feature:where([data-rk-ba6398]) { grid-row: span 1; }
}
@media (prefers-reduced-motion: reduce) { .scm__art:where([data-rk-ba6398]) .p-media { transition: none; } }