From d8b59b03f2aff4584aeb3fe2d050f9c56fc9fded Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sun, 4 Oct 2026 23:26:39 -0300 Subject: [PATCH 01/23] feat(design): container e tipografia fluidos, cabecalho de secao em duas colunas e grade de projetos automatica - tokens --shell, --fs-section-title e --fs-lead: iguais ao desenho atual ate ~1440px e crescem em telas grandes - .ds-section-header vira container; acima de 880px o titulo fica a esquerda e a descricao a direita (aninhamento nativo + container query) - .ds-grid-fluid: 2 colunas em notebook e 3 em tela grande, sem breakpoint (usada em Projects) - Hero acompanha a largura util em telas grandes Co-Authored-By: Claude Sonnet 5.5 --- app/components/Hero.tsx | 2 +- app/components/Projects.tsx | 2 +- app/globals.css | 50 +++++++++++++++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index 32cbed0..ebe092a 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -35,7 +35,7 @@ export default function Hero() { duration: shouldReduceMotion ? 0 : 0.45, ease: "easeOut", }} - className="mx-auto grid w-full max-w-[1400px] grid-cols-1 items-center gap-sp-8 px-sp-6 pt-[calc(var(--site-header-height)+32px)] pb-sp-10 lg:grid-cols-[minmax(0,53fr)_minmax(0,47fr)] lg:gap-sp-8 lg:px-sp-12 lg:pb-sp-16" + className="mx-auto grid w-full max-w-[max(1400px,var(--shell))] grid-cols-1 items-center gap-sp-8 px-sp-6 pt-[calc(var(--site-header-height)+32px)] pb-sp-10 lg:grid-cols-[minmax(0,53fr)_minmax(0,47fr)] lg:gap-sp-8 lg:px-sp-12 lg:pb-sp-16" >

