perf(movimento): sem tela vazia, passagens em CSS e vitrine leve

Projetos sobem sobre o palco do hero e o celular tem hero de uma tela sem mergulho; passagens com animation-timeline em CSS, sem GSAP nem motion; troca da vitrine sem texto duplicado; moldura de celular só com pôster e scrub reencodado em 960 px (9,4 para 5,2 MB); 3D monta na primeira interação e para quando parado; transição entre páginas do mesmo segmento; foco em anel duplo; texto de 15 px no celular; código morto removido.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 14:15:50 -03:00
1 parent 10f170abf4
commit 318a764190
77 files changed
+4293 -4951

No files matched your search

+141
View File
@@ -0,0 +1,141 @@
import { act, render } from "@testing-library/react";
import { useRef } from "react";
import { useInView } from "./useInView";
type Observer = {
callback: IntersectionObserverCallback;
options?: IntersectionObserverInit;
observed: Element[];
disconnect: jest.Mock;
};
let observers: Observer[] = [];
const original = global.IntersectionObserver;
beforeEach(() => {
observers = [];
Object.defineProperty(window, "innerHeight", {
configurable: true,
value: 800,
});
global.IntersectionObserver = class {
private readonly record: Observer;
constructor(
callback: IntersectionObserverCallback,
options?: IntersectionObserverInit,
) {
this.record = { callback, options, observed: [], disconnect: jest.fn() };
observers.push(this.record);
}
observe(target: Element) {
this.record.observed.push(target);
}
unobserve() {}
disconnect() {
this.record.disconnect();
}
takeRecords() {
return [];
}
} as unknown as typeof IntersectionObserver;
});
afterEach(() => {
global.IntersectionObserver = original;
});
function report(ratio: number, visibleHeight: number) {
act(() => {
for (const observer of observers)
observer.callback(
[
{
isIntersecting: ratio > 0,
intersectionRatio: ratio,
intersectionRect: { height: visibleHeight },
target: observer.observed[0],
} as unknown as IntersectionObserverEntry,
],
{} as IntersectionObserver,
);
});
}
function Probe({ amount, once }: { amount?: number; once?: boolean }) {
const ref = useRef<HTMLDivElement>(null);
const inView = useInView(ref, { amount, once });
return <div ref={ref} data-in-view={String(inView)} />;
}
const state = (container: HTMLElement) =>
container.firstElementChild?.getAttribute("data-in-view");
describe("useInView", () => {
it("is false until enough of the element is on screen (20 % by default)", () => {
const { container } = render(<Probe />);
expect(state(container)).toBe("false");
expect(observers).toHaveLength(1);
expect(observers[0].observed).toHaveLength(1);
report(0.1, 40); // pouco à vista: ainda não
expect(state(container)).toBe("false");
report(0.25, 100);
expect(state(container)).toBe("true");
});
it("stays true after the element leaves (once), and stops observing", () => {
const { container } = render(<Probe />);
report(1, 300);
expect(state(container)).toBe("true");
expect(observers[0].disconnect).toHaveBeenCalled();
report(0, 0);
expect(state(container)).toBe("true");
});
it("follows the element in and out when once is false", () => {
const { container } = render(<Probe once={false} />);
report(1, 300);
expect(state(container)).toBe("true");
report(0, 0);
expect(state(container)).toBe("false");
report(0.5, 200);
expect(state(container)).toBe("true");
});
it("counts a very tall element once a fifth of the window is showing, even if that is a tiny share of it", () => {
const { container } = render(<Probe />);
report(0.02, 100); // 100 px de um bloco de 5000: menos de 20% da janela (800 px)
expect(state(container)).toBe("false");
report(0.04, 200); // 200 px >= 20% de 800
expect(state(container)).toBe("true");
});
it("asks the observer for the amount it was given", () => {
render(<Probe amount={0.7} />);
expect(observers[0].options?.threshold).toEqual([0, 0.7]);
});
it("is true at once when the browser has no IntersectionObserver (content is never left hidden)", () => {
// biome-ignore lint/suspicious/noExplicitAny: simula um navegador sem a API
(global as any).IntersectionObserver = undefined;
const { container } = render(<Probe />);
expect(state(container)).toBe("true");
});
it("disconnects the observer when the component unmounts", () => {
const { unmount } = render(<Probe />);
unmount();
expect(observers[0].disconnect).toHaveBeenCalled();
});
});
+54
View File
@@ -0,0 +1,54 @@
"use client";
import { type RefObject, useEffect, useState } from "react";
interface InViewOptions {
/** Fração do elemento (0 a 1) que precisa estar à vista; em elementos altos, vale também essa fração da altura da janela. */
amount?: number;
/** Verdadeiro (padrão): depois de aparecer uma vez, continua verdadeiro. Falso: acompanha a entrada e a saída. */
once?: boolean;
}
function isShown(entry: IntersectionObserverEntry, amount: number) {
return (
entry.isIntersecting &&
(entry.intersectionRatio >= amount ||
entry.intersectionRect.height >= window.innerHeight * amount)
);
}
/**
* O elemento está à vista? Usa IntersectionObserver (sem ouvir a rolagem). Sem IntersectionObserver devolve
* verdadeiro logo na montagem: o conteúdo nunca fica escondido por falta do recurso.
*/
export function useInView(
ref: RefObject<Element | null>,
{ amount = 0.2, once = true }: InViewOptions = {},
): boolean {
const [inView, setInView] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
if (typeof IntersectionObserver === "undefined") {
setInView(true);
return;
}
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
const shown = isShown(entry, amount);
if (shown) {
setInView(true);
if (once) observer.disconnect();
} else if (!once) setInView(false);
}
},
{ threshold: [0, amount] },
);
observer.observe(element);
return () => observer.disconnect();
}, [ref, amount, once]);
return inView;
}
+66 -23
View File
@@ -1,49 +1,92 @@
import { renderHook } from "@testing-library/react";
import { act, renderHook } from "@testing-library/react";
import { createElement } from "react";
import { renderToString } from "react-dom/server";
import { useReducedMotion } from "./useReducedMotion";
// Mock do Framer Motion
jest.mock("motion/react", () => ({
useReducedMotion: jest.fn(),
}));
type Listener = () => void;
/** matchMedia que responde só à consulta de movimento reduzido e deixa o teste mudar a preferência depois. */
function mockPreference(initial: boolean) {
const listeners = new Set<Listener>();
const query = {
matches: initial,
media: "(prefers-reduced-motion: reduce)",
addEventListener: jest.fn((_: string, listener: Listener) =>
listeners.add(listener),
),
removeEventListener: jest.fn((_: string, listener: Listener) =>
listeners.delete(listener),
),
};
window.matchMedia = jest.fn().mockReturnValue(query);
return {
query,
change(value: boolean) {
query.matches = value;
for (const listener of listeners) listener();
},
};
}
const original = window.matchMedia;
afterEach(() => {
window.matchMedia = original;
});
describe("useReducedMotion", () => {
it("deve retornar false quando prefers-reduced-motion não está ativo", () => {
const { useReducedMotion: mockUseReducedMotion } =
jest.requireMock("motion/react");
mockUseReducedMotion.mockReturnValue(false);
it("is false when the system does not ask for reduced motion", () => {
mockPreference(false);
const { result } = renderHook(() => useReducedMotion());
expect(result.current).toBe(false);
});
it("deve retornar true quando prefers-reduced-motion está ativo", () => {
const { useReducedMotion: mockUseReducedMotion } =
jest.requireMock("motion/react");
mockUseReducedMotion.mockReturnValue(true);
it("is true when the system asks for reduced motion", () => {
mockPreference(true);
const { result } = renderHook(() => useReducedMotion());
expect(result.current).toBe(true);
expect(window.matchMedia).toHaveBeenCalledWith(
"(prefers-reduced-motion: reduce)",
);
});
it("deve retornar false quando o hook do Framer Motion retorna null", () => {
const { useReducedMotion: mockUseReducedMotion } =
jest.requireMock("motion/react");
mockUseReducedMotion.mockReturnValue(null);
it("follows the preference when it changes while the page is open", () => {
const preference = mockPreference(false);
const { result } = renderHook(() => useReducedMotion());
expect(result.current).toBe(false);
act(() => preference.change(true));
expect(result.current).toBe(true);
act(() => preference.change(false));
expect(result.current).toBe(false);
});
it("deve retornar false quando o hook do Framer Motion retorna undefined", () => {
const { useReducedMotion: mockUseReducedMotion } =
jest.requireMock("motion/react");
mockUseReducedMotion.mockReturnValue(undefined);
it("stops listening when the component unmounts", () => {
const preference = mockPreference(false);
const { unmount } = renderHook(() => useReducedMotion());
unmount();
expect(preference.query.removeEventListener).toHaveBeenCalledTimes(1);
});
it("is false on the server and in browsers without matchMedia, so the HTML always hydrates the same", () => {
mockPreference(true);
function Probe() {
return createElement("i", { "data-reduced": String(useReducedMotion()) });
}
expect(renderToString(createElement(Probe))).toContain(
'data-reduced="false"',
);
// biome-ignore lint/suspicious/noExplicitAny: simula um navegador sem a API
(window as any).matchMedia = undefined;
const { result } = renderHook(() => useReducedMotion());
expect(result.current).toBe(false);
});
});
+14 -17
View File
@@ -1,31 +1,28 @@
"use client";
import { useReducedMotion as useFramerReducedMotion } from "motion/react";
import { useEffect, useState } from "react";
const QUERY = "(prefers-reduced-motion: reduce)";
/**
* Hook que detecta a preferência prefers-reduced-motion do sistema operacional.
* Wrapper sobre o hook useReducedMotion do Framer Motion.
*
* @returns {boolean} true se prefers-reduced-motion está ativo, false caso contrário
* Preferência `prefers-reduced-motion` do sistema operacional, lida com matchMedia (sem biblioteca de animação).
* Falso no servidor e na primeira renderização no navegador, para o HTML hidratar igual ao do servidor.
*
* @example
* ```tsx
* const reducedMotion = useReducedMotion();
*
* return (
* <motion.div
* animate={{ opacity: 1, y: reducedMotion ? 0 : 20 }}
* />
* );
* ```
* return <div data-calm={reducedMotion || undefined} />;
*/
export function useReducedMotion(): boolean {
const systemPreference = useFramerReducedMotion();
const [isMounted, setIsMounted] = useState(false);
const [reduced, setReduced] = useState(false);
useEffect(() => {
setIsMounted(true);
if (typeof window.matchMedia !== "function") return;
const query = window.matchMedia(QUERY);
const update = () => setReduced(query.matches);
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
return isMounted && systemPreference === true;
return reduced;
}