// parts.jsx — shared UI: Nav, Footer, Pattern overlay, Marquee
const { useEffect, useState, useRef, useMemo } = React;
// ───────────────────────────────────────────────────────────────────
// Pattern overlay — line-art watermark using ArtFace/Hand/Blob
// Density tweakable: off | subtle | regular | strong
// ───────────────────────────────────────────────────────────────────
const PatternBackdrop = ({ density = 'subtle', accent = 'var(--accent)' }) => {
if (density === 'off') return null;
const opacity = { subtle: 0.18, regular: 0.30, strong: 0.45 }[density] || 0.18;
// Static layout — feels intentional, not random
const items = [
{ x: 4, y: 6, w: 130, r: -8, k: 'face' },
{ x: 78, y: 12, w: 90, r: 14, k: 'hand' },
{ x: 36, y: 28, w: 160, r: 6, k: 'blob' },
{ x: 86, y: 44, w: 110, r: -22, k: 'face' },
{ x: 6, y: 56, w: 140, r: 18, k: 'blob' },
{ x: 62, y: 70, w: 100, r: -6, k: 'hand' },
{ x: 28, y: 86, w: 130, r: 12, k: 'blob' },
{ x: 88, y: 92, w: 90, r: 28, k: 'hand' }];
return (
{items.map((it, i) => {
const Comp = it.k === 'face' ? ArtFace : it.k === 'hand' ? ArtHand : ArtBlob;
return (
);
})}
);
};
// ───────────────────────────────────────────────────────────────────
// Wordmark + nav
// ───────────────────────────────────────────────────────────────────
const Wordmark = ({ size = 28, ink = 'var(--ink)' }) =>
;
const Nav = ({ route, setRoute, copy, onOpenTelegram, scrolled }) => {
const [mobileOpen, setMobileOpen] = useState(false);
// Close menu on route change
useEffect(() => { setMobileOpen(false); }, [route]);
const link = (key, label) =>
setRoute(key)}
className={`nav-link eyebrow py-2${route === key ? ' nav-active' : ''}`}
style={{
color: route === key ? 'var(--ink)' : 'var(--ink-3)',
fontSize: '13px'
}}>
{label}
;
const mobileLink = (key, label) =>
{ setRoute(key); setMobileOpen(false); }}
className="w-full text-left display py-5 border-b"
style={{
fontSize: 'clamp(28px, 7vw, 40px)',
borderColor: 'var(--hair)',
fontStyle: route === key ? 'italic' : 'normal',
color: route === key ? 'var(--ink)' : 'var(--ink-3)',
fontWeight: 500,
letterSpacing: '-0.01em',
lineHeight: 1.05
}}>
{label}
;
return (
<>
setRoute('home')} className="flex items-center" aria-label="Артштуки — на главную">
{link('home', copy.nav.home)}
{link('gallery', copy.nav.gallery)}
{link('about', copy.nav.about)}
{link('order', copy.nav.order)}
Написать нам
{/* mobile: hamburger + telegram */}
setMobileOpen(o => !o)}
className="inline-flex flex-col items-center justify-center w-10 h-10 gap-[5px]"
aria-label={mobileOpen ? 'Закрыть меню' : 'Открыть меню'}>
{/* mobile menu panel */}
{mobileLink('home', copy.nav.home)}
{mobileLink('gallery', copy.nav.gallery)}
{mobileLink('about', copy.nav.about)}
{mobileLink('order', copy.nav.order)}
{/* backdrop when menu open */}
{mobileOpen && setMobileOpen(false)} />}
>);
};
// ───────────────────────────────────────────────────────────────────
// Footer
// ───────────────────────────────────────────────────────────────────
const Footer = ({ copy, onOpenTelegram, setRoute }) =>
;
// ───────────────────────────────────────────────────────────────────
// Telegram modal
// ───────────────────────────────────────────────────────────────────
const TelegramModal = ({ open, onClose, copy, productTitle }) => {
if (!open) return null;
return (
);
};
// ───────────────────────────────────────────────────────────────────
// Marquee
// ───────────────────────────────────────────────────────────────────
const Marquee = ({ lang }) => {
const items = lang === 'ru' ?
['Сделано руками', 'Один экземпляр', 'Москва', 'С 2019 года', 'Тёплая керамика', 'Лён, орех, бумага', 'Без серий, без склада'] :
['Made by hand', 'One of one', 'Moscow', 'Since 2019', 'Warm ceramics', 'Linen, walnut, paper', 'No series, no warehouse'];
const sequence = [...items, ...items, ...items, ...items];
return (
{sequence.map((s, i) =>
{s} ✿
)}
);
};
// ───────────────────────────────────────────────────────────────────
// Product card
// ───────────────────────────────────────────────────────────────────
const ProductCard = ({ p, lang, onOpen, copy, layout = 'grid' }) => {
const t = COPY[lang].product;
const aspect = layout === 'masonry' ?
p.id.charCodeAt(2) % 3 === 0 ? '3/4' : p.id.charCodeAt(2) % 3 === 1 ? '4/5' : '1/1' :
'4/5';
const coverImg = p.imgs && p.imgs[0];
return (
{coverImg
?
:
}
Смотреть →
{p.title[lang]}
{p.subtitle[lang]}
{p.price ? p.price.toLocaleString('ru-RU') + ' ₽' : '—'}
);
};
Object.assign(window, {
PatternBackdrop, Wordmark, Nav, Footer, TelegramModal, Marquee, ProductCard
});