diff --git a/app/components/Projects.tsx b/app/components/Projects.tsx index a41d635..81a3781 100644 --- a/app/components/Projects.tsx +++ b/app/components/Projects.tsx @@ -35,7 +35,7 @@ export default function Projects({ dark disableChildrenAnimation > -

+
{projects.map((project) => { const caseStudy = caseStudies.find( (entry) => entry.id === project.id, diff --git a/app/globals.css b/app/globals.css index d8ed4a0..fe70236 100644 --- a/app/globals.css +++ b/app/globals.css @@ -48,6 +48,10 @@ --r-lg: 16px; --r-full: 999px; --site-header-height: 60px; + /* Escala fluida: igual ao desenho original até ~1440px e cresce nas telas grandes. */ + --shell: clamp(1240px, 76vw, 1760px); + --fs-section-title: min(max(clamp(24px, 4vw, 44px), 3.05vw), 68px); + --fs-lead: min(max(15px, 1.05vw), 19px); --font-display: var(--font-space-grotesk), sans-serif; --font-hero: var(--font-monument-extended), sans-serif; --font-mono: var(--font-geist-mono), monospace; @@ -248,7 +252,7 @@ textarea { .ds-section-shell, .ds-loading, .ds-error { - width: min(1240px, 100%); + width: min(var(--shell), 100%); margin: 0 auto; } @@ -258,10 +262,45 @@ textarea { align-items: flex-start; gap: var(--sp-6); margin-bottom: var(--sp-10); + /* o cabeçalho mede a si mesmo: a mesma regra serve para qualquer largura de tela */ + container-type: inline-size; } .ds-section-meta { + flex: 1 1 auto; + min-width: 0; max-width: 760px; + + /* Largo: título à esquerda, descrição à direita, alinhados pela base (some a faixa vazia ao lado do título). */ + @container (min-width: 880px) { + max-width: none; + display: grid; + grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); + grid-template-areas: + "eyebrow eyebrow" + "title lead" + "line line"; + column-gap: var(--sp-10); + align-items: end; + + > .ds-eyebrow { + grid-area: eyebrow; + } + + > .ds-section-title { + grid-area: title; + } + + > .ds-section-description { + grid-area: lead; + max-width: 62ch; + font-size: var(--fs-lead); + } + + > .ds-draw-line { + grid-area: line; + } + } } .ds-eyebrow, @@ -278,7 +317,7 @@ textarea { .ds-section-title { font-family: var(--font-display); - font-size: clamp(24px, 4vw, 44px); + font-size: var(--fs-section-title); line-height: 1.02; font-weight: 800; letter-spacing: -0.035em; @@ -301,6 +340,13 @@ textarea { grid-template-columns: repeat(2, minmax(0, 1fr)); } +/* Colunas automáticas: sem breakpoint. Cabem 2 até ~1360px de largura útil e 3 acima disso. */ +.ds-grid-fluid { + display: grid; + gap: var(--sp-6); + grid-template-columns: repeat(auto-fit, minmax(min(100%, 440px), 1fr)); +} + .ds-grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } From e008a7c2416d11ad8b6f20ae3d74ac7fba5fb518 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sun, 4 Oct 2026 23:28:17 -0300 Subject: [PATCH 02/23] feat(design): botoes .ds-link com 44px de altura minima e cantos retos - .ds-link centraliza o conteudo e garante min-height 44px (alvo de toque), igual em todas as 25 ocorrencias - remove rounded-sm do CTA do Footer e das abas do HeroSystem para seguir o resto do site (cantos retos) Co-Authored-By: Claude Sonnet 5.5 --- app/components/Footer.tsx | 2 +- app/components/cinematic/HeroSystem.tsx | 2 +- app/globals.css | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx index 59b6703..3e6c234 100644 --- a/app/components/Footer.tsx +++ b/app/components/Footer.tsx @@ -63,7 +63,7 @@ export default function Footer() { Conversar sobre meu projeto diff --git a/app/components/cinematic/HeroSystem.tsx b/app/components/cinematic/HeroSystem.tsx index ac8ee0e..ff5b189 100644 --- a/app/components/cinematic/HeroSystem.tsx +++ b/app/components/cinematic/HeroSystem.tsx @@ -294,7 +294,7 @@ export default function HeroSystem() { if (layer.id === "overview" && sceneActive && visible) queuePose(0, 0); }} - className={`min-h-11 rounded-sm px-sp-2 py-sp-2 text-[12px] font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--slash)] motion-reduce:transition-none sm:text-[13px] ${activeLayer === layer.id ? "bg-[var(--slash)] text-[var(--ink)]" : "bg-white/5 text-white/75 hover:bg-white/10 hover:text-white"}`} + className={`min-h-11 px-sp-2 py-sp-2 text-[12px] font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--slash)] motion-reduce:transition-none sm:text-[13px] ${activeLayer === layer.id ? "bg-[var(--slash)] text-[var(--ink)]" : "bg-white/5 text-white/75 hover:bg-white/10 hover:text-white"}`} > {layer.label} diff --git a/app/globals.css b/app/globals.css index fe70236..997e631 100644 --- a/app/globals.css +++ b/app/globals.css @@ -435,6 +435,8 @@ textarea { .ds-link { display: inline-flex; align-items: center; + justify-content: center; + min-height: 44px; /* alvo de toque minimo e mesma altura em todos os botoes */ gap: var(--sp-2); text-decoration: none; font-size: 11px; From d5f04162a8889b294114accf221edc57fff28fe6 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sun, 4 Oct 2026 23:40:07 -0300 Subject: [PATCH 03/23] feat(projects): capa nos cards de projeto (captura real ou painel gerado em codigo) - ProjectCover usa as capturas ja publicadas (next/image, webp/avif) e, sem captura, um painel com monograma e stack - .ds-cover mantem proporcao 16:10 e nao encolhe no card flex Co-Authored-By: Claude Sonnet 5.5 --- app/components/ProjectCover.tsx | 52 ++++++++++++++++++++++++ app/components/Projects.tsx | 2 + app/components/story/notebookProjects.ts | 3 +- app/globals.css | 47 +++++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 app/components/ProjectCover.tsx diff --git a/app/components/ProjectCover.tsx b/app/components/ProjectCover.tsx new file mode 100644 index 0000000..67fca2c --- /dev/null +++ b/app/components/ProjectCover.tsx @@ -0,0 +1,52 @@ +import Image from "next/image"; +import { SCREENSHOT_BY_PROJECT_ID } from "./story/notebookProjects"; + +type CoverProject = { id: string; title: string }; + +const SCREENSHOTS: Record = + SCREENSHOT_BY_PROJECT_ID; + +function initials(title: string) { + return title + .split(/\s+/) + .filter((word) => word.length > 2 || /^[A-Z]/.test(word)) + .slice(0, 2) + .map((word) => word[0]) + .join("") + .toUpperCase(); +} + +/** + * Capa do card de projeto: a captura de tela real quando existe; sem captura, um painel gerado em código + * (só o monograma, sem repetir a stack que já aparece nas tags do card) em vez de uma imagem inventada. + */ +export function ProjectCover({ + project, + priority = false, +}: { + project: CoverProject; + priority?: boolean; +}) { + const src = SCREENSHOTS[project.id]; + + if (src) { + return ( +
+ {`Captura +
+ ); + } + + return ( + + ); +} diff --git a/app/components/Projects.tsx b/app/components/Projects.tsx index 81a3781..c7b296c 100644 --- a/app/components/Projects.tsx +++ b/app/components/Projects.tsx @@ -6,6 +6,7 @@ import type { PortfolioProjectView } from "@/lib/portfolio-data"; import { caseStudies } from "@/lib/site-content"; import { DrawLine } from "./animations/DrawLine"; import MagneticButton from "./cinematic/MagneticButton"; +import { ProjectCover } from "./ProjectCover"; import { Section } from "./Section"; export default function Projects({ @@ -59,6 +60,7 @@ export default function Projects({ whileTap={shouldReduceMotion ? undefined : { scale: 0.99 }} transition={{ type: "spring", stiffness: 170, damping: 18 }} > +
{project.category}
diff --git a/app/components/story/notebookProjects.ts b/app/components/story/notebookProjects.ts index dda3c1b..afa19b2 100644 --- a/app/components/story/notebookProjects.ts +++ b/app/components/story/notebookProjects.ts @@ -20,7 +20,8 @@ const NOTEBOOK_ORDER = [ "as-servicos", ] as const; -const SCREENSHOT_BY_PROJECT_ID: Record< +/** Capturas de tela publicadas; os cards de projeto reutilizam as mesmas imagens. */ +export const SCREENSHOT_BY_PROJECT_ID: Record< (typeof NOTEBOOK_ORDER)[number], string > = { diff --git a/app/globals.css b/app/globals.css index 997e631..3838add 100644 --- a/app/globals.css +++ b/app/globals.css @@ -942,6 +942,53 @@ textarea { } } +/* Capa dos cards de projeto: captura real ou painel gerado em codigo */ +.ds-cover { + position: relative; + flex: none; /* o card e uma coluna flex: sem isto a capa encolhe e perde a proporcao */ + aspect-ratio: 16 / 10; + overflow: hidden; + margin-bottom: var(--sp-5); + border: 1px solid var(--border); + background: var(--ink); + + & img { + width: 100%; + height: 100%; + object-fit: cover; + object-position: top; + transition: transform 0.7s var(--ease-out); + } + + .ds-card:hover & img { + transform: scale(1.035); + } +} + +.ds-section-dark .ds-cover { + border-color: rgba(255, 255, 255, 0.1); +} + +.ds-cover-fallback { + display: grid; + place-items: center; + padding: var(--sp-4); + color: var(--paper); + background: + radial-gradient(120% 90% at 100% 0%, var(--slash-dim), transparent 60%), + linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px) 0 0 / 100% 32px, + linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px) 0 0 / 32px 100%, + var(--ink); + + & .ds-cover-mark { + font-family: var(--font-display); + font-size: clamp(48px, 7vw, 96px); + font-weight: 800; + letter-spacing: -0.05em; + color: var(--slash); + } +} + /* Cinematic card hover effect */ .ds-card { transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1); From 2b5b4a22675a9cbb23694856bddffa3495418a54 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sun, 4 Oct 2026 23:42:27 -0300 Subject: [PATCH 04/23] 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; From 2ffe718fac6339593c0155205de4dbe3f35d3e73 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sun, 4 Oct 2026 23:44:00 -0300 Subject: [PATCH 05/23] feat(typography): texto corrido em sans 16px; mono so em rotulos, tags e metadados - --font-body (Space Grotesk) aplicado a p/li/dd de main e footer com :where() (especificidade zero) - rotulos (.ds-eyebrow, .ds-kicker, .ds-tag, .ds-metric, .quality-*) seguem em Geist Mono - .ds-copy 16px/1.6, descricoes e listas 15px/1.7 (celular deixa de usar 13px) - cards de servicos (WorkModalities) em sans Co-Authored-By: Claude Sonnet 5.5 --- app/components/WorkModalities.tsx | 6 +++--- app/globals.css | 35 ++++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/app/components/WorkModalities.tsx b/app/components/WorkModalities.tsx index 6db6137..12afc77 100644 --- a/app/components/WorkModalities.tsx +++ b/app/components/WorkModalities.tsx @@ -165,7 +165,7 @@ function TierCard({ tier }: Readonly<{ tier: Tier }>) {
{tier.pricing}
-
+
{tier.pricingNote}
@@ -253,10 +253,10 @@ export function WorkModalities() { href={service.href} className="min-w-0 rounded-[8px] border border-[var(--border)] bg-white/70 px-sp-4 py-sp-4 transition-colors duration-200 hover:border-[var(--slash)] hover:bg-white" > -
+
{service.label}
-
+
{service.description}
diff --git a/app/globals.css b/app/globals.css index 3838add..6d71c8d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -55,6 +55,7 @@ --font-display: var(--font-space-grotesk), sans-serif; --font-hero: var(--font-monument-extended), sans-serif; --font-mono: var(--font-geist-mono), monospace; + --font-body: var(--font-display); --ease-out: cubic-bezier(0.16, 1, 0.3, 1); --ease-in: cubic-bezier(0.4, 0, 1, 1); } @@ -197,6 +198,24 @@ svg { max-width: 100%; } +/* Texto corrido em sans (leitura); a monoespacada fica nos rotulos, tags e metadados. + :where() mantem especificidade zero: classes de componente e utilitarios continuam mandando. */ +:where(main, footer) :where(p, li, dd) { + font-family: var(--font-body); +} + +:where( + .ds-eyebrow, + .ds-kicker, + .ds-tag, + .ds-metric, + .quality-code, + .quality-hint, + .quality-disclaimer + ) { + font-family: var(--font-mono); +} + @supports (overflow: clip) { html, body { @@ -326,8 +345,8 @@ textarea { .ds-section-description { max-width: 58ch; color: rgba(10, 10, 10, 0.74); - font-size: 14px; - line-height: 1.82; + font-size: 15px; + line-height: 1.7; } .ds-grid-2, @@ -382,8 +401,8 @@ textarea { .ds-copy { margin-top: var(--sp-4); color: var(--ink-80); - font-size: 14px; - line-height: 1.8; + font-size: 16px; + line-height: 1.6; flex-grow: 1; } @@ -497,8 +516,8 @@ textarea { .ds-list { margin-top: var(--sp-5); padding-left: 18px; - font-size: 14px; - line-height: 1.85; + font-size: 15px; + line-height: 1.7; flex-grow: 1; } @@ -800,8 +819,8 @@ textarea { .ds-section-description, .ds-copy, .ds-list { - font-size: 13px; - line-height: 1.75; + font-size: 15px; + line-height: 1.65; } .ds-tags { From 2a428ec7a21aef5656b2fdf416dc14f8f51093de Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sun, 4 Oct 2026 23:44:45 -0300 Subject: [PATCH 06/23] feat(mobile): espacamento menor, rotulos legiveis e botao de analytics compacto no celular - .ds-section usa clamp(52px, 13vw, 64px) em vez de 72px; header de secao com menos margem - eyebrow/kicker 11px e tags/links 12px no celular (antes 9-11px) - botao persistente de analytics mostra 'Cookies' abaixo de 640px; o nome acessivel continua 'Preferencias de analytics' Co-Authored-By: Claude Sonnet 5.5 --- app/components/GoogleAnalytics.tsx | 5 ++++- app/globals.css | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/components/GoogleAnalytics.tsx b/app/components/GoogleAnalytics.tsx index a091fe1..9ddf67b 100644 --- a/app/components/GoogleAnalytics.tsx +++ b/app/components/GoogleAnalytics.tsx @@ -246,7 +246,10 @@ function GoogleAnalyticsConsent() { className={`${buttonClassName} fixed bottom-sp-4 right-sp-4 z-[9980] border-white/25 bg-[var(--ink)] text-white shadow-[4px_4px_0_var(--slash)]`} onClick={() => setPreferencesOpen((open) => !open)} > - Preferências de analytics + Preferências de analytics + {preferencesOpen && (
Date: Sun, 4 Oct 2026 23:44:45 -0300 Subject: [PATCH 07/23] refactor(footer): remove o convite de contato duplicado (fica a secao Contato acima) e mantem o e-mail direto Co-Authored-By: Claude Sonnet 5.5 --- app/components/Footer.test.tsx | 7 ++++--- app/components/Footer.tsx | 19 +------------------ 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/app/components/Footer.test.tsx b/app/components/Footer.test.tsx index 8c9e174..d42b1dc 100644 --- a/app/components/Footer.test.tsx +++ b/app/components/Footer.test.tsx @@ -2,12 +2,13 @@ import { render, screen } from "@testing-library/react"; import Footer from "./Footer"; describe("Footer contact and navigation", () => { - it("offers project contact and a direct mailto link", () => { + it("offers a direct mailto link without repeating the contact call to action", () => { render(