/* 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 });