-18%
E-commerce ecommerceshopcardproductadd-to-cartwishlist
Store Product Card
Карточка товара с иллюстрацией или фото, бейджем, рейтингом, цветами, старой ценой, избранным и кнопкой «в корзину».
Живое превью
Использование
Скопируйте и вставьте этот код на вашу страницу Astro:
astro
---
import { StoreProductCard } from 'astro-component-kit/ecommerce';
---
<StoreProductCard
href="/product/aurora-headphones/"
categoryLabel="Audio"
product={{
slug: 'aurora-headphones',
name: 'Aurora Wireless Headphones',
price: 189,
compareAt: 229,
category: 'audio',
rating: 4.8,
reviews: 312,
badge: '-18%',
icon: 'headphones',
hue: 38,
}}
/> ---
import { StoreProductCard } from 'astro-component-kit/ecommerce';
---
<StoreProductCard
href="/product/aurora-headphones/"
categoryLabel="Audio"
product={{
slug: 'aurora-headphones',
name: 'Aurora Wireless Headphones',
price: 189,
compareAt: 229,
category: 'audio',
rating: 4.8,
reviews: 312,
badge: '-18%',
icon: 'headphones',
hue: 38,
}}
/> Ручная установка
Если вы не используете пакет npm, создайте файл и вставьте следующий код: src/components/lib/StoreProductCard.astro:
astro
---
/**
* StoreProductCard — Shop product tile with media, badge, rating, colour swatches,
* price (with compare-at), wishlist toggle and an add-to-cart button wired to the cart store.
*
* @param {Product} product - Product data (see ecommerce/types.ts). A demo product is used by default.
* @param {string} href - Product page URL. Default '#'.
* @param {string} categoryLabel - Small label above the title (e.g. category name).
* @param {string} currency - ISO currency. Default 'USD'.
* @param {string} locale - Number format locale. Default 'en-US'.
* @param {string} ctaLabel - Add-to-cart label. Default 'Add to cart'.
* @param {string} addedLabel - Label shown briefly after adding. Default 'Added'.
* @param {string} cartKey - Cart storage key (several stores on one domain). Default 'ack-cart'.
* @param {boolean} openCart - Open the CartDrawer after adding; false fires a CartToast instead. Default true.
* @param {string} variant - Visual style (glass|minimal|bordered). Default 'glass'.
* @param {string} size - Card density (sm|md|lg). Default 'md'.
* @param {string} headingLevel - Title element (h2|h3|h4). Default 'h3'.
* @param {string} className - Extra class.
*
* Accessibility: the title link covers the whole card (stretched link) so there are no
* nested interactive elements; wishlist uses aria-pressed; out-of-stock disables the button.
*/
import ProductMedia from './ProductMedia.astro';
import ProductRating from './ProductRating.astro';
import Badge from '../feedback/Badge.astro';
import { formatMoney } from './cart-store';
import type { Product } from './types';
interface Props {
product?: Product;
href?: string;
categoryLabel?: string;
currency?: string;
locale?: string;
ctaLabel?: string;
addedLabel?: string;
cartKey?: string;
openCart?: boolean;
variant?: 'glass' | 'minimal' | 'bordered';
size?: 'sm' | 'md' | 'lg';
headingLevel?: 'h2' | 'h3' | 'h4';
className?: string;
}
const {
product = {
slug: 'aurora-headphones',
name: 'Aurora Wireless Headphones',
price: 189,
compareAt: 229,
category: 'audio',
rating: 4.8,
reviews: 312,
badge: '-18%',
badgeVariant: 'warning',
colors: [{ name: 'Graphite', hex: '#2b2b2f' }, { name: 'Sand', hex: '#d8c3a5' }, { name: 'Ocean', hex: '#3d6f9e' }],
icon: 'headphones',
hue: 38,
inStock: true,
},
href = '#',
categoryLabel = 'Audio',
currency = 'USD',
locale = 'en-US',
ctaLabel = 'Add to cart',
addedLabel = 'Added',
cartKey = 'ack-cart',
openCart = true,
variant = 'glass',
size = 'md',
headingLevel = 'h3',
className = '',
} = Astro.props;
const Heading = headingLevel;
const money = { currency, locale };
const inStock = product.inStock !== false;
const cartPayload = JSON.stringify({
id: product.slug,
name: product.name,
price: product.price,
icon: product.icon,
hue: product.hue,
image: product.image,
variant: product.colors?.[0]?.name,
});
const titleId = `spc-${product.slug}`;
---
<article
class:list={['sp-card', `sp-card--${variant}`, `sp-card--${size}`, 'is-hoverable', className]}
aria-labelledby={titleId}
data-product
data-name={product.name.toLowerCase()}
data-category={product.category}
data-price={product.price}
data-rating={product.rating ?? 0}
data-colors={(product.colors ?? []).map((c) => c.name.toLowerCase()).join(',')}
data-stock={inStock ? 'in' : 'out'}
>
<div class="sp-card__media">
<ProductMedia image={product.image} alt={product.name} icon={product.icon} hue={product.hue} />
{product.badge && (
<div class="sp-card__badge"><Badge variant={product.badgeVariant ?? 'default'}>{product.badge}</Badge></div>
)}
<button class="sp-card__wish" type="button" aria-pressed="false" aria-label={`Save ${product.name} to wishlist`} data-wishlist>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1 1.1L12 21l7.8-7.5 1-1.1a5.5 5.5 0 0 0 0-7.8z"/></svg>
</button>
</div>
<div class="sp-card__body">
{categoryLabel && <p class="sp-card__cat">{categoryLabel}</p>}
<Heading class="sp-card__title" id={titleId}>
<a class="sp-card__link" href={href}>{product.name}</a>
</Heading>
{product.rating !== undefined && <ProductRating value={product.rating} count={product.reviews} />}
{product.colors && product.colors.length > 0 && (
<ul class="sp-card__swatches" aria-label={`Colours: ${product.colors.map((c) => c.name).join(', ')}`}>
{product.colors.slice(0, 4).map((c) => (
<li class="sp-card__swatch" style={`--sw: ${c.hex}`} title={c.name} />
))}
{product.colors.length > 4 && <li class="sp-card__swatch-more">+{product.colors.length - 4}</li>}
</ul>
)}
<div class="sp-card__foot">
<p class="sp-card__price">
<span class="sp-card__now">{formatMoney(product.price, money)}</span>
{product.compareAt && (
<s class="sp-card__was"><span class="sr-only">Was </span>{formatMoney(product.compareAt, money)}</s>
)}
</p>
<button
class="sp-card__add"
type="button"
data-add-to-cart={cartPayload}
data-cart-key={cartKey}
data-open-cart={openCart ? undefined : 'false'}
data-added-label={addedLabel}
disabled={!inStock}
aria-label={inStock ? `${ctaLabel}: ${product.name}` : `${product.name} is sold out`}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 6h15l-1.5 9h-12z"/><path d="M6 6 5 3H2"/><circle cx="9" cy="20" r="1.4"/><circle cx="18" cy="20" r="1.4"/></svg>
<span data-label>{inStock ? ctaLabel : 'Sold out'}</span>
</button>
</div>
</div>
</article>
<script>
import { bindAddToCart } from './cart-store';
function init() {
bindAddToCart(document);
document.querySelectorAll<HTMLButtonElement>('[data-wishlist]:not([data-wish-bound])').forEach((btn) => {
btn.dataset.wishBound = '';
btn.addEventListener('click', () => {
btn.setAttribute('aria-pressed', String(btn.getAttribute('aria-pressed') !== 'true'));
});
});
}
init();
document.addEventListener('astro:page-load', init);
</script>
<style>
.sp-card {
container-type: inline-size;
position: relative;
display: flex;
flex-direction: column;
border-radius: var(--r-lg, 20px);
overflow: hidden;
background: var(--c-surface, rgb(var(--ink-rgb, 255 255 255) / 0.02));
border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05));
transition: border-color 0.25s ease, transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s ease;
}
.sp-card--glass {
background: linear-gradient(180deg, rgb(var(--ink-rgb, 255 255 255) / 0.035), rgb(var(--ink-rgb, 255 255 255) / 0.01));
backdrop-filter: blur(10px);
}
.sp-card--minimal { background: transparent; border-color: transparent; }
.sp-card--bordered { border-color: rgb(var(--ink-rgb, 255 255 255) / 0.12); }
.sp-card:hover {
border-color: rgba(212, 160, 23, 0.35);
transform: translateY(-3px);
box-shadow: 0 18px 40px -22px rgba(212, 160, 23, 0.45);
}
.sp-card:focus-within {
outline: 2px solid var(--c-primary-light, #d4a017);
outline-offset: 2px;
}
.sp-card__media { position: relative; border-radius: var(--r-lg, 20px) var(--r-lg, 20px) 0 0; }
.sp-card--minimal .sp-card__media { border-radius: var(--r-lg, 20px); }
.sp-card__badge { position: absolute; top: var(--sp-3, 0.75rem); left: var(--sp-3, 0.75rem); z-index: 3; }
.sp-card__wish {
position: absolute;
top: var(--sp-2, 0.5rem);
right: var(--sp-2, 0.5rem);
z-index: 3;
display: grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: var(--r-full, 9999px);
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.1);
background: rgb(var(--paper-rgb, 5 5 5) / 0.55);
backdrop-filter: blur(8px);
color: var(--c-text-1, #fff);
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
}
.sp-card__wish:hover { transform: scale(1.06); }
.sp-card__wish[aria-pressed='true'] { color: var(--c-danger, #f87171); }
.sp-card__wish[aria-pressed='true'] svg { fill: currentColor; }
.sp-card__wish:focus-visible,
.sp-card__add:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
.sp-card__body {
display: flex;
flex-direction: column;
gap: var(--sp-2, 0.5rem);
padding: var(--sp-4, 1rem) var(--sp-4, 1rem) var(--sp-5, 1.25rem);
flex: 1;
}
.sp-card--sm .sp-card__body { padding: var(--sp-3, 0.75rem); }
.sp-card--lg .sp-card__body { padding: var(--sp-6, 1.5rem); }
.sp-card--minimal .sp-card__body { padding-inline: 0; }
.sp-card__cat {
margin: 0;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--c-primary-light, #d4a017);
}
.sp-card__title { margin: 0; font-size: 1rem; font-weight: 700; line-height: 1.3; }
.sp-card--lg .sp-card__title { font-size: 1.15rem; }
.sp-card__link { color: var(--c-text-1, #fff); text-decoration: none; }
.sp-card__link:focus-visible { outline: none; }
.sp-card__link::after { content: ''; position: absolute; inset: 0; z-index: 2; }
.sp-card__swatches { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; padding: 0; list-style: none; }
.sp-card__swatch {
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--sw);
box-shadow: 0 0 0 1px rgb(var(--ink-rgb, 255 255 255) / 0.25);
}
.sp-card__swatch-more { font-size: 0.72rem; color: var(--c-text-2, #a3a3a3); }
.sp-card__foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-3, 0.75rem);
margin-top: auto;
padding-top: var(--sp-2, 0.5rem);
}
.sp-card__price { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 var(--sp-2, 0.5rem); margin: 0; }
.sp-card__now { font-size: 1.1rem; font-weight: 800; color: var(--c-text-1, #fff); }
.sp-card__was { font-size: 0.82rem; color: var(--c-text-2, #a3a3a3); }
.sp-card__add {
position: relative;
z-index: 3;
display: inline-flex;
align-items: center;
gap: 0.4rem;
min-height: 44px;
padding: 0 var(--sp-4, 1rem);
border: 0;
border-radius: var(--r-full, 9999px);
background: var(--c-primary-light, #d4a017);
color: var(--c-on-primary, #050505);
font: inherit;
font-size: 0.82rem;
font-weight: 700;
white-space: nowrap;
cursor: pointer;
transition: transform 0.2s ease, filter 0.2s ease, background 0.2s ease;
}
.sp-card__add:hover { filter: brightness(1.08); transform: translateY(-1px); }
.sp-card__add:disabled { background: rgb(var(--ink-rgb, 255 255 255) / 0.08); color: var(--c-text-2, #a3a3a3); cursor: not-allowed; transform: none; }
.sp-card__add.is-added { background: var(--c-success, #34d399); }
.sp-card--sm .sp-card__add span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.sp-card--sm .sp-card__add { width: 44px; padding: 0; justify-content: center; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
/* Narrow cards (2-up on phones): stack price above a full-width button */
@container (max-width: 250px) {
.sp-card__body { padding: var(--sp-3, 0.75rem) var(--sp-3, 0.75rem) var(--sp-4, 1rem); }
.sp-card__foot { flex-direction: column; align-items: stretch; gap: var(--sp-2, 0.5rem); }
.sp-card__add { justify-content: center; }
.sp-card__title { font-size: 0.92rem; }
}
@media (prefers-reduced-motion: reduce) {
.sp-card, .sp-card__add, .sp-card__wish { transition: none; }
.sp-card:hover { transform: none; }
}
</style> ---
/**
* StoreProductCard — Shop product tile with media, badge, rating, colour swatches,
* price (with compare-at), wishlist toggle and an add-to-cart button wired to the cart store.
*
* @param {Product} product - Product data (see ecommerce/types.ts). A demo product is used by default.
* @param {string} href - Product page URL. Default '#'.
* @param {string} categoryLabel - Small label above the title (e.g. category name).
* @param {string} currency - ISO currency. Default 'USD'.
* @param {string} locale - Number format locale. Default 'en-US'.
* @param {string} ctaLabel - Add-to-cart label. Default 'Add to cart'.
* @param {string} addedLabel - Label shown briefly after adding. Default 'Added'.
* @param {string} cartKey - Cart storage key (several stores on one domain). Default 'ack-cart'.
* @param {boolean} openCart - Open the CartDrawer after adding; false fires a CartToast instead. Default true.
* @param {string} variant - Visual style (glass|minimal|bordered). Default 'glass'.
* @param {string} size - Card density (sm|md|lg). Default 'md'.
* @param {string} headingLevel - Title element (h2|h3|h4). Default 'h3'.
* @param {string} className - Extra class.
*
* Accessibility: the title link covers the whole card (stretched link) so there are no
* nested interactive elements; wishlist uses aria-pressed; out-of-stock disables the button.
*/
import ProductMedia from './ProductMedia.astro';
import ProductRating from './ProductRating.astro';
import Badge from '../feedback/Badge.astro';
import { formatMoney } from './cart-store';
import type { Product } from './types';
interface Props {
product?: Product;
href?: string;
categoryLabel?: string;
currency?: string;
locale?: string;
ctaLabel?: string;
addedLabel?: string;
cartKey?: string;
openCart?: boolean;
variant?: 'glass' | 'minimal' | 'bordered';
size?: 'sm' | 'md' | 'lg';
headingLevel?: 'h2' | 'h3' | 'h4';
className?: string;
}
const {
product = {
slug: 'aurora-headphones',
name: 'Aurora Wireless Headphones',
price: 189,
compareAt: 229,
category: 'audio',
rating: 4.8,
reviews: 312,
badge: '-18%',
badgeVariant: 'warning',
colors: [{ name: 'Graphite', hex: '#2b2b2f' }, { name: 'Sand', hex: '#d8c3a5' }, { name: 'Ocean', hex: '#3d6f9e' }],
icon: 'headphones',
hue: 38,
inStock: true,
},
href = '#',
categoryLabel = 'Audio',
currency = 'USD',
locale = 'en-US',
ctaLabel = 'Add to cart',
addedLabel = 'Added',
cartKey = 'ack-cart',
openCart = true,
variant = 'glass',
size = 'md',
headingLevel = 'h3',
className = '',
} = Astro.props;
const Heading = headingLevel;
const money = { currency, locale };
const inStock = product.inStock !== false;
const cartPayload = JSON.stringify({
id: product.slug,
name: product.name,
price: product.price,
icon: product.icon,
hue: product.hue,
image: product.image,
variant: product.colors?.[0]?.name,
});
const titleId = `spc-${product.slug}`;
---
<article
class:list={['sp-card', `sp-card--${variant}`, `sp-card--${size}`, 'is-hoverable', className]}
aria-labelledby={titleId}
data-product
data-name={product.name.toLowerCase()}
data-category={product.category}
data-price={product.price}
data-rating={product.rating ?? 0}
data-colors={(product.colors ?? []).map((c) => c.name.toLowerCase()).join(',')}
data-stock={inStock ? 'in' : 'out'}
>
<div class="sp-card__media">
<ProductMedia image={product.image} alt={product.name} icon={product.icon} hue={product.hue} />
{product.badge && (
<div class="sp-card__badge"><Badge variant={product.badgeVariant ?? 'default'}>{product.badge}</Badge></div>
)}
<button class="sp-card__wish" type="button" aria-pressed="false" aria-label={`Save ${product.name} to wishlist`} data-wishlist>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1 1.1L12 21l7.8-7.5 1-1.1a5.5 5.5 0 0 0 0-7.8z"/></svg>
</button>
</div>
<div class="sp-card__body">
{categoryLabel && <p class="sp-card__cat">{categoryLabel}</p>}
<Heading class="sp-card__title" id={titleId}>
<a class="sp-card__link" href={href}>{product.name}</a>
</Heading>
{product.rating !== undefined && <ProductRating value={product.rating} count={product.reviews} />}
{product.colors && product.colors.length > 0 && (
<ul class="sp-card__swatches" aria-label={`Colours: ${product.colors.map((c) => c.name).join(', ')}`}>
{product.colors.slice(0, 4).map((c) => (
<li class="sp-card__swatch" style={`--sw: ${c.hex}`} title={c.name} />
))}
{product.colors.length > 4 && <li class="sp-card__swatch-more">+{product.colors.length - 4}</li>}
</ul>
)}
<div class="sp-card__foot">
<p class="sp-card__price">
<span class="sp-card__now">{formatMoney(product.price, money)}</span>
{product.compareAt && (
<s class="sp-card__was"><span class="sr-only">Was </span>{formatMoney(product.compareAt, money)}</s>
)}
</p>
<button
class="sp-card__add"
type="button"
data-add-to-cart={cartPayload}
data-cart-key={cartKey}
data-open-cart={openCart ? undefined : 'false'}
data-added-label={addedLabel}
disabled={!inStock}
aria-label={inStock ? `${ctaLabel}: ${product.name}` : `${product.name} is sold out`}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 6h15l-1.5 9h-12z"/><path d="M6 6 5 3H2"/><circle cx="9" cy="20" r="1.4"/><circle cx="18" cy="20" r="1.4"/></svg>
<span data-label>{inStock ? ctaLabel : 'Sold out'}</span>
</button>
</div>
</div>
</article>
<script>
import { bindAddToCart } from './cart-store';
function init() {
bindAddToCart(document);
document.querySelectorAll<HTMLButtonElement>('[data-wishlist]:not([data-wish-bound])').forEach((btn) => {
btn.dataset.wishBound = '';
btn.addEventListener('click', () => {
btn.setAttribute('aria-pressed', String(btn.getAttribute('aria-pressed') !== 'true'));
});
});
}
init();
document.addEventListener('astro:page-load', init);
</script>
<style>
.sp-card {
container-type: inline-size;
position: relative;
display: flex;
flex-direction: column;
border-radius: var(--r-lg, 20px);
overflow: hidden;
background: var(--c-surface, rgb(var(--ink-rgb, 255 255 255) / 0.02));
border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05));
transition: border-color 0.25s ease, transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s ease;
}
.sp-card--glass {
background: linear-gradient(180deg, rgb(var(--ink-rgb, 255 255 255) / 0.035), rgb(var(--ink-rgb, 255 255 255) / 0.01));
backdrop-filter: blur(10px);
}
.sp-card--minimal { background: transparent; border-color: transparent; }
.sp-card--bordered { border-color: rgb(var(--ink-rgb, 255 255 255) / 0.12); }
.sp-card:hover {
border-color: rgba(212, 160, 23, 0.35);
transform: translateY(-3px);
box-shadow: 0 18px 40px -22px rgba(212, 160, 23, 0.45);
}
.sp-card:focus-within {
outline: 2px solid var(--c-primary-light, #d4a017);
outline-offset: 2px;
}
.sp-card__media { position: relative; border-radius: var(--r-lg, 20px) var(--r-lg, 20px) 0 0; }
.sp-card--minimal .sp-card__media { border-radius: var(--r-lg, 20px); }
.sp-card__badge { position: absolute; top: var(--sp-3, 0.75rem); left: var(--sp-3, 0.75rem); z-index: 3; }
.sp-card__wish {
position: absolute;
top: var(--sp-2, 0.5rem);
right: var(--sp-2, 0.5rem);
z-index: 3;
display: grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: var(--r-full, 9999px);
border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.1);
background: rgb(var(--paper-rgb, 5 5 5) / 0.55);
backdrop-filter: blur(8px);
color: var(--c-text-1, #fff);
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
}
.sp-card__wish:hover { transform: scale(1.06); }
.sp-card__wish[aria-pressed='true'] { color: var(--c-danger, #f87171); }
.sp-card__wish[aria-pressed='true'] svg { fill: currentColor; }
.sp-card__wish:focus-visible,
.sp-card__add:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
.sp-card__body {
display: flex;
flex-direction: column;
gap: var(--sp-2, 0.5rem);
padding: var(--sp-4, 1rem) var(--sp-4, 1rem) var(--sp-5, 1.25rem);
flex: 1;
}
.sp-card--sm .sp-card__body { padding: var(--sp-3, 0.75rem); }
.sp-card--lg .sp-card__body { padding: var(--sp-6, 1.5rem); }
.sp-card--minimal .sp-card__body { padding-inline: 0; }
.sp-card__cat {
margin: 0;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--c-primary-light, #d4a017);
}
.sp-card__title { margin: 0; font-size: 1rem; font-weight: 700; line-height: 1.3; }
.sp-card--lg .sp-card__title { font-size: 1.15rem; }
.sp-card__link { color: var(--c-text-1, #fff); text-decoration: none; }
.sp-card__link:focus-visible { outline: none; }
.sp-card__link::after { content: ''; position: absolute; inset: 0; z-index: 2; }
.sp-card__swatches { display: flex; align-items: center; gap: 6px; margin: 2px 0 0; padding: 0; list-style: none; }
.sp-card__swatch {
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--sw);
box-shadow: 0 0 0 1px rgb(var(--ink-rgb, 255 255 255) / 0.25);
}
.sp-card__swatch-more { font-size: 0.72rem; color: var(--c-text-2, #a3a3a3); }
.sp-card__foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-3, 0.75rem);
margin-top: auto;
padding-top: var(--sp-2, 0.5rem);
}
.sp-card__price { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 var(--sp-2, 0.5rem); margin: 0; }
.sp-card__now { font-size: 1.1rem; font-weight: 800; color: var(--c-text-1, #fff); }
.sp-card__was { font-size: 0.82rem; color: var(--c-text-2, #a3a3a3); }
.sp-card__add {
position: relative;
z-index: 3;
display: inline-flex;
align-items: center;
gap: 0.4rem;
min-height: 44px;
padding: 0 var(--sp-4, 1rem);
border: 0;
border-radius: var(--r-full, 9999px);
background: var(--c-primary-light, #d4a017);
color: var(--c-on-primary, #050505);
font: inherit;
font-size: 0.82rem;
font-weight: 700;
white-space: nowrap;
cursor: pointer;
transition: transform 0.2s ease, filter 0.2s ease, background 0.2s ease;
}
.sp-card__add:hover { filter: brightness(1.08); transform: translateY(-1px); }
.sp-card__add:disabled { background: rgb(var(--ink-rgb, 255 255 255) / 0.08); color: var(--c-text-2, #a3a3a3); cursor: not-allowed; transform: none; }
.sp-card__add.is-added { background: var(--c-success, #34d399); }
.sp-card--sm .sp-card__add span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.sp-card--sm .sp-card__add { width: 44px; padding: 0; justify-content: center; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
/* Narrow cards (2-up on phones): stack price above a full-width button */
@container (max-width: 250px) {
.sp-card__body { padding: var(--sp-3, 0.75rem) var(--sp-3, 0.75rem) var(--sp-4, 1rem); }
.sp-card__foot { flex-direction: column; align-items: stretch; gap: var(--sp-2, 0.5rem); }
.sp-card__add { justify-content: center; }
.sp-card__title { font-size: 0.92rem; }
}
@media (prefers-reduced-motion: reduce) {
.sp-card, .sp-card__add, .sp-card__wish { transition: none; }
.sp-card:hover { transform: none; }
}
</style>
Краткая информация
- Категория
- E-commerce
- Имя файла
StoreProductCard.astro- Зависимости
- Нет — чистый Astro + CSS
- Теги
- ecommerceshopcardproductadd-to-cartwishlist