1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
E-commerce

Shop Header Editorial

Cabecera editorial de tienda: línea de aviso, logotipo centrado, mega menú con destacado, cambio de tema y contador de la bolsa en vivo.

headernavbarmega-menuecommerceeditorial

Uso

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

tsx
import { ShopHeaderEditorial } from 'react-component-kit-ui/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/' },
]} />

Instalación Manual

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

ShopHeaderEditorial.tsx
'use client';

import { useEffect, useRef } from 'react';
import ProductMedia from '../ecommerce/ProductMedia';
import { createCart } from '../ecommerce/cart-store';
import './ShopHeaderEditorial.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 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 }

/**
 * 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.
 */
export interface ShopHeaderEditorialProps {
  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;
}

export function ShopHeaderEditorial({
  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 = '',
}: ShopHeaderEditorialProps) {

  // Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
  const mounted = useRef(false);
  useEffect(() => {
    if (mounted.current) return;
    mounted.current = true;
    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 } })));
          }
        }
      });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <header className={cx(['she', `she--${variant}`, `she--${size}`, { 'she--sticky': sticky }, className])} data-shop-header data-key={cartKey} data-rk-74ee75="">
        {announcement && <p className="she__announce" data-rk-74ee75="">{announcement}</p>}
        <div className="she__bar" data-rk-74ee75="">
          <div className="she__left" data-rk-74ee75="">
            <button className="she__icon she__burger" type="button" aria-expanded="false" aria-controls={`${id}-nav`} aria-label="Menu" data-burger data-rk-74ee75="">
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden="true" data-rk-74ee75=""><path d="M3 8h18M3 16h18" data-rk-74ee75="" /></svg>
            </button>
            <nav className="she__nav" id={`${id}-nav`} aria-label="Main" data-nav data-rk-74ee75="">
              <ul data-rk-74ee75="">
                {links.map((link, i) => (
                  <li key={i} data-rk-74ee75="">
                    {link.columns?.length ? (
                      <>
                        <button className="she__link she__link--desk" type="button" aria-expanded="false" aria-controls={`${id}-mega-${i}`} data-mega-toggle data-rk-74ee75="">{link.label}</button>
                        <a className="she__link she__link--mob" href={link.href} data-rk-74ee75="">{link.label}</a>
                        <ul className="she__sub" data-rk-74ee75="">
                          {link.columns.flatMap((c) => c.links).map((l, __i: number) => <li key={__i} data-rk-74ee75=""><a href={l.href} data-rk-74ee75="">{l.label}</a></li>)}
                        </ul>
                      </>
                    ) : (
                      <a className="she__link" href={link.href} aria-current={currentHref === link.href ? 'page' : undefined} data-rk-74ee75="">{link.label}</a>
                    )}
                  </li>
                ))}
              </ul>
            </nav>
          </div>
      
          <a className="she__brand" href={brandHref} data-rk-74ee75="">{brand}</a>
      
          <div className="she__right" data-rk-74ee75="">
            {searchHref && (
              <a className="she__icon" href={searchHref} aria-label="Search" data-rk-74ee75="">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden="true" data-rk-74ee75=""><circle cx="11" cy="11" r="7" data-rk-74ee75="" /><path d="m20 20-3.5-3.5" data-rk-74ee75="" /></svg>
              </a>
            )}
            {themeToggle && (
              <button className="she__icon" type="button" aria-label="Switch theme" data-theme-btn data-rk-74ee75="">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" aria-hidden="true" data-rk-74ee75=""><circle cx="12" cy="12" r="8" data-rk-74ee75="" /><path d="M12 4a8 8 0 0 1 0 16z" fill="currentColor" data-rk-74ee75="" /></svg>
              </button>
            )}
            {bagHref ? (
              <a className="she__bag" href={bagHref} aria-label="Bag" data-bag-link data-rk-74ee75="">
                Bag <span className="she__count" data-count data-rk-74ee75="">0</span>
              </a>
            ) : (
              <button className="she__bag" type="button" aria-label="Bag" data-bag-link data-cart-open data-cart-key={cartKey} data-rk-74ee75="">
                Bag <span className="she__count" data-count data-rk-74ee75="">0</span>
              </button>
            )}
          </div>
        </div>
      
        {links.map((link, i) => link.columns?.length ? (
          <div className="she__mega" id={`${id}-mega-${i}`} hidden data-mega data-rk-74ee75="">
            <div className="she__mega-inner" data-rk-74ee75="">
              {link.columns.map((col, __i: number) => (
                <div key={__i} className="she__col" data-rk-74ee75="">
                  <p className="she__col-title" data-rk-74ee75="">{col.title}</p>
                  <ul data-rk-74ee75="">{col.links.map((l, __i: number) => <li key={__i} data-rk-74ee75=""><a href={l.href} data-rk-74ee75="">{l.label}</a></li>)}</ul>
                </div>
              ))}
              <a className="she__col she__col--all" href={link.href} data-rk-74ee75="">View everything <span aria-hidden="true" data-rk-74ee75="">→</span></a>
              {link.feature && (
                <a className="she__feature" href={link.feature.href} data-rk-74ee75="">
                  <ProductMedia icon={link.feature.icon} hue={link.feature.hue} ratio="4 / 3" variant="soft" size="lg" />
                  <span className="she__feature-title" data-rk-74ee75="">{link.feature.title}</span>
                  {link.feature.text && <span className="she__feature-text" data-rk-74ee75="">{link.feature.text}</span>}
                </a>
              )}
            </div>
          </div>
        ) : null)}
      </header>
    </>
  );
}

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

