diff --git a/README.md b/README.md
index 0520413..949189f 100644
--- a/README.md
+++ b/README.md
@@ -114,7 +114,7 @@ O notebook 3D permanece em celulares, tablets e desktop: tampa, tela, abas e cur
As quatro capturas desktop reais ficam em `public/assets/notebook/`, em WebP de 1600 × 1000. A troca aguarda a decodificação da próxima imagem, mantendo a anterior visível durante o carregamento ou uma falha.
-Uma entrada normal na homepage começa no topo. A recarga também volta ao início, removendo o fragmento deixado por uma navegação anterior. Links diretos como `/#about` continuam chegando à seção após a estabilização do notebook; rolagem deliberada durante a abertura cancela esse ajuste. A política inicial de scroll é aplicada antes da hidratação, sem disputar com a rolagem suave dos links posteriores.
+Uma entrada normal na homepage começa no topo. A recarga também volta ao início, removendo o fragmento deixado por uma navegação anterior. Links diretos como `/#about` continuam chegando à seção após a estabilização do notebook; rolagem deliberada durante a abertura cancela esse ajuste. A política inicial de scroll é aplicada antes da hidratação, sem disputar com a rolagem suave dos links posteriores. Assim que a chegada se resolve, a restauração volta a pertencer ao navegador: Voltar e Avançar restauram a posição do histórico em vez de forçar o topo.
### Identidade visual
diff --git a/app/components/cinematic/CinematicHome.test.tsx b/app/components/cinematic/CinematicHome.test.tsx
index 46bbb7e..64a5b4e 100644
--- a/app/components/cinematic/CinematicHome.test.tsx
+++ b/app/components/cinematic/CinematicHome.test.tsx
@@ -1,5 +1,8 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
-import { useHomeFragmentNavigation } from "./useHomeFragmentNavigation";
+import {
+ resetHomeArrivalState,
+ useHomeFragmentNavigation,
+} from "./useHomeFragmentNavigation";
function HomeNavigation() {
const onLayoutReady = useHomeFragmentNavigation();
@@ -23,6 +26,7 @@ function HomeNavigation() {
const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
beforeEach(() => {
+ resetHomeArrivalState();
window.history.replaceState(null, "", "/");
HTMLElement.prototype.scrollIntoView = jest.fn();
});
@@ -53,6 +57,26 @@ describe("home fragment navigation", () => {
expect(restoredPosition).toBe(0);
});
+ it("leaves a same-document return to the position the browser restores", () => {
+ const scrollTo = jest
+ .spyOn(window, "scrollTo")
+ .mockImplementation(() => {});
+ const firstVisit = render();
+ fireEvent.click(
+ screen.getByRole("button", { name: "Establish notebook layout" }),
+ );
+ firstVisit.unmount();
+ expect(window.history.scrollRestoration).not.toBe("manual");
+
+ scrollTo.mockClear();
+ render();
+ fireEvent.click(
+ screen.getByRole("button", { name: "Establish notebook layout" }),
+ );
+ expect(scrollTo).not.toHaveBeenCalled();
+ expect(window.history.scrollRestoration).not.toBe("manual");
+ });
+
it("does not reset the visitor's position after intentional scrolling during startup", () => {
let position = 0;
jest
diff --git a/app/components/cinematic/useHomeFragmentNavigation.ts b/app/components/cinematic/useHomeFragmentNavigation.ts
index 97a7496..816f21a 100644
--- a/app/components/cinematic/useHomeFragmentNavigation.ts
+++ b/app/components/cinematic/useHomeFragmentNavigation.ts
@@ -16,6 +16,17 @@ function focusFragment(target: HTMLElement) {
target.focus({ preventScroll: true });
}
+/**
+ * A document owns one outstanding homepage arrival. Later mounts are same-document
+ * history, where the browser restores its own position.
+ */
+let arrivalSettled = false;
+
+/** Tests replay several documents inside one jsdom document. */
+export function resetHomeArrivalState() {
+ arrivalSettled = false;
+}
+
/** Correct only an outstanding arrival; ordinary links and history keep native scrolling. */
export function useHomeFragmentNavigation() {
const ready = useRef(false);
@@ -26,12 +37,11 @@ export function useHomeFragmentNavigation() {
| PerformanceNavigationTiming
| undefined;
// A history restoration owns its saved position, not the URL's old fragment.
- let pending =
- navigation?.type === "back_forward" ? "" : window.location.hash;
- let pendingTop =
- navigation?.type !== "back_forward" && !window.location.hash;
- if (navigation?.type !== "back_forward")
- window.history.scrollRestoration = "manual";
+ const restoration = navigation?.type === "back_forward";
+ const arrival = !restoration && !arrivalSettled;
+ let pending = restoration ? "" : window.location.hash;
+ let pendingTop = arrival && !window.location.hash;
+ if (arrival) window.history.scrollRestoration = "manual";
const resolve = () => {
if (!ready.current) return;
@@ -69,6 +79,7 @@ export function useHomeFragmentNavigation() {
const cancelArrival = () => {
pending = "";
pendingTop = false;
+ window.history.scrollRestoration = "auto";
};
const onKeyDown = (event: KeyboardEvent) => {
if (
@@ -151,5 +162,7 @@ export function useHomeFragmentNavigation() {
return useCallback(() => {
ready.current = true;
resolveArrival.current?.();
+ arrivalSettled = true;
+ window.history.scrollRestoration = "auto";
}, []);
}