/* ========================================================= Innovaria — primitives + Nav + Footer ========================================================= */ const { useState, useEffect, useRef } = React; /* ---------- Icon helper (Lucide) ---------- Each Icon renders an inside a React-managed , then we call lucide.createIcons() to replace the with an . Wrapping in a span means React only reconciles the span (stable), so the svg inside survives re-renders cleanly. */ function Icon({ name, size = 16, strokeWidth = 1.5, className }) { const ref = useRef(null); useEffect(() => { if (!ref.current || !window.lucide) return; ref.current.innerHTML = ``; window.lucide.createIcons(); }, [name, size, strokeWidth]); return ( ); } /* ---------- Inline brand SVGs (Lucide removed brand icons) ---------- */ function SocialIcon({ kind, size = 14 }) { const common = { width: size, height: size, viewBox: "0 0 24 24", fill: "currentColor" }; if (kind === "instagram") return ( ); if (kind === "facebook") return ( ); if (kind === "youtube") return ( ); if (kind === "linkedin") return ( ); return null; } /* ---------- Button ---------- */ function Button({ variant = "primary", size = "md", icon, iconRight, children, as = "button", ...rest }) { const cls = ["btn", `btn--${variant}`, size === "sm" && "btn--sm"].filter(Boolean).join(" "); const Comp = as; return ( {icon && } {children} {iconRight && } ); } /* ---------- Utility bar ---------- */ function UtilityBar() { return (
  • +34 604 49 40 89
  • hola@innovaria.es
); } /* ---------- Nav ---------- */ function Nav() { const [menuOpen, setMenuOpen] = useState(false); const [langOpen, setLangOpen] = useState(false); const [lang, setLang] = useState("ES"); useEffect(() => { if (menuOpen) { document.body.style.overflow = "hidden"; } else { document.body.style.overflow = ""; } const onKey = (e) => { if (e.key === "Escape") { setMenuOpen(false); setLangOpen(false); } }; const onClickOutside = (e) => { if (!e.target.closest || !e.target.closest(".nav__lang")) { setLangOpen(false); } }; window.addEventListener("keydown", onKey); window.addEventListener("click", onClickOutside); return () => { window.removeEventListener("keydown", onKey); window.removeEventListener("click", onClickOutside); document.body.style.overflow = ""; }; }, [menuOpen]); return (
{/* Mobile drawer */}
); } /* ---------- Site header (wraps utility + nav, toggles scrolled state) ---------- */ function SiteHeader() { const [scrolled, setScrolled] = useState(false); useEffect(() => { // Hysteresis: activate at 80, deactivate at 30 → no flicker around the threshold const onScroll = () => { const y = window.scrollY; setScrolled((prev) => { if (!prev && y > 80) return true; if (prev && y < 30) return false; return prev; }); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); return (
); } /* ---------- Site header v2 — always white, no top utility bar ---------- Variant used for pages where we want a solid, simple header from the start (e.g. property detail page). */ function SiteHeader2() { return (
); } /* ---------- Site header v3 — like v2 (always white) but WITH top utility bar. Usado en /panel (dashboard) y otras páginas autenticadas donde queremos mantener el topbar de contacto siempre visible. La CSS que pliega el utility-bar cuando .is-scrolled vive en styles.css; en auth.css forzamos que se mantenga visible para .site-header--v3. */ function SiteHeader3() { return (
); } /* ---------- Footer ---------- */ function Footer() { // Reveal-on-scroll effect with subtle parallax: // - Measure footer height and expose as CSS var so body can reserve // scrollable space below main. // - Translate the footer up as the user approaches the bottom of the // document, so it rises gradually (parallax — moves slower than main). const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const applyHeight = () => { document.documentElement.style.setProperty( "--footer-real-height", el.offsetHeight + "px" ); }; applyHeight(); const ro = new ResizeObserver(applyHeight); ro.observe(el); let raf = 0; const onScroll = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = 0; const fh = el.offsetHeight; const docH = document.documentElement.scrollHeight; const vh = window.innerHeight; const maxScroll = docH - vh; const clamped = Math.max(0, Math.min(maxScroll, window.scrollY)); const remaining = Math.max(0, maxScroll - clamped); const zone = fh * 1.2; const progress = remaining >= zone ? 0 : 1 - remaining / zone; const translateY = (1 - progress) * fh * 0.7; el.style.transform = `translate3d(0, ${translateY.toFixed(1)}px, 0)`; }); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); return () => { ro.disconnect(); window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); if (raf) cancelAnimationFrame(raf); }; }, []); return ( ); } /* ---------- Float WhatsApp ---------- */ function FloatWA() { return ( ); } /* ---------- FAQ Item ---------- */ function FAQItem({ q, a, open, onClick }) { return (
{a}
); } Object.assign(window, { Icon, SocialIcon, Button, UtilityBar, Nav, SiteHeader, SiteHeader2, SiteHeader3, Footer, FloatWA, FAQItem });