.she:where([data-rk-74ee75]) {
  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:where([data-rk-74ee75]) { --she-h: 60px; }
.she--lg:where([data-rk-74ee75]) { --she-h: 88px; }
.she--sticky:where([data-rk-74ee75]) { position: sticky; top: 0; }
.she--transparent:where([data-rk-74ee75]) { background: transparent; border-bottom-color: transparent; }
.she--inverted:where([data-rk-74ee75]) { background: var(--c-text-1, #fff); color: var(--c-bg, #050505); }

.she__announce:where([data-rk-74ee75]) {
  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:where([data-rk-74ee75]) {
  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:where([data-rk-74ee75]), .she__right:where([data-rk-74ee75]) { display: flex; align-items: center; gap: var(--sp-1, 0.25rem); }
.she__right:where([data-rk-74ee75]) { justify-content: flex-end; }
.she__brand:where([data-rk-74ee75]) {
  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:where([data-rk-74ee75])::after { content: '°'; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }

.she__nav:where([data-rk-74ee75]) ul:where([data-rk-74ee75]) { display: flex; gap: var(--sp-1, 0.25rem); margin: 0; padding: 0; list-style: none; }
.she__link:where([data-rk-74ee75]) {
  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:where([data-rk-74ee75])::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:where([data-rk-74ee75])::after, .she__link[aria-expanded='true']:where([data-rk-74ee75])::after, .she__link[aria-current='page']:where([data-rk-74ee75])::after { transform: scaleX(1); }

.she__icon:where([data-rk-74ee75]), .she__bag:where([data-rk-74ee75]) {
  display: inline-grid;
  place-items: center;
  min-width: 44px;
  height: 44px;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
  text-decoration: none;
}
.she__bag:where([data-rk-74ee75]) { 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:where([data-rk-74ee75]) {
  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:where([data-rk-74ee75]) .she__count:where([data-rk-74ee75]) { background: var(--c-bg, #050505); color: var(--c-text-1, #fff); }
.she__count.is-bump:where([data-rk-74ee75]) { animation: shop-header-editorial-she-bump 0.45s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes shop-header-editorial-she-bump { 40% { transform: scale(1.3); background: var(--shop-accent, var(--c-primary-light, #d4a017)); } }
.she:where([data-rk-74ee75]) :is(a, button):focus-visible:where([data-rk-74ee75]) { outline: 2px solid var(--shop-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
.she__burger:where([data-rk-74ee75]) { display: none; }
.she__link--mob:where([data-rk-74ee75]), .she__sub:where([data-rk-74ee75]) { display: none !important; }

.she__mega:where([data-rk-74ee75]) {
  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: shop-header-editorial-she-drop 0.25s ease-out;
}
.she__mega[hidden]:where([data-rk-74ee75]) { display: none; }
.she__mega-inner:where([data-rk-74ee75]) {
  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:where([data-rk-74ee75]) { 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:where([data-rk-74ee75]) ul:where([data-rk-74ee75]) { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }
.she__col:where([data-rk-74ee75]) a:where([data-rk-74ee75]) { 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:where([data-rk-74ee75]) a:hover:where([data-rk-74ee75]) { color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
.she__col--all:where([data-rk-74ee75]) { align-self: end; font-family: inherit !important; font-size: 0.85rem !important; text-decoration: underline !important; text-underline-offset: 4px; }
.she__feature:where([data-rk-74ee75]) { display: grid; gap: 6px; color: inherit; text-decoration: none; }
.she__feature:where([data-rk-74ee75]) .p-media { border-radius: var(--r-sm, 2px); }
.she__feature-title:where([data-rk-74ee75]) { font-family: var(--font-display, Georgia, serif); font-size: 1.2rem; color: var(--c-text-1, #fff); }
.she__feature-text:where([data-rk-74ee75]) { font-size: 0.85rem; color: var(--c-text-2, #a3a3a3); }
@keyframes shop-header-editorial-she-drop { from { opacity: 0; transform: translateY(-6px); } }

@container (max-width: 900px) {
  .she__burger:where([data-rk-74ee75]) { display: inline-grid; }
  .she__nav:where([data-rk-74ee75]) {
    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:where([data-rk-74ee75]) { display: block; }
  .she__nav:where([data-rk-74ee75]) ul:where([data-rk-74ee75]) { flex-direction: column; }
  .she__link:where([data-rk-74ee75]) { 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:where([data-rk-74ee75])::after { display: none; }
  .she__mega-inner:where([data-rk-74ee75]) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .she__feature:where([data-rk-74ee75]) { grid-column: 1 / -1; }
  .she__right:where([data-rk-74ee75]) [aria-label='Search']:where([data-rk-74ee75]) { display: none; }
  .she__link--desk:where([data-rk-74ee75]), .she__mega:where([data-rk-74ee75]) { display: none !important; }
  .she__link--mob:where([data-rk-74ee75]) { display: inline-flex !important; }
  .she__sub:where([data-rk-74ee75]) { display: flex !important; flex-direction: row !important; flex-wrap: wrap; gap: 0 1rem !important; padding-bottom: var(--sp-3, 0.75rem) !important; }
  .she__sub:where([data-rk-74ee75]) a:where([data-rk-74ee75]) { 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:where([data-rk-74ee75]) { font-size: 0; gap: 0; }
  .she__count:where([data-rk-74ee75]) { font-size: 0.7rem; }
}
@media (prefers-reduced-motion: reduce) {
  .she__mega:where([data-rk-74ee75]), .she__count.is-bump:where([data-rk-74ee75]) { animation: none; }
  .she__link:where([data-rk-74ee75])::after { transition: none; }
}