// 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) => ; const mobileLink = (key, label) => ; return ( <>
{/* mobile: hamburger + telegram */}
{/* mobile menu panel */}
{/* 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 (

Напишите нам

{productTitle ? <>Ответим в течение дня. Расскажите, что вас зацепило: «{productTitle}» — и любые вопросы. : <>Ответим в течение дня. Расскажите, что ищете — подскажем и пришлём фото.}

Написать в Telegram Написать на почту Написать во ВКонтакте
); }; // ─────────────────────────────────────────────────────────────────── // 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.title[lang]}
{p.subtitle[lang]}
{p.price ? p.price.toLocaleString('ru-RU') + ' ₽' : '—'}
); }; Object.assign(window, { PatternBackdrop, Wordmark, Nav, Footer, TelegramModal, Marquee, ProductCard });