refactor(home): remove a secao animada do notebook

O mergulho do hero termina direto em Projetos. As capturas de tela seguem nos
cards (ProjectCover). Remove componentes, testes e 670 linhas de CSS do notebook.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Evandro Fonseca JuniorandClaude Opus 5.5 committed 2026-10-05 01:12:08 -03:00
1 parent 92c602c0ad
commit 44efcfcb20
18 files changed
+30 -3059

No files matched your search

+12 -12
View File
@@ -9,7 +9,7 @@ function HomeNavigation() {
return (
<>
<button type="button" onClick={onLayoutReady}>
Establish notebook layout
Establish layout
</button>
<a href="/#about">Sobre</a>
<a href="/#contact">Contato</a>
@@ -42,7 +42,7 @@ afterEach(() => {
});
describe("home fragment navigation", () => {
it("starts an unfragmented visit at the top after the notebook establishes layout", () => {
it("starts an unfragmented visit at the top after the home establishes layout", () => {
let restoredPosition = 3200;
jest
.spyOn(window, "scrollTo")
@@ -52,7 +52,7 @@ describe("home fragment navigation", () => {
});
render(<HomeNavigation />);
fireEvent.click(
screen.getByRole("button", { name: "Establish notebook layout" }),
screen.getByRole("button", { name: "Establish layout" }),
);
expect(restoredPosition).toBe(0);
});
@@ -63,7 +63,7 @@ describe("home fragment navigation", () => {
.mockImplementation(() => {});
const firstVisit = render(<HomeNavigation />);
fireEvent.click(
screen.getByRole("button", { name: "Establish notebook layout" }),
screen.getByRole("button", { name: "Establish layout" }),
);
firstVisit.unmount();
expect(window.history.scrollRestoration).not.toBe("manual");
@@ -71,7 +71,7 @@ describe("home fragment navigation", () => {
scrollTo.mockClear();
render(<HomeNavigation />);
fireEvent.click(
screen.getByRole("button", { name: "Establish notebook layout" }),
screen.getByRole("button", { name: "Establish layout" }),
);
expect(scrollTo).not.toHaveBeenCalled();
expect(window.history.scrollRestoration).not.toBe("manual");
@@ -88,24 +88,24 @@ describe("home fragment navigation", () => {
position = 600;
fireEvent.wheel(window, { deltaY: 600 });
fireEvent.click(
screen.getByRole("button", { name: "Establish notebook layout" }),
screen.getByRole("button", { name: "Establish layout" }),
);
expect(position).toBe(600);
});
it("waits for the notebook layout on fragment arrival, then never steals focus on refresh", () => {
it("waits for the home layout on fragment arrival, then never steals focus on refresh", () => {
window.history.replaceState(null, "", "/#contact");
render(<HomeNavigation />);
const contact = screen.getByRole("region", { name: "Contato" });
expect(contact).not.toHaveFocus();
fireEvent.click(
screen.getByRole("button", { name: "Establish notebook layout" }),
screen.getByRole("button", { name: "Establish layout" }),
);
expect(contact).toHaveFocus();
screen.getByRole("button", { name: "Read biography" }).focus();
fireEvent.click(
screen.getByRole("button", { name: "Establish notebook layout" }),
screen.getByRole("button", { name: "Establish layout" }),
);
expect(
screen.getByRole("button", { name: "Read biography" }),
@@ -119,7 +119,7 @@ describe("home fragment navigation", () => {
message.focus();
fireEvent.wheel(window, { deltaY: 200 });
fireEvent.click(
screen.getByRole("button", { name: "Establish notebook layout" }),
screen.getByRole("button", { name: "Establish layout" }),
);
expect(message).toHaveFocus();
});
@@ -128,7 +128,7 @@ describe("home fragment navigation", () => {
window.history.replaceState(null, "", "/#about");
render(<HomeNavigation />);
fireEvent.click(
screen.getByRole("button", { name: "Establish notebook layout" }),
screen.getByRole("button", { name: "Establish layout" }),
);
const link = screen.getByRole("link", { name: "Sobre" });
link.focus();
@@ -152,7 +152,7 @@ describe("home fragment navigation", () => {
window.dispatchEvent(new HashChangeEvent("hashchange"));
});
fireEvent.click(
screen.getByRole("button", { name: "Establish notebook layout" }),
screen.getByRole("button", { name: "Establish layout" }),
);
expect(message).toHaveFocus();
});
+3 -4
View File
@@ -14,9 +14,8 @@ import Hero, { type HeroStats } from "../Hero";
import LighthouseMetrics from "../LighthouseMetrics";
import Projects from "../Projects";
import Skills from "../Skills";
import NotebookStory from "../story/NotebookStory";
import { WorkModalities } from "../WorkModalities";
import { useRef } from "react";
import { useEffect, useRef } from "react";
import ExperienceDisclosure from "./ExperienceDisclosure";
import { useCinematicScroll } from "./useCinematicScroll";
import { useHomeFragmentNavigation } from "./useHomeFragmentNavigation";
@@ -35,6 +34,8 @@ export default function CinematicHome({
const onLayoutReady = useHomeFragmentNavigation();
const mainRef = useRef<HTMLElement>(null);
useCinematicScroll(mainRef);
// o layout da home é estável na montagem: a chegada por fragmento (#contato etc.) já pode ser resolvida
useEffect(() => onLayoutReady(), [onLayoutReady]);
return (
<div className="flex min-h-screen flex-col overflow-x-clip">
@@ -42,8 +43,6 @@ export default function CinematicHome({
<main ref={mainRef} className="ds-main relative z-0">
<Hero stats={heroStats} />
<NotebookStory projects={projects} onLayoutReady={onLayoutReady} />
<Projects projects={projects} />
<WorkModalities />
+1 -1
View File
@@ -38,7 +38,7 @@ export default function HeroField() {
const [visible, setVisible] = useState(false);
const [ready, setReady] = useState(false);
const [failed, setFailed] = useState(false);
// fim do mergulho: a cena já está no escuro e o hero se dissolve; parar o desenho poupa a GPU para o notebook
// fim do mergulho: a cena já está no escuro e o hero se dissolve; parar o desenho poupa a GPU
const [done, setDone] = useState(false);
useEffect(() => {
+7 -22
View File
@@ -36,35 +36,20 @@ const holdPrev = (tl: gsap.core.Timeline, prev: HTMLElement) =>
// a seção que entra também fica parada no topo: a transição acontece "no lugar"
const holdNext = (tl: gsap.core.Timeline, next: HTMLElement) =>
tl.fromTo(next, { y: () => -vh() }, { y: 0 }, 0);
const stageOf = (section: HTMLElement) =>
section.querySelector<HTMLElement>(".notebook-story-sticky") ?? section;
// uma linguagem diferente por passagem, na ordem das seções da home
const TRANSITIONS: Transition[] = [
// hero → notebook: o mergulho termina no escuro, o hero some e o notebook abre como tela de cinema
// hero → projetos: o mergulho termina no escuro, o hero some e os projetos abrem como tela de cinema
(tl, prev, next) => {
holdPrev(tl, prev);
holdNext(tl, next);
const band = (open: number) => () =>
`inset(${(vh() * (1 - open)) / 2}px 0px ${next.offsetHeight - vh() + (vh() * (1 - open)) / 2}px 0px)`;
tl.fromTo(prev, { autoAlpha: 1 }, { autoAlpha: 0, duration: 0.4 }, 0).fromTo(
stageOf(next),
{ opacity: 0, clipPath: "inset(32% 0% 32% 0%)" },
{ opacity: 1, clipPath: "inset(0% 0% 0% 0%)", duration: 0.65 },
0.35,
);
},
// notebook → projetos: a seção sobe como um cartão e o notebook recolhe ao fundo
(tl, prev, next) => {
holdPrev(tl, prev);
tl.fromTo(
stageOf(prev),
{ opacity: 1, clipPath: "inset(0% 0% 0% 0% round 0px)" },
{ opacity: 0.15, clipPath: "inset(8% 6% 8% 6% round 32px)" },
0,
).fromTo(
next,
{ clipPath: "inset(0% 5% 0% 5% round 40px 40px 0px 0px)" },
{ clipPath: "inset(0% 0% 0% 0% round 0px 0px 0px 0px)" },
0,
{ opacity: 0, clipPath: band(0.36) },
{ opacity: 1, clipPath: band(1), duration: 0.65 },
0.35,
);
},
// projetos → modalidades: uma íris abre do centro enquanto a anterior avança contra a câmera
@@ -197,7 +182,7 @@ export function useCinematicScroll(mainRef: RefObject<HTMLElement | null>) {
const scenes = Array.from(main.children) as HTMLElement[];
const context = gsap.context(() => {
scenes.forEach((scene, index) => {
// quem entra fica por cima; o hero, que sai por dissolução, fica por cima do notebook
// quem entra fica por cima; o hero, que sai por dissolução, fica por cima da cena seguinte
gsap.set(scene, { position: "relative", zIndex: index === 0 ? 30 : index + 1 });
});
TRANSITIONS.forEach((transition, index) => {