// Shared UI primitives + nav + footer + reveal-on-scroll hook.
const { useEffect, useRef, useState, useMemo } = React;
// ----- Reveal on scroll -----
function useReveal() {
useEffect(() => {
const els = document.querySelectorAll(
'.reveal:not(.in), .reveal-left:not(.in), .reveal-right:not(.in), .reveal-scale:not(.in)'
);
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add('in');
io.unobserve(e.target);
}
});
}, { threshold: 0.10 });
els.forEach((el) => io.observe(el));
return () => io.disconnect();
});
}
// ----- Buttons -----
const PrimaryButton = ({ children, className = "", icon, ...rest }) => (
);
const GhostButton = ({ children, className = "", icon, ...rest }) => (
);
// ----- Eyebrow label -----
const Eyebrow = ({ children, className = "" }) => (
{children}
);
// ----- Section heading -----
const SectionTitle = ({ kicker, title, sub, align = "left" }) => (
{kicker &&
{kicker}}
{title}
{sub &&
{sub}
}
);
// ----- Marker placeholder (architectural) -----
const ArchPh = ({ variant = 1, label, className = "" }) => (
{/* layered architectural shapes */}
{label && (
{label}
)}
);
// ----- Logo -----
const Logo = ({ className = "", onClick }) => (
);
// ----- Theme toggle -----
const ThemeToggle = ({ colorMode, toggle }) => (
);
// ----- Nav -----
const NAV = [
{ id: 'home', label: 'Home' },
{ id: 'commercial', label: 'Commercial' },
{ id: 'residential', label: 'Residential' },
{ id: 'brands', label: 'Brands' },
{ id: 'leadership', label: 'Leadership' },
{ id: 'contact', label: 'Contact' },
];
const Nav = ({ page, setPage, colorMode, toggleColorMode }) => {
const [scrolled, setScrolled] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
useEffect(() => {
const onS = () => setScrolled(window.scrollY > 16);
onS(); window.addEventListener('scroll', onS, { passive: true });
return () => window.removeEventListener('scroll', onS);
}, []);
const go = (id) => { setPage(id); setMobileOpen(false); window.scrollTo({ top: 0, behavior: 'smooth' }); };
const unscrolledCls = colorMode === 'light'
? 'py-2 bg-white/40 backdrop-blur-md border border-black/10 shadow-[0_4px_24px_rgba(0,0,0,0.10)]'
: 'py-2 bg-black/30 backdrop-blur-md border border-white/10 shadow-[0_4px_32px_rgba(0,0,0,0.45)]';
return (
);
};
// ----- Marquee strip -----
const Marquee = ({ items }) => (
{[...items, ...items, ...items].map((t, i) => (
{t}
))}
);
// ----- Footer -----
const Footer = ({ setPage }) => {
const go = (id) => { setPage(id); window.scrollTo({ top: 0, behavior: 'smooth' }); };
return (
);
};
// ----- Centered modal portal (escapes Framer Motion transform stacking context) -----
const ModalPortal = ({ onClose, children }) => {
React.useEffect(() => {
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = prev; };
}, []);
return ReactDOM.createPortal(
{ if (e.target === e.currentTarget) onClose(); }}
>
{children}
,
document.body
);
};
// Expose
Object.assign(window, {
useReveal, PrimaryButton, GhostButton, Eyebrow, SectionTitle, ArchPh, Logo, Nav, Marquee, Footer, NAV, ThemeToggle, ModalPortal,
});