/* global React */ // Shared primitives + working navigation + motion helpers const { useState, useEffect, useRef } = React; // --- Lucide icon wrapper ------------------------------------------------- function Icon({ name, size = 20, stroke = 1.6, className = "", style = {} }) { const ref = useRef(null); useEffect(() => { if (ref.current && window.lucide) { ref.current.innerHTML = ""; const el = document.createElement("i"); el.setAttribute("data-lucide", name); ref.current.appendChild(el); window.lucide.createIcons({ attrs: { width: size, height: size, "stroke-width": stroke }, nameAttr: "data-lucide", }); } }, [name, size, stroke]); return ; } function Eyebrow({ children, light = false, center = false }) { return (
{children}
); } // Smooth-scroll to an on-page anchor; otherwise let the browser navigate function navTo(e, href) { if (href && href.charAt(0) === "#") { const el = document.querySelector(href); if (el) { e.preventDefault(); el.scrollIntoView({ behavior: "smooth", block: "start" }); } } } function Button({ children, variant = "primary", size = "md", icon, iconRight, onClick, href }) { const cls = `tf-btn tf-btn-${variant} tf-btn-${size}`; const inner = ( <> {icon && } {children} {iconRight && } ); if (href) return { navTo(e, href); onClick && onClick(e); }}>{inner}; return ; } function Logo({ onDark = false, size = 46, showText = true, href = "index.html" }) { const src = onDark ? "assets/logo-tf-ondark.png" : "assets/logo-tf.png"; return ( navTo(e, href)}> Thays Fernandes {showText && (
Thays Fernandes Tricologista
)}
); } // --- Navbar -------------------------------------------------------------- // base = "" on the home page (anchors are #id); "index.html" on sub-pages. function Navbar({ onBook, base = "" }) { const [scrolled, setScrolled] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 24); window.addEventListener("scroll", onScroll); onScroll(); return () => window.removeEventListener("scroll", onScroll); }, []); const links = [ { l: "Início", h: base + "#topo" }, { l: "Tratamentos", h: base + "#tratamentos" }, { l: "Tecnologia", h: base + "#tecnologia" }, { l: "Método", h: base + "#metodo" }, { l: "Sobre", h: base + "#sobre" }, { l: "Contato", h: base + "#contato" }, ]; return (
); } // --- Footer -------------------------------------------------------------- function Footer({ onBook, base = "" }) { return ( ); } // --- Motion: scroll-reveal + smooth cursor spotlight --------------------- function initMotion() { // reveal on scroll const io = new IntersectionObserver((entries) => { entries.forEach((en) => { if (en.isIntersecting) { en.target.classList.add("in"); io.unobserve(en.target); } }); }, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" }); document.querySelectorAll("[data-reveal]").forEach((el) => io.observe(el)); initSpotlight(); } /* Global Cursor Spotlight — one continuous layer that follows the cursor across the whole site (header, sides, all sections) with no per-section clipping. - #tf-spotlight: a single fixed, viewport-sized green radial-gradient, shown through the transparent light-section backgrounds (z-index 1, behind the z-index-2 sections) so it only ever appears in the background. - dark sections (method/footer) are opaque, so each gets a .tf-darkglow gold layer behind its content. - smooth rAF position lerp; green fades out / gold fades in when the cursor crosses into a dark section (and vice-versa) for a soft colour transition. - disabled on touch / very small screens for performance. */ function initSpotlight() { const coarse = window.matchMedia("(hover: none), (pointer: coarse)").matches; if (coarse || window.innerWidth < 768) return; const page = document.querySelector(".tf-page"); if (!page) return; let spot = page.querySelector(":scope > #tf-spotlight"); if (!spot) { spot = document.createElement("div"); spot.id = "tf-spotlight"; page.insertBefore(spot, page.firstChild); } const darks = Array.from(document.querySelectorAll(".tf-method, .tf-footer")).map((el) => { let glow = el.querySelector(":scope > .tf-darkglow"); if (!glow) { glow = document.createElement("div"); glow.className = "tf-darkglow"; el.insertBefore(glow, el.firstChild); } return { el, glow }; }); const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const ease = reduce ? 1 : 0.16; let tx = 0, ty = 0, cx = 0, cy = 0, gOp = 0; const dOp = darks.map(() => 0); let moved = false, raf = 0; function inRect(r, x, y) { return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom; } function frame() { cx += (tx - cx) * ease; cy += (ty - cy) * ease; // which dark section is the cursor over? (viewport coords; spotlight is fixed) let active = -1; for (let i = 0; i < darks.length; i++) { if (inRect(darks[i].el.getBoundingClientRect(), tx, ty)) { active = i; break; } } // global green: visible everywhere EXCEPT over a dark section const gTarget = (moved && active === -1) ? 1 : 0; gOp += (gTarget - gOp) * ease; spot.style.setProperty("--sx", cx + "px"); spot.style.setProperty("--sy", cy + "px"); spot.style.opacity = gOp.toFixed(3); // per dark section gold glow darks.forEach((d, i) => { const t = (moved && active === i) ? 1 : 0; dOp[i] += (t - dOp[i]) * ease; d.glow.style.opacity = dOp[i].toFixed(3); if (active === i) { const r = d.el.getBoundingClientRect(); d.glow.style.setProperty("--dx", (cx - r.left) + "px"); d.glow.style.setProperty("--dy", (cy - r.top) + "px"); } }); raf = requestAnimationFrame(frame); } window.addEventListener("mousemove", (e) => { tx = e.clientX; ty = e.clientY; moved = true; }, { passive: true }); window.addEventListener("mouseleave", () => { moved = false; }); raf = requestAnimationFrame(frame); } Object.assign(window, { Icon, Eyebrow, Button, Logo, Navbar, Footer, navTo, initMotion });