/* * Âncoras da home (links #id para a mesma página), num lugar só. * * - Um único ouvinte de clique no documento (capturando, antes de qualquer menu remover o link ativado). Quem quer * reagir a um clique num link de âncora se inscreve com `onFragmentClick`: a rolagem suave (useCinematicScroll) e a * chegada por fragmento (useHomeFragmentNavigation) recebem o mesmo clique já interpretado, sem cada uma ler o * evento de novo. * - Um único dono de `history.scrollRestoration`: o script que roda antes da hidratação (`FRAGMENT_NAV_BOOT_SCRIPT`, * que app/layout.tsx põe no ) e `setScrollRestoration`, que os ganchos usam. Nenhum outro código escreve nele. * * Sem "use client": o layout (servidor) importa só a constante do script; o resto roda no navegador. */ /** Clique do botão principal, sem tecla modificadora, que nenhum outro código já tratou. */ export function isPlainPrimaryClick(event: MouseEvent) { return !( event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey ); } /** O link () que o clique atingiu, se abre na mesma aba e não baixa arquivo; `null` para qualquer outro clique. */ function sameTabLink(event: MouseEvent) { const link = event.target instanceof Element ? event.target.closest("a[href]") : null; if (!(link instanceof HTMLAnchorElement) || link.hasAttribute("download")) return null; return !link.target || link.target === "_self" ? link : null; } /** * O `#id` de um link para esta mesma página (mesma origem e mesmo caminho); `null` se leva a outro lugar ou não tem * âncora. A busca (`?tipo=`) não conta: a página é a mesma e o link do cabeçalho (`/#contact`) não a repete. */ function hashOnThisPage(link: HTMLAnchorElement) { const url = new URL(link.href); const same = url.origin === location.origin && url.pathname === location.pathname; return same && url.hash ? url.hash : null; } /** O elemento que um `#id` aponta (aceita percent-encoding); `null` se não existe ou o hash é malformado. */ export function elementForHash(hash: string) { if (!hash) return null; try { return document.getElementById(decodeURIComponent(hash.slice(1))); } catch { return null; } } export type FragmentHit = { target: HTMLElement; hash: string }; /** Link para outro ponto da MESMA página (`#id`): o elemento de destino e o hash; `null` para qualquer outro clique. */ export function samePageHashTarget(event: MouseEvent): FragmentHit | null { const link = sameTabLink(event); const hash = link && hashOnThisPage(link); const target = hash ? elementForHash(hash) : null; return hash && target ? { target, hash } : null; } export type FragmentClickHandler = ( hit: FragmentHit, event: MouseEvent, ) => void; const handlers = new Set(); /** O único ouvinte: um clique simples num link de âncora chega a cada inscrito, na ordem em que se inscreveram. */ function onDocumentClick(event: MouseEvent) { if (!isPlainPrimaryClick(event)) return; const hit = samePageHashTarget(event); if (!hit) return; for (const handler of [...handlers]) handler(hit, event); } /** * Inscreve `handler` nos cliques em links de âncora da mesma página. O ouvinte do documento existe só enquanto houver * inscritos. Devolve a função que cancela a inscrição. */ export function onFragmentClick(handler: FragmentClickHandler) { if (handlers.size === 0) document.addEventListener("click", onDocumentClick, true); handlers.add(handler); return () => { handlers.delete(handler); if (handlers.size === 0) document.removeEventListener("click", onDocumentClick, true); }; } /** Dá o foco ao destino de uma âncora (um alvo sem tabindex ganha -1), sem rolar a página. */ export function focusFragment(target: HTMLElement) { if (!target.hasAttribute("tabindex")) target.tabIndex = -1; target.focus({ preventScroll: true }); } /** Única escrita em `history.scrollRestoration` fora do script de abertura: "manual" na chegada, "auto" depois. */ export function setScrollRestoration(mode: "manual" | "auto") { window.history.scrollRestoration = mode; } /** * Roda no , antes da hidratação e da restauração nativa de rolagem: numa chegada nova à home a rolagem fica * "manual" (a home posiciona a página depois de montar o layout; num recarregamento o fragmento antigo é tirado da * URL), e ao sair da página volta a "auto". Voltar e avançar no histórico seguem com a restauração do navegador. */ export const FRAGMENT_NAV_BOOT_SCRIPT = ` if (location.pathname === "/") { const navigation = performance.getEntriesByType("navigation")[0]; if (navigation?.type !== "back_forward") { history.scrollRestoration = "manual"; if (navigation?.type === "reload" && location.hash) { history.replaceState(history.state, "", location.pathname + location.search); } addEventListener("pagehide", () => { history.scrollRestoration = "auto"; }, { once: true }); } }`;