Shop Collection Mosaic
Асимметричная сетка коллекций с главной плиткой, нумерацией, количеством товаров и ссылками.
collectionsgridecommercecategories
Использование
Скопируйте и вставьте этот код в ваш 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 },
]} />Ручная установка
Если вы не используете пакет npm, создайте файл и вставьте следующий код: 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; } }