Секции страниц headernavbarmega-menuecommerceeditorial
Shop Header Editorial
Редакционная шапка магазина: строка объявления, логотип по центру, мега-меню, переключатель темы и живой счётчик корзины.
Живое превью
Complimentary shipping over €150 · Free returns within 30 days
Использование
Скопируйте и вставьте этот код на вашу страницу Astro:
astro
---
import { ShopHeaderEditorial } from 'astro-component-kit/sections';
---
<ShopHeaderEditorial brand="Forma" bagHref="/bag/" links={[
{ label: 'Shop', href: '/shop/', columns: [{ title: 'Clothing', links: [{ label: 'Knitwear', href: '/shop/?category=knit' }] }] },
{ label: 'Journal', href: '/journal/' },
]} /> ---
import { ShopHeaderEditorial } from 'astro-component-kit/sections';
---
<ShopHeaderEditorial brand="Forma" bagHref="/bag/" links={[
{ label: 'Shop', href: '/shop/', columns: [{ title: 'Clothing', links: [{ label: 'Knitwear', href: '/shop/?category=knit' }] }] },
{ label: 'Journal', href: '/journal/' },
]} /> Ручная установка
Если вы не используете пакет npm, создайте файл и вставьте следующий код: src/components/lib/ShopHeaderEditorial.astro:
astro
---
/**
* ShopHeaderEditorial — Editorial store header: thin announcement line, left menu, centred
* wordmark, right utilities (search, theme toggle, bag count) and a full-width mega menu.
*
* @param {string} brand - Wordmark text. Default 'Forma'.
* @param {string} brandHref - Wordmark link. Default '/'.
* @param {string} announcement - Top line text (empty hides it).
* @param {Array} links - `{ label, href, columns?: { title, links: {label, href}[] }[], feature?: { title, text, href, icon?, hue? } }`.
* @param {string} searchHref - Search page URL (empty hides the icon). Default '#'.
* @param {string} bagHref - Bag page URL. When empty, the bag button opens a CartDrawer instead.
* @param {string} cartKey - Cart storage key for the live count. Default 'ack-cart'.
* @param {boolean} themeToggle - Show a light/dark toggle (html[data-theme]). Default true.
* @param {string} currentHref - Marks the active link.
* @param {boolean} sticky - Stick to the top on scroll. Default true.
* @param {string} variant - Style (paper|transparent|inverted). Default 'paper'.
* @param {string} size - Height (sm|md|lg). Default 'md'.
* @param {string} id - Base id. Default 'shop-header'.
* @param {string} className - Extra class.
*
* Accessibility: mega menus are disclosure buttons (aria-expanded) that close on Escape/outside
* click; the mobile menu is a disclosure too; the bag link label includes the live count.
*/
import ProductMedia from '../ecommerce/ProductMedia.astro';
interface Col { title: string; links: Array<{ label: string; href: string }> }
interface Feature { title: string; text?: string; href: string; icon?: string; hue?: number }
interface NavLink { label: string; href: string; columns?: Col[]; feature?: Feature }
interface Props {
brand?: string;
brandHref?: string;
announcement?: string;
links?: NavLink[];
searchHref?: string;
bagHref?: string;
cartKey?: string;
themeToggle?: boolean;
currentHref?: string;
sticky?: boolean;
variant?: 'paper' | 'transparent' | 'inverted';
size?: 'sm' | 'md' | 'lg';
id?: string;
className?: string;
}
const {
brand = 'Forma',
brandHref = '/',
announcement = 'Complimentary shipping over €150 · Free returns within 30 days',
links = [
{
label: 'Shop',
href: '#',
columns: [
{ title: 'Clothing', links: [{ label: 'Overshirts', href: '#' }, { label: 'Knitwear', href: '#' }, { label: 'Outerwear', href: '#' }] },
{ title: 'Objects', links: [{ label: 'Ceramics', href: '#' }, { label: 'Lighting', href: '#' }, { label: 'Plants', href: '#' }] },
{ title: 'Accessories', links: [{ label: 'Bags', href: '#' }, { label: 'Eyewear', href: '#' }, { label: 'Watches', href: '#' }] },
],
feature: { title: 'The Autumn edit', text: 'Twelve pieces in warm, quiet tones.', href: '#', icon: 'jacket', hue: 22 },
},
{ label: 'New in', href: '#' },
{ label: 'Journal', href: '#' },
],
searchHref = '#',
bagHref = '#',
cartKey = 'ack-cart',
themeToggle = true,
currentHref,
sticky = true,
variant = 'paper',
size = 'md',
id = 'shop-header',
className = '',
} = Astro.props;
---
<header class:list={['she', `she--${variant}`, `she--${size}`, { 'she--sticky': sticky }, className]} data-shop-header data-key={cartKey}>
{announcement && <p class="she__announce">{announcement}</p>}
<div class="she__bar">
<div class="she__left">
<button class="she__icon she__burger" type="button" aria-expanded="false" aria-controls={`${id}-nav`} aria-label="Menu" data-burger>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M3 8h18M3 16h18"/></svg>
</button>
<nav class="she__nav" id={`${id}-nav`} aria-label="Main" data-nav>
<ul>
{links.map((link, i) => (
<li>
{link.columns?.length ? (
<>
<button class="she__link she__link--desk" type="button" aria-expanded="false" aria-controls={`${id}-mega-${i}`} data-mega-toggle>{link.label}</button>
<a class="she__link she__link--mob" href={link.href}>{link.label}</a>
<ul class="she__sub">
{link.columns.flatMap((c) => c.links).map((l) => <li><a href={l.href}>{l.label}</a></li>)}
</ul>
</>
) : (
<a class="she__link" href={link.href} aria-current={currentHref === link.href ? 'page' : undefined}>{link.label}</a>
)}
</li>
))}
</ul>
</nav>
</div>
<a class="she__brand" href={brandHref}>{brand}</a>
<div class="she__right">
{searchHref && (
<a class="she__icon" href={searchHref} aria-label="Search">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
</a>
)}
{themeToggle && (
<button class="she__icon" type="button" aria-label="Switch theme" data-theme-btn>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="8"/><path d="M12 4a8 8 0 0 1 0 16z" fill="currentColor"/></svg>
</button>
)}
{bagHref ? (
<a class="she__bag" href={bagHref} aria-label="Bag" data-bag-link>
Bag <span class="she__count" data-count>0</span>
</a>
) : (
<button class="she__bag" type="button" aria-label="Bag" data-bag-link data-cart-open data-cart-key={cartKey}>
Bag <span class="she__count" data-count>0</span>
</button>
)}
</div>
</div>
{links.map((link, i) => link.columns?.length ? (
<div class="she__mega" id={`${id}-mega-${i}`} hidden data-mega>
<div class="she__mega-inner">
{link.columns.map((col) => (
<div class="she__col">
<p class="she__col-title">{col.title}</p>
<ul>{col.links.map((l) => <li><a href={l.href}>{l.label}</a></li>)}</ul>
</div>
))}
<a class="she__col she__col--all" href={link.href}>View everything <span aria-hidden="true">→</span></a>
{link.feature && (
<a class="she__feature" href={link.feature.href}>
<ProductMedia icon={link.feature.icon} hue={link.feature.hue} ratio="4 / 3" variant="soft" size="lg" />
<span class="she__feature-title">{link.feature.title}</span>
{link.feature.text && <span class="she__feature-text">{link.feature.text}</span>}
</a>
)}
</div>
</div>
) : null)}
</header>
<script>
import { createCart } from '../ecommerce/cart-store';
document.querySelectorAll<HTMLElement>('[data-shop-header]:not([data-bound])').forEach((header) => {
header.dataset.bound = '';
const toggles = Array.from(header.querySelectorAll<HTMLButtonElement>('[data-mega-toggle]'));
const panelOf = (b: HTMLButtonElement) => document.getElementById(b.getAttribute('aria-controls') || '');
const closeAll = (except?: HTMLButtonElement) => toggles.forEach((b) => {
if (b === except) return;
b.setAttribute('aria-expanded', 'false');
const p = panelOf(b);
if (p) p.hidden = true;
});
toggles.forEach((b) => b.addEventListener('click', () => {
const open = b.getAttribute('aria-expanded') !== 'true';
closeAll(b);
b.setAttribute('aria-expanded', String(open));
const p = panelOf(b);
if (p) p.hidden = !open;
}));
document.addEventListener('click', (e) => { if (!header.contains(e.target as Node)) closeAll(); });
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return;
const open = toggles.find((b) => b.getAttribute('aria-expanded') === 'true');
closeAll();
open?.focus();
const burger = header.querySelector<HTMLButtonElement>('[data-burger]');
if (burger?.getAttribute('aria-expanded') === 'true') { burger.click(); burger.focus(); }
});
const burger = header.querySelector<HTMLButtonElement>('[data-burger]');
const nav = header.querySelector<HTMLElement>('[data-nav]');
burger?.addEventListener('click', () => {
const open = burger.getAttribute('aria-expanded') !== 'true';
burger.setAttribute('aria-expanded', String(open));
nav?.classList.toggle('is-open', open);
});
header.querySelector('[data-theme-btn]')?.addEventListener('click', () => {
const root = document.documentElement;
const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
root.dataset.theme = next;
try { window.localStorage.setItem('ack-theme', next); } catch { /* ignore */ }
});
const bag = header.querySelector<HTMLElement>('[data-bag-link]');
const count = header.querySelector<HTMLElement>('[data-count]');
if (bag && count) {
const cart = createCart(header.dataset.key);
const render = () => {
const n = cart.count();
count.textContent = String(n);
bag.setAttribute('aria-label', `Bag, ${n} ${n === 1 ? 'item' : 'items'}`);
};
render();
cart.subscribe(() => {
render();
count.classList.remove('is-bump');
void count.offsetWidth;
count.classList.add('is-bump');
});
if (bag.tagName === 'BUTTON') {
bag.addEventListener('click', () => document.dispatchEvent(new CustomEvent('ack:cart-open', { detail: { key: cart.key, trigger: bag } })));
}
}
});
</script>
<style>
.she {
container-type: inline-size;
--she-h: 72px;
position: relative;
z-index: 50;
color: var(--c-text-1, #fff);
background: var(--c-bg, #050505);
border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.1));
}
.she--sm { --she-h: 60px; }
.she--lg { --she-h: 88px; }
.she--sticky { position: sticky; top: 0; }
.she--transparent { background: transparent; border-bottom-color: transparent; }
.she--inverted { background: var(--c-text-1, #fff); color: var(--c-bg, #050505); }
.she__announce {
margin: 0;
padding: 0.5rem var(--sp-4, 1rem);
border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.1));
font-size: 0.72rem;
letter-spacing: 0.12em;
text-align: center;
text-transform: uppercase;
color: var(--c-text-2, #a3a3a3);
}
.she__bar {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: var(--sp-4, 1rem);
max-width: 1440px;
height: var(--she-h);
margin: 0 auto;
padding: 0 clamp(1rem, 3vw, 2.5rem);
}
.she__left, .she__right { display: flex; align-items: center; gap: var(--sp-1, 0.25rem); }
.she__right { justify-content: flex-end; }
.she__brand {
font-family: var(--font-display, Georgia, 'Times New Roman', serif);
font-size: clamp(1.6rem, 3vw, 2.1rem);
font-weight: 400;
letter-spacing: -0.03em;
color: inherit;
text-decoration: none;
}
.she__brand::after { content: '°'; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
.she__nav ul { display: flex; gap: var(--sp-1, 0.25rem); margin: 0; padding: 0; list-style: none; }
.she__link {
position: relative;
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 0 0.75rem;
border: 0;
background: none;
color: inherit;
font: inherit;
font-size: 0.82rem;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
text-decoration: none;
cursor: pointer;
}
.she__link::after {
content: '';
position: absolute;
left: 0.75rem;
right: 0.75rem;
bottom: 10px;
height: 1px;
background: currentColor;
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
}
.she__link:hover::after, .she__link[aria-expanded='true']::after, .she__link[aria-current='page']::after { transform: scaleX(1); }
.she__icon, .she__bag {
display: inline-grid;
place-items: center;
min-width: 44px;
height: 44px;
border: 0;
background: none;
color: inherit;
cursor: pointer;
text-decoration: none;
}
.she__bag { display: inline-flex; align-items: center; gap: 6px; padding: 0 0.5rem; font: inherit; font-size: 0.82rem; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; }
.she__count {
display: inline-grid;
place-items: center;
min-width: 22px;
height: 22px;
padding: 0 6px;
border-radius: 999px;
background: var(--c-text-1, #fff);
color: var(--c-bg, #050505);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0;
}
.she--inverted .she__count { background: var(--c-bg, #050505); color: var(--c-text-1, #fff); }
.she__count.is-bump { animation: she-bump 0.45s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes she-bump { 40% { transform: scale(1.3); background: var(--shop-accent, var(--c-primary-light, #d4a017)); } }
.she :is(a, button):focus-visible { outline: 2px solid var(--shop-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
.she__burger { display: none; }
.she__link--mob, .she__sub { display: none !important; }
.she__mega {
position: absolute;
left: 0;
right: 0;
top: 100%;
background: var(--c-bg, #050505);
border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.1));
box-shadow: 0 40px 60px -40px rgba(0, 0, 0, 0.5);
animation: she-drop 0.25s ease-out;
}
.she__mega[hidden] { display: none; }
.she__mega-inner {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(0, 0.8fr) minmax(0, 1.4fr);
gap: var(--sp-8, 2rem);
max-width: 1440px;
margin: 0 auto;
padding: var(--sp-8, 2rem) clamp(1rem, 3vw, 2.5rem) var(--sp-10, 2.5rem);
}
.she__col-title { margin: 0 0 var(--sp-3, 0.75rem); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--c-text-2, #a3a3a3); }
.she__col ul { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }
.she__col a { display: inline-flex; min-height: 36px; align-items: center; font-family: var(--font-display, Georgia, serif); font-size: 1.15rem; color: var(--c-text-1, #fff); text-decoration: none; }
.she__col a:hover { color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
.she__col--all { align-self: end; font-family: inherit !important; font-size: 0.85rem !important; text-decoration: underline !important; text-underline-offset: 4px; }
.she__feature { display: grid; gap: 6px; color: inherit; text-decoration: none; }
.she__feature :global(.p-media) { border-radius: var(--r-sm, 2px); }
.she__feature-title { font-family: var(--font-display, Georgia, serif); font-size: 1.2rem; color: var(--c-text-1, #fff); }
.she__feature-text { font-size: 0.85rem; color: var(--c-text-2, #a3a3a3); }
@keyframes she-drop { from { opacity: 0; transform: translateY(-6px); } }
@container (max-width: 900px) {
.she__burger { display: inline-grid; }
.she__nav {
position: absolute;
left: 0;
right: 0;
top: 100%;
display: none;
padding: var(--sp-4, 1rem) clamp(1rem, 3vw, 2.5rem) var(--sp-6, 1.5rem);
background: var(--c-bg, #050505);
border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.1));
}
.she__nav.is-open { display: block; }
.she__nav ul { flex-direction: column; }
.she__link { width: 100%; min-height: 52px; padding: 0; font-family: var(--font-display, Georgia, serif); font-size: 1.6rem; letter-spacing: -0.01em; text-transform: none; }
.she__link::after { display: none; }
.she__mega-inner { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.she__feature { grid-column: 1 / -1; }
.she__right [aria-label='Search'] { display: none; }
.she__link--desk, .she__mega { display: none !important; }
.she__link--mob { display: inline-flex !important; }
.she__sub { display: flex !important; flex-direction: row !important; flex-wrap: wrap; gap: 0 1rem !important; padding-bottom: var(--sp-3, 0.75rem) !important; }
.she__sub a { display: inline-flex; min-height: 40px; align-items: center; font-size: 0.9rem; color: var(--c-text-2, #a3a3a3); text-decoration: none; }
}
@container (max-width: 480px) {
.she__bag { font-size: 0; gap: 0; }
.she__count { font-size: 0.7rem; }
}
@media (prefers-reduced-motion: reduce) {
.she__mega, .she__count.is-bump { animation: none; }
.she__link::after { transition: none; }
}
</style> ---
/**
* ShopHeaderEditorial — Editorial store header: thin announcement line, left menu, centred
* wordmark, right utilities (search, theme toggle, bag count) and a full-width mega menu.
*
* @param {string} brand - Wordmark text. Default 'Forma'.
* @param {string} brandHref - Wordmark link. Default '/'.
* @param {string} announcement - Top line text (empty hides it).
* @param {Array} links - `{ label, href, columns?: { title, links: {label, href}[] }[], feature?: { title, text, href, icon?, hue? } }`.
* @param {string} searchHref - Search page URL (empty hides the icon). Default '#'.
* @param {string} bagHref - Bag page URL. When empty, the bag button opens a CartDrawer instead.
* @param {string} cartKey - Cart storage key for the live count. Default 'ack-cart'.
* @param {boolean} themeToggle - Show a light/dark toggle (html[data-theme]). Default true.
* @param {string} currentHref - Marks the active link.
* @param {boolean} sticky - Stick to the top on scroll. Default true.
* @param {string} variant - Style (paper|transparent|inverted). Default 'paper'.
* @param {string} size - Height (sm|md|lg). Default 'md'.
* @param {string} id - Base id. Default 'shop-header'.
* @param {string} className - Extra class.
*
* Accessibility: mega menus are disclosure buttons (aria-expanded) that close on Escape/outside
* click; the mobile menu is a disclosure too; the bag link label includes the live count.
*/
import ProductMedia from '../ecommerce/ProductMedia.astro';
interface Col { title: string; links: Array<{ label: string; href: string }> }
interface Feature { title: string; text?: string; href: string; icon?: string; hue?: number }
interface NavLink { label: string; href: string; columns?: Col[]; feature?: Feature }
interface Props {
brand?: string;
brandHref?: string;
announcement?: string;
links?: NavLink[];
searchHref?: string;
bagHref?: string;
cartKey?: string;
themeToggle?: boolean;
currentHref?: string;
sticky?: boolean;
variant?: 'paper' | 'transparent' | 'inverted';
size?: 'sm' | 'md' | 'lg';
id?: string;
className?: string;
}
const {
brand = 'Forma',
brandHref = '/',
announcement = 'Complimentary shipping over €150 · Free returns within 30 days',
links = [
{
label: 'Shop',
href: '#',
columns: [
{ title: 'Clothing', links: [{ label: 'Overshirts', href: '#' }, { label: 'Knitwear', href: '#' }, { label: 'Outerwear', href: '#' }] },
{ title: 'Objects', links: [{ label: 'Ceramics', href: '#' }, { label: 'Lighting', href: '#' }, { label: 'Plants', href: '#' }] },
{ title: 'Accessories', links: [{ label: 'Bags', href: '#' }, { label: 'Eyewear', href: '#' }, { label: 'Watches', href: '#' }] },
],
feature: { title: 'The Autumn edit', text: 'Twelve pieces in warm, quiet tones.', href: '#', icon: 'jacket', hue: 22 },
},
{ label: 'New in', href: '#' },
{ label: 'Journal', href: '#' },
],
searchHref = '#',
bagHref = '#',
cartKey = 'ack-cart',
themeToggle = true,
currentHref,
sticky = true,
variant = 'paper',
size = 'md',
id = 'shop-header',
className = '',
} = Astro.props;
---
<header class:list={['she', `she--${variant}`, `she--${size}`, { 'she--sticky': sticky }, className]} data-shop-header data-key={cartKey}>
{announcement && <p class="she__announce">{announcement}</p>}
<div class="she__bar">
<div class="she__left">
<button class="she__icon she__burger" type="button" aria-expanded="false" aria-controls={`${id}-nav`} aria-label="Menu" data-burger>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M3 8h18M3 16h18"/></svg>
</button>
<nav class="she__nav" id={`${id}-nav`} aria-label="Main" data-nav>
<ul>
{links.map((link, i) => (
<li>
{link.columns?.length ? (
<>
<button class="she__link she__link--desk" type="button" aria-expanded="false" aria-controls={`${id}-mega-${i}`} data-mega-toggle>{link.label}</button>
<a class="she__link she__link--mob" href={link.href}>{link.label}</a>
<ul class="she__sub">
{link.columns.flatMap((c) => c.links).map((l) => <li><a href={l.href}>{l.label}</a></li>)}
</ul>
</>
) : (
<a class="she__link" href={link.href} aria-current={currentHref === link.href ? 'page' : undefined}>{link.label}</a>
)}
</li>
))}
</ul>
</nav>
</div>
<a class="she__brand" href={brandHref}>{brand}</a>
<div class="she__right">
{searchHref && (
<a class="she__icon" href={searchHref} aria-label="Search">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
</a>
)}
{themeToggle && (
<button class="she__icon" type="button" aria-label="Switch theme" data-theme-btn>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="8"/><path d="M12 4a8 8 0 0 1 0 16z" fill="currentColor"/></svg>
</button>
)}
{bagHref ? (
<a class="she__bag" href={bagHref} aria-label="Bag" data-bag-link>
Bag <span class="she__count" data-count>0</span>
</a>
) : (
<button class="she__bag" type="button" aria-label="Bag" data-bag-link data-cart-open data-cart-key={cartKey}>
Bag <span class="she__count" data-count>0</span>
</button>
)}
</div>
</div>
{links.map((link, i) => link.columns?.length ? (
<div class="she__mega" id={`${id}-mega-${i}`} hidden data-mega>
<div class="she__mega-inner">
{link.columns.map((col) => (
<div class="she__col">
<p class="she__col-title">{col.title}</p>
<ul>{col.links.map((l) => <li><a href={l.href}>{l.label}</a></li>)}</ul>
</div>
))}
<a class="she__col she__col--all" href={link.href}>View everything <span aria-hidden="true">→</span></a>
{link.feature && (
<a class="she__feature" href={link.feature.href}>
<ProductMedia icon={link.feature.icon} hue={link.feature.hue} ratio="4 / 3" variant="soft" size="lg" />
<span class="she__feature-title">{link.feature.title}</span>
{link.feature.text && <span class="she__feature-text">{link.feature.text}</span>}
</a>
)}
</div>
</div>
) : null)}
</header>
<script>
import { createCart } from '../ecommerce/cart-store';
document.querySelectorAll<HTMLElement>('[data-shop-header]:not([data-bound])').forEach((header) => {
header.dataset.bound = '';
const toggles = Array.from(header.querySelectorAll<HTMLButtonElement>('[data-mega-toggle]'));
const panelOf = (b: HTMLButtonElement) => document.getElementById(b.getAttribute('aria-controls') || '');
const closeAll = (except?: HTMLButtonElement) => toggles.forEach((b) => {
if (b === except) return;
b.setAttribute('aria-expanded', 'false');
const p = panelOf(b);
if (p) p.hidden = true;
});
toggles.forEach((b) => b.addEventListener('click', () => {
const open = b.getAttribute('aria-expanded') !== 'true';
closeAll(b);
b.setAttribute('aria-expanded', String(open));
const p = panelOf(b);
if (p) p.hidden = !open;
}));
document.addEventListener('click', (e) => { if (!header.contains(e.target as Node)) closeAll(); });
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return;
const open = toggles.find((b) => b.getAttribute('aria-expanded') === 'true');
closeAll();
open?.focus();
const burger = header.querySelector<HTMLButtonElement>('[data-burger]');
if (burger?.getAttribute('aria-expanded') === 'true') { burger.click(); burger.focus(); }
});
const burger = header.querySelector<HTMLButtonElement>('[data-burger]');
const nav = header.querySelector<HTMLElement>('[data-nav]');
burger?.addEventListener('click', () => {
const open = burger.getAttribute('aria-expanded') !== 'true';
burger.setAttribute('aria-expanded', String(open));
nav?.classList.toggle('is-open', open);
});
header.querySelector('[data-theme-btn]')?.addEventListener('click', () => {
const root = document.documentElement;
const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
root.dataset.theme = next;
try { window.localStorage.setItem('ack-theme', next); } catch { /* ignore */ }
});
const bag = header.querySelector<HTMLElement>('[data-bag-link]');
const count = header.querySelector<HTMLElement>('[data-count]');
if (bag && count) {
const cart = createCart(header.dataset.key);
const render = () => {
const n = cart.count();
count.textContent = String(n);
bag.setAttribute('aria-label', `Bag, ${n} ${n === 1 ? 'item' : 'items'}`);
};
render();
cart.subscribe(() => {
render();
count.classList.remove('is-bump');
void count.offsetWidth;
count.classList.add('is-bump');
});
if (bag.tagName === 'BUTTON') {
bag.addEventListener('click', () => document.dispatchEvent(new CustomEvent('ack:cart-open', { detail: { key: cart.key, trigger: bag } })));
}
}
});
</script>
<style>
.she {
container-type: inline-size;
--she-h: 72px;
position: relative;
z-index: 50;
color: var(--c-text-1, #fff);
background: var(--c-bg, #050505);
border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.1));
}
.she--sm { --she-h: 60px; }
.she--lg { --she-h: 88px; }
.she--sticky { position: sticky; top: 0; }
.she--transparent { background: transparent; border-bottom-color: transparent; }
.she--inverted { background: var(--c-text-1, #fff); color: var(--c-bg, #050505); }
.she__announce {
margin: 0;
padding: 0.5rem var(--sp-4, 1rem);
border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.1));
font-size: 0.72rem;
letter-spacing: 0.12em;
text-align: center;
text-transform: uppercase;
color: var(--c-text-2, #a3a3a3);
}
.she__bar {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: var(--sp-4, 1rem);
max-width: 1440px;
height: var(--she-h);
margin: 0 auto;
padding: 0 clamp(1rem, 3vw, 2.5rem);
}
.she__left, .she__right { display: flex; align-items: center; gap: var(--sp-1, 0.25rem); }
.she__right { justify-content: flex-end; }
.she__brand {
font-family: var(--font-display, Georgia, 'Times New Roman', serif);
font-size: clamp(1.6rem, 3vw, 2.1rem);
font-weight: 400;
letter-spacing: -0.03em;
color: inherit;
text-decoration: none;
}
.she__brand::after { content: '°'; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
.she__nav ul { display: flex; gap: var(--sp-1, 0.25rem); margin: 0; padding: 0; list-style: none; }
.she__link {
position: relative;
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 0 0.75rem;
border: 0;
background: none;
color: inherit;
font: inherit;
font-size: 0.82rem;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
text-decoration: none;
cursor: pointer;
}
.she__link::after {
content: '';
position: absolute;
left: 0.75rem;
right: 0.75rem;
bottom: 10px;
height: 1px;
background: currentColor;
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
}
.she__link:hover::after, .she__link[aria-expanded='true']::after, .she__link[aria-current='page']::after { transform: scaleX(1); }
.she__icon, .she__bag {
display: inline-grid;
place-items: center;
min-width: 44px;
height: 44px;
border: 0;
background: none;
color: inherit;
cursor: pointer;
text-decoration: none;
}
.she__bag { display: inline-flex; align-items: center; gap: 6px; padding: 0 0.5rem; font: inherit; font-size: 0.82rem; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; }
.she__count {
display: inline-grid;
place-items: center;
min-width: 22px;
height: 22px;
padding: 0 6px;
border-radius: 999px;
background: var(--c-text-1, #fff);
color: var(--c-bg, #050505);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0;
}
.she--inverted .she__count { background: var(--c-bg, #050505); color: var(--c-text-1, #fff); }
.she__count.is-bump { animation: she-bump 0.45s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes she-bump { 40% { transform: scale(1.3); background: var(--shop-accent, var(--c-primary-light, #d4a017)); } }
.she :is(a, button):focus-visible { outline: 2px solid var(--shop-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
.she__burger { display: none; }
.she__link--mob, .she__sub { display: none !important; }
.she__mega {
position: absolute;
left: 0;
right: 0;
top: 100%;
background: var(--c-bg, #050505);
border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.1));
box-shadow: 0 40px 60px -40px rgba(0, 0, 0, 0.5);
animation: she-drop 0.25s ease-out;
}
.she__mega[hidden] { display: none; }
.she__mega-inner {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(0, 0.8fr) minmax(0, 1.4fr);
gap: var(--sp-8, 2rem);
max-width: 1440px;
margin: 0 auto;
padding: var(--sp-8, 2rem) clamp(1rem, 3vw, 2.5rem) var(--sp-10, 2.5rem);
}
.she__col-title { margin: 0 0 var(--sp-3, 0.75rem); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--c-text-2, #a3a3a3); }
.she__col ul { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }
.she__col a { display: inline-flex; min-height: 36px; align-items: center; font-family: var(--font-display, Georgia, serif); font-size: 1.15rem; color: var(--c-text-1, #fff); text-decoration: none; }
.she__col a:hover { color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
.she__col--all { align-self: end; font-family: inherit !important; font-size: 0.85rem !important; text-decoration: underline !important; text-underline-offset: 4px; }
.she__feature { display: grid; gap: 6px; color: inherit; text-decoration: none; }
.she__feature :global(.p-media) { border-radius: var(--r-sm, 2px); }
.she__feature-title { font-family: var(--font-display, Georgia, serif); font-size: 1.2rem; color: var(--c-text-1, #fff); }
.she__feature-text { font-size: 0.85rem; color: var(--c-text-2, #a3a3a3); }
@keyframes she-drop { from { opacity: 0; transform: translateY(-6px); } }
@container (max-width: 900px) {
.she__burger { display: inline-grid; }
.she__nav {
position: absolute;
left: 0;
right: 0;
top: 100%;
display: none;
padding: var(--sp-4, 1rem) clamp(1rem, 3vw, 2.5rem) var(--sp-6, 1.5rem);
background: var(--c-bg, #050505);
border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.1));
}
.she__nav.is-open { display: block; }
.she__nav ul { flex-direction: column; }
.she__link { width: 100%; min-height: 52px; padding: 0; font-family: var(--font-display, Georgia, serif); font-size: 1.6rem; letter-spacing: -0.01em; text-transform: none; }
.she__link::after { display: none; }
.she__mega-inner { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.she__feature { grid-column: 1 / -1; }
.she__right [aria-label='Search'] { display: none; }
.she__link--desk, .she__mega { display: none !important; }
.she__link--mob { display: inline-flex !important; }
.she__sub { display: flex !important; flex-direction: row !important; flex-wrap: wrap; gap: 0 1rem !important; padding-bottom: var(--sp-3, 0.75rem) !important; }
.she__sub a { display: inline-flex; min-height: 40px; align-items: center; font-size: 0.9rem; color: var(--c-text-2, #a3a3a3); text-decoration: none; }
}
@container (max-width: 480px) {
.she__bag { font-size: 0; gap: 0; }
.she__count { font-size: 0.7rem; }
}
@media (prefers-reduced-motion: reduce) {
.she__mega, .she__count.is-bump { animation: none; }
.she__link::after { transition: none; }
}
</style>
Краткая информация
- Категория
- Секции страниц
- Имя файла
ShopHeaderEditorial.astro- Зависимости
- Нет — чистый Astro + CSS
- Теги
- headernavbarmega-menuecommerceeditorial