From 2b5b4a22675a9cbb23694856bddffa3495418a54 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sun, 4 Oct 2026 23:42:27 -0300 Subject: [PATCH] feat(experience): skills e experiencia visiveis (aberta em telas largas) e link Experiencia no menu - ExperienceDisclosure abre a gaveta em >=1024px e quando o link aponta para #experience/#skills; no celular segue recolhida - novo item Experiencia em NAVIGATION_ITEMS (desktop e menu mobile); testes do Header atualizados Co-Authored-By: Claude Sonnet 5.5 --- app/components/Header.test.tsx | 17 ++++- app/components/cinematic/CinematicHome.tsx | 8 +-- .../cinematic/ExperienceDisclosure.tsx | 63 +++++++++++++++++++ lib/site-navigation.ts | 1 + 4 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 app/components/cinematic/ExperienceDisclosure.tsx diff --git a/app/components/Header.test.tsx b/app/components/Header.test.tsx index 5bf9426..57fbd9b 100644 --- a/app/components/Header.test.tsx +++ b/app/components/Header.test.tsx @@ -32,7 +32,13 @@ describe("Header", () => { within(navigation) .getAllByRole("link") .map((link) => link.getAttribute("href")), - ).toEqual(["/#modalities", "/cases", "/#process", "/#about"]); + ).toEqual([ + "/#modalities", + "/cases", + "/#process", + "/#about", + "/#experience", + ]); const contactLink = screen .getAllByRole("link") .find((link) => link.getAttribute("href") === "/#contact"); @@ -68,7 +74,14 @@ describe("Header", () => { within(mobileNavigation) .getAllByRole("link") .map((link) => link.getAttribute("href")), - ).toEqual(["/#modalities", "/cases", "/#process", "/#about", "/#contact"]); + ).toEqual([ + "/#modalities", + "/cases", + "/#process", + "/#about", + "/#experience", + "/#contact", + ]); expect( within(mobileNavigation).queryByRole("link", { name: /currículo|download cv/i, diff --git a/app/components/cinematic/CinematicHome.tsx b/app/components/cinematic/CinematicHome.tsx index 0976ce7..1af3419 100644 --- a/app/components/cinematic/CinematicHome.tsx +++ b/app/components/cinematic/CinematicHome.tsx @@ -16,6 +16,7 @@ import Projects from "../Projects"; import Skills from "../Skills"; import NotebookStory from "../story/NotebookStory"; import { WorkModalities } from "../WorkModalities"; +import ExperienceDisclosure from "./ExperienceDisclosure"; import ScrollVelocityText from "./ScrollVelocityText"; import SectionDivider from "./SectionDivider"; import { useHomeFragmentNavigation } from "./useHomeFragmentNavigation"; @@ -56,13 +57,10 @@ export default function CinematicHome({ -
- - Experiência e tecnologias por trás das entregas - + -
+
diff --git a/app/components/cinematic/ExperienceDisclosure.tsx b/app/components/cinematic/ExperienceDisclosure.tsx new file mode 100644 index 0000000..84e9b77 --- /dev/null +++ b/app/components/cinematic/ExperienceDisclosure.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { type ReactNode, useEffect, useState } from "react"; + +const TARGETS = new Set(["#experience", "#skills"]); + +/** + * Skills e Experiência ficavam numa gaveta sempre fechada e sem link no menu. + * Agora abre sozinha em telas largas (onde há espaço) e quando um link aponta para #experience ou #skills; + * no celular continua recolhida para a página não crescer. + */ +export default function ExperienceDisclosure({ + children, +}: { + children: ReactNode; +}) { + const [open, setOpen] = useState(false); + + useEffect(() => { + const reveal = (hash: string) => { + if (!TARGETS.has(hash)) return; + setOpen(true); + // espera o React abrir a gaveta para o alvo existir no layout + requestAnimationFrame(() => + document + .getElementById(hash.slice(1)) + ?.scrollIntoView({ behavior: "instant", block: "start" }), + ); + }; + if (window.matchMedia("(min-width: 1024px)").matches) setOpen(true); + else if (TARGETS.has(window.location.hash)) setOpen(true); + + const onClick = (event: MouseEvent) => { + const link = + event.target instanceof Element + ? event.target.closest("a[href*='#']") + : null; + if (!(link instanceof HTMLAnchorElement)) return; + const url = new URL(link.href); + if (url.pathname === window.location.pathname) reveal(url.hash); + }; + const onHashChange = () => reveal(window.location.hash); + document.addEventListener("click", onClick); + window.addEventListener("hashchange", onHashChange); + return () => { + document.removeEventListener("click", onClick); + window.removeEventListener("hashchange", onHashChange); + }; + }, []); + + return ( +
setOpen(event.currentTarget.open)} + className="ds-section !pt-0" + > + + Experiência e tecnologias por trás das entregas + + {children} +
+ ); +} diff --git a/lib/site-navigation.ts b/lib/site-navigation.ts index ccf63d7..4d43e63 100644 --- a/lib/site-navigation.ts +++ b/lib/site-navigation.ts @@ -6,4 +6,5 @@ export const NAVIGATION_ITEMS = [ { href: "/cases", label: "Projetos" }, { href: "/#process", label: "Como trabalho" }, { href: "/#about", label: "Sobre" }, + { href: "/#experience", label: "Experiência" }, ] as const;