/* global React */ const { useState, useEffect, useRef } = React; // ─── Eyebrow ────────────────────────────────────────────────── function Eyebrow({ children, dark, withBar = true, style }) { return ( {withBar && } {children} ); } // ─── Tag / chip ─────────────────────────────────────────────── function Tag({ children, onClick, active, dark }) { const base = active ? { background: 'var(--asphalt)', color: 'var(--limestone)', borderColor: 'var(--asphalt)' } : dark ? { background: 'transparent', color: 'var(--limestone)', borderColor: 'rgba(232,226,212,0.32)' } : { background: 'transparent', color: 'var(--asphalt)', borderColor: 'var(--line-strong)' }; return ( ); } // ─── Button ─────────────────────────────────────────────────── function Button({ children, variant = 'primary', onClick, icon, size = 'md', type = 'button', dark, full, href, disabled = false }) { const sizes = { sm: { padding: '8px 14px', fontSize: 12 }, md: { padding: '13px 20px', fontSize: 14 }, lg: { padding: '16px 26px', fontSize: 15 } }; const variants = { primary: { background: 'var(--asphalt)', color: 'var(--limestone)', border: '1px solid var(--asphalt)' }, accent: { background: 'var(--taxi)', color: 'var(--asphalt)', border: '1px solid var(--taxi)' }, ghost: { background: 'transparent', color: dark ? 'var(--limestone)' : 'var(--asphalt)', border: dark ? '1px solid rgba(232,226,212,0.32)' : '1px solid var(--line-strong)' }, link: { background: 'transparent', color: 'var(--brick)', borderRadius: 0, padding: '4px 0', borderBottom: '1px solid currentColor' }, }; const Tag = href ? 'a' : 'button'; const props = href ? { href } : { type, onClick, disabled }; return ( e.currentTarget.style.transform = 'translateY(1px)'} onMouseUp={e => e.currentTarget.style.transform = 'translateY(0)'} onMouseLeave={e => e.currentTarget.style.transform = 'translateY(0)'} > {children} {icon && {icon}} ); } // ─── Field ──────────────────────────────────────────────────── function Field({ label, hint, value, onChange, type = 'text', placeholder, multiline, rows = 4, name, required = false, maxLength, autoComplete }) { const T = multiline ? 'textarea' : 'input'; const [focus, setFocus] = useState(false); return ( ); } // ─── Lucide icon helper ─────────────────────────────────────── function Icon({ name, size = 18, stroke = 1.5, color = 'currentColor' }) { const ref = useRef(null); useEffect(() => { if (window.lucide && ref.current) { ref.current.innerHTML = ''; const el = document.createElement('i'); el.setAttribute('data-lucide', name); ref.current.appendChild(el); window.lucide.createIcons({ attrs: { 'stroke-width': stroke, width: size, height: size } }); } }, [name, size, stroke]); return ; } // ─── Reveal: reliable one-time staggered entrance ───────────── function Reveal({ children, as: As = 'div', style, delay = 0 }) { const ref = useRef(null); const [visible, setVisible] = useState(() => ( typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches )); useEffect(() => { const node = ref.current; if (!node || visible) return undefined; if (!('IntersectionObserver' in window)) { setVisible(true); return undefined; } const observer = new IntersectionObserver(([entry]) => { if (!entry.isIntersecting) return; setVisible(true); observer.disconnect(); }, { threshold: 0.08, rootMargin: '0px 0px -6% 0px' }); observer.observe(node); return () => observer.disconnect(); }, [visible]); return ( {children} ); } // ─── Grain overlay ──────────────────────────────────────────── // A tiny static CSS pattern is substantially cheaper to composite than the // previous full-screen SVG turbulence filter. const GRAIN_URL = ''; function Grain({ opacity = 0.06, blend = 'overlay' }) { return (
); } Object.assign(window, { Eyebrow, Tag, Button, Field, Icon, Reveal, Grain, GRAIN_URL });