Shop Collection Mosaic
Cuadrícula asimétrica de colecciones con una pieza destacada, numeración, recuento y enlaces subrayados.
collectionsgridecommercecategories
Vista Previa en Vivo
Collections
Shop by intention
Uso
Copia y pega este código directamente en tu página de Astro:
astro
---
import { ShopCollectionMosaic } from 'astro-component-kit/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 },
]} /> ---
import { ShopCollectionMosaic } from 'astro-component-kit/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.astro:
astro
---
/**
* 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.
*/
import ProductMedia from '../ecommerce/ProductMedia.astro';
interface Tile { title: string; text?: string; href: string; count?: number; icon?: string; hue?: number; image?: string }
interface Props {
eyebrow?: string;
title?: string;
items?: Tile[];
linkLabel?: string;
countLabel?: string;
variant?: 'feature-left' | 'feature-right';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const {
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 = '',
} = Astro.props;
const parts = title.split(/\*(.+?)\*/g);
---
<section class:list={['scm', `scm--${variant}`, `scm--${size}`, className]}>
<header class="scm__head">
{eyebrow && <p class="scm__eyebrow">{eyebrow}</p>}
<h2 class="scm__title">{parts.map((p, i) => (i % 2 ? <em>{p}</em> : p))}</h2>
</header>
<ul class="scm__grid">
{items.slice(0, 5).map((t, i) => (
<li class:list={['scm__item', { 'scm__item--feature': i === 0 }]}>
<a class="scm__tile" href={t.href}>
<span class="scm__art"><ProductMedia image={t.image} alt="" icon={t.icon} hue={t.hue} ratio="auto" variant="soft" size={i === 0 ? 'lg' : 'md'} /></span>
<span class="scm__num" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>
<span class="scm__body">
<h3 class="scm__name">{t.title}</h3>
{t.text && <span class="scm__text">{t.text}</span>}
<span class="scm__link">{t.count !== undefined ? `${linkLabel} · ${t.count} ${countLabel}` : linkLabel}</span>
</span>
</a>
</li>
))}
</ul>
</section>
<style>
.scm { container-type: inline-size; --scm-h: 300px; color: var(--c-text-1, #fff); }
.scm--sm { --scm-h: 230px; }
.scm--lg { --scm-h: 380px; }
.scm__head { 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 { 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 { 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 em { font-style: italic; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
.scm__grid {
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 { grid-column: span 2; grid-row: span 2; }
.scm--feature-right .scm__item--feature { grid-column: 3 / span 2; grid-row: 1 / span 2; }
.scm__item:nth-child(4):last-child { grid-column: span 2; }
.scm__tile {
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 { position: absolute; inset: 0; z-index: -1; }
.scm__art :global(.p-media) { height: 100%; aspect-ratio: auto; transition: transform 1s cubic-bezier(0.16, 1, 0.3, 1); }
.scm__art :global(.p-media__art) { width: auto; height: 42%; margin-bottom: 22%; }
.scm__tile:hover .scm__art :global(.p-media) { transform: scale(1.04); }
.scm__tile::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 { 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 { display: grid; gap: 4px; }
.scm__name { 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 .scm__name { font-size: clamp(2rem, 3.5vw, 3rem); }
.scm__text { max-width: 24rem; font-size: 0.88rem; color: var(--c-text-2, #a3a3a3); }
.scm__link { 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 .scm__link { color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
.scm__tile:focus-visible { outline: 2px solid var(--shop-accent, var(--c-primary-light, #d4a017)); outline-offset: 3px; }
@container (max-width: 900px) {
.scm__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: 220px; }
.scm__item--feature, .scm--feature-right .scm__item--feature { grid-column: 1 / -1; grid-row: span 2; }
.scm__item:nth-child(4):last-child { grid-column: auto; }
}
@container (max-width: 480px) {
.scm__grid { grid-template-columns: 1fr; }
.scm__item--feature, .scm--feature-right .scm__item--feature { grid-row: span 1; }
}
@media (prefers-reduced-motion: reduce) { .scm__art :global(.p-media) { transition: none; } }
</style> ---
/**
* 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.
*/
import ProductMedia from '../ecommerce/ProductMedia.astro';
interface Tile { title: string; text?: string; href: string; count?: number; icon?: string; hue?: number; image?: string }
interface Props {
eyebrow?: string;
title?: string;
items?: Tile[];
linkLabel?: string;
countLabel?: string;
variant?: 'feature-left' | 'feature-right';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const {
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 = '',
} = Astro.props;
const parts = title.split(/\*(.+?)\*/g);
---
<section class:list={['scm', `scm--${variant}`, `scm--${size}`, className]}>
<header class="scm__head">
{eyebrow && <p class="scm__eyebrow">{eyebrow}</p>}
<h2 class="scm__title">{parts.map((p, i) => (i % 2 ? <em>{p}</em> : p))}</h2>
</header>
<ul class="scm__grid">
{items.slice(0, 5).map((t, i) => (
<li class:list={['scm__item', { 'scm__item--feature': i === 0 }]}>
<a class="scm__tile" href={t.href}>
<span class="scm__art"><ProductMedia image={t.image} alt="" icon={t.icon} hue={t.hue} ratio="auto" variant="soft" size={i === 0 ? 'lg' : 'md'} /></span>
<span class="scm__num" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>
<span class="scm__body">
<h3 class="scm__name">{t.title}</h3>
{t.text && <span class="scm__text">{t.text}</span>}
<span class="scm__link">{t.count !== undefined ? `${linkLabel} · ${t.count} ${countLabel}` : linkLabel}</span>
</span>
</a>
</li>
))}
</ul>
</section>
<style>
.scm { container-type: inline-size; --scm-h: 300px; color: var(--c-text-1, #fff); }
.scm--sm { --scm-h: 230px; }
.scm--lg { --scm-h: 380px; }
.scm__head { 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 { 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 { 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 em { font-style: italic; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
.scm__grid {
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 { grid-column: span 2; grid-row: span 2; }
.scm--feature-right .scm__item--feature { grid-column: 3 / span 2; grid-row: 1 / span 2; }
.scm__item:nth-child(4):last-child { grid-column: span 2; }
.scm__tile {
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 { position: absolute; inset: 0; z-index: -1; }
.scm__art :global(.p-media) { height: 100%; aspect-ratio: auto; transition: transform 1s cubic-bezier(0.16, 1, 0.3, 1); }
.scm__art :global(.p-media__art) { width: auto; height: 42%; margin-bottom: 22%; }
.scm__tile:hover .scm__art :global(.p-media) { transform: scale(1.04); }
.scm__tile::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 { 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 { display: grid; gap: 4px; }
.scm__name { 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 .scm__name { font-size: clamp(2rem, 3.5vw, 3rem); }
.scm__text { max-width: 24rem; font-size: 0.88rem; color: var(--c-text-2, #a3a3a3); }
.scm__link { 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 .scm__link { color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
.scm__tile:focus-visible { outline: 2px solid var(--shop-accent, var(--c-primary-light, #d4a017)); outline-offset: 3px; }
@container (max-width: 900px) {
.scm__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: 220px; }
.scm__item--feature, .scm--feature-right .scm__item--feature { grid-column: 1 / -1; grid-row: span 2; }
.scm__item:nth-child(4):last-child { grid-column: auto; }
}
@container (max-width: 480px) {
.scm__grid { grid-template-columns: 1fr; }
.scm__item--feature, .scm--feature-right .scm__item--feature { grid-row: span 1; }
}
@media (prefers-reduced-motion: reduce) { .scm__art :global(.p-media) { transition: none; } }
</style>