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