feat: refine portfolio showcase, services and quality lab
CI / verify (push) Failing after 5m9s

Align the notebook browser with the physical LCD, refresh real project captures and navigation, simplify service content, and stabilize hero hydration.
This commit is contained in:
Evandro Fonseca Junior committed 2026-09-05 14:20:54 -03:00
1 parent e2b20bc699
commit 8b12e51bc5
35 files changed
+2359 -1299

No files matched your search

+312 -84
View File
@@ -1,115 +1,343 @@
"use client";
import { motion } from "motion/react";
import { useReducedMotion } from "@/app/hooks/useReducedMotion";
import { Section } from "./Section";
import { useState } from "react";
const METRICS = [
{
score: 100,
id: "performance",
label: "Performance",
code: "LCP / INP / CLS",
title: "O essencial chega primeiro.",
description:
"Cada milissegundo conta. Tempos de carregamento instantâneos retêm mais usuários, aumentam a conversão e garantem que a interface pareça nativa.",
"O carregamento começa pelo que importa. Imagens, fontes e JavaScript precisam trabalhar a favor da experiência, não competir com ela.",
checks: [
"Conteúdo principal com prioridade de carregamento",
"Dimensões reservadas para evitar saltos de layout",
"JavaScript carregado onde faz diferença",
],
caption: "Em foco: o conteúdo principal e a estabilidade da página.",
},
{
score: 100,
id: "accessibility",
label: "Acessibilidade",
code: "TECLADO / FOCO / SEMÂNTICA",
title: "A interface inclui mais pessoas.",
description:
"Acesso universal. Contraste perfeito, navegação por teclado e semântica adequada garantindo o uso por todos os perfis e mitigando riscos.",
"Uma boa interface não exige um jeito único de navegar. Teclado, leitores de tela e preferências de movimento fazem parte do projeto.",
checks: [
"Foco visível e navegação por teclado",
"Controles com nomes e estrutura semântica",
"Contraste legível e movimento reduzido",
],
caption:
"Em foco: controles reconhecíveis e um caminho claro pelo teclado.",
},
{
score: 100,
label: "Best Practices",
id: "practices",
label: "Boas práticas",
code: "HTTPS / PRIVACIDADE / DEPENDÊNCIAS",
title: "Confiança também se constrói.",
description:
"Código seguro e moderno. Uso das APIs mais recentes, prevenção inteligente contra vulnerabilidades comuns e front-end limpo.",
"Cuidar da entrega também é cuidar dos dados, das dependências e dos limites do navegador. Segurança não se resume a uma nota.",
checks: [
"Conexões seguras e políticas do navegador",
"Analytics condicionado ao consentimento",
"Dependências verificadas e atualizadas",
],
caption: "Em foco: a conexão e os limites de acesso aos dados.",
},
{
score: 100,
id: "seo",
label: "SEO",
code: "HTML / METADATA / INDEXAÇÃO",
title: "Conteúdo pronto para ser encontrado.",
description:
"Rankeamento orgânico superior. Estrutura impecável para indexação precisa pelos motores de busca e otimização para crawlers sociais.",
"Estrutura antes de promessas. Conteúdo claro, títulos coerentes e dados estruturados ajudam pessoas e buscadores a entender cada página.",
checks: [
"Títulos, descrições e URLs canônicas",
"HTML semântico e conteúdo rastreável",
"Sitemap e dados estruturados factuais",
],
caption: "Em foco: a estrutura que dá contexto ao conteúdo.",
},
];
const BLUEPRINT = (
<svg
className="quality-blueprint"
viewBox="0 0 640 440"
fill="none"
aria-hidden="true"
focusable="false"
>
<g className="quality-dimensions">
<path d="M64 68V48m512 20V48M64 58h512M44 86H24m20 284H24M34 86v284" />
<path d="m60 54 8 8m504-8 8 8M30 82l8 8m-8 276 8 8" />
<text x="304" y="47">
RESPONSIVE
</text>
<path d="M64 390v18m512-18v18M64 400h180m152 0h180" />
<text x="272" y="405">
WEB / BLUEPRINT
</text>
</g>
<rect
className="quality-browser-frame"
x="64"
y="86"
width="512"
height="284"
rx="8"
/>
<path className="quality-wire" d="M64 123h512" />
<g className="quality-browser-dots">
<circle cx="83" cy="105" r="3" />
<circle cx="95" cy="105" r="3" />
<circle cx="107" cy="105" r="3" />
</g>
<rect
className="quality-wire"
x="142"
y="96"
width="350"
height="18"
rx="3"
/>
<text className="quality-address" x="158" y="109">
evandro.dev.br
</text>
<path className="quality-wire" d="M539 100h15m-15 5h15m-15 5h15" />
<text className="quality-browser-heading" x="96" y="176">
Engenharia
</text>
<text className="quality-browser-heading" x="96" y="203">
que funciona.
</text>
<path className="quality-wire" d="M96 222h170M96 230h130" />
<rect
className="quality-browser-button"
x="96"
y="254"
width="142"
height="32"
rx="2"
/>
<text className="quality-browser-button-label" x="112" y="274">
EXPLORAR PROJETO
</text>
<rect
className="quality-wire"
x="330"
y="144"
width="216"
height="142"
rx="3"
/>
<g className="quality-wire">
<path d="m438 164 58 33v55l-58 33-58-33v-55zM380 197l58 33 58-33m-58 33v55m0-121v66" />
<path d="m409 181 58 33v55m-58 0v-55l58-33" />
<path d="M96 306h450M96 324h113m-113 10h80m84-10h113m-113 10h80m84-10h122m-122 10h80" />
</g>
<g className="quality-layer" data-layer="performance">
<rect
className="quality-highlight"
x="318"
y="133"
width="240"
height="165"
rx="4"
/>
<path className="quality-trace" pathLength="1" d="M558 216h43V59h-91" />
<circle cx="558" cy="216" r="4" />
<text x="393" y="63">
LCP / CONTEÚDO
</text>
<path d="M330 299v12m216-12v12m-216-6h216" />
<rect
className="quality-annotation"
x="368"
y="311"
width="154"
height="18"
/>
<text x="376" y="323">
ESPAÇO RESERVADO
</text>
</g>
<g className="quality-layer" data-layer="accessibility">
<rect
className="quality-highlight quality-focus-frame"
x="88"
y="246"
width="158"
height="48"
rx="5"
/>
<path className="quality-trace" pathLength="1" d="M88 270H50v149h20" />
<circle cx="88" cy="270" r="4" />
<text x="79" y="424">
TAB / FOCO VISÍVEL
</text>
<rect x="260" y="259" width="45" height="24" rx="3" />
<text x="270" y="275">
TAB
</text>
</g>
<g className="quality-layer" data-layer="practices">
<rect
className="quality-highlight"
x="134"
y="91"
width="366"
height="28"
rx="4"
/>
<path className="quality-trace" pathLength="1" d="M500 105h101v314h-90" />
<circle cx="500" cy="105" r="4" />
<path
className="quality-highlight"
d="m499 220 36 13v32c0 25-36 41-36 41s-36-16-36-41v-32z"
/>
<rect x="487" y="255" width="24" height="20" rx="2" />
<path d="M491 255v-7a8 8 0 0 1 16 0v7m-8 6v7" />
<text x="290" y="424">
HTTPS / CONSENTIMENTO
</text>
</g>
<g className="quality-layer" data-layer="seo">
<rect
className="quality-highlight"
x="85"
y="144"
width="224"
height="95"
rx="3"
/>
<path className="quality-trace" pathLength="1" d="M85 159H48V58h135" />
<circle cx="85" cy="159" r="4" />
<text x="74" y="49">
HTML / SEMÂNTICA
</text>
<text x="99" y="140">
&lt;h1&gt;
</text>
<path d="M331 317h213v29H331z" />
<text x="350" y="337">
&lt;article&gt; / &lt;nav&gt;
</text>
</g>
</svg>
);
export default function LighthouseMetrics() {
const shouldReduceMotion = useReducedMotion();
const [active, setActive] = useState(METRICS[0]);
return (
<Section
<section
id="lighthouse"
eyebrow="Quality & Performance"
title="Engenharia de ponta a ponta: 100/100 não é vaidade."
description="Sistemas não podem apenas parecer bons. Eles precisam ser consistentes, rápidos, acessíveis, seguros e fáceis de processar. Entregar métricas máximas significa que a fundação técnica do projeto está preparada para escalar sem fricção."
dark
disableChildrenAnimation={false}
className="ds-section ds-section-dark quality-lab"
aria-labelledby="quality-title"
data-inspection={active.id}
>
<div
className="mt-sp-4 grid grid-cols-1 gap-sp-4 md:grid-cols-2 lg:grid-cols-4"
data-reel="fade"
>
{METRICS.map((metric, i) => (
<motion.article
key={metric.label}
className="ds-card flex flex-col items-center gap-sp-6 text-center lg:items-start lg:text-left"
initial={
shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 20 }
}
whileInView={
shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
}
viewport={{ once: true, amount: 0.2 }}
transition={{ duration: 0.6, delay: i * 0.1 }}
>
{/* The Ring Container */}
<div className="relative flex h-[100px] w-[100px] shrink-0 items-center justify-center rounded-full bg-[rgba(0,0,0,0.3)] shadow-inner">
<svg
className="absolute inset-0 h-full w-full rotate-[-90deg] drop-shadow-[0_0_12px_rgba(12,206,107,0.35)]"
viewBox="0 0 36 36"
aria-hidden="true"
>
{/* Background Track */}
<path
className="text-[rgba(255,255,255,0.1)]"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
/>
{/* Animated Value Track */}
<motion.path
className="text-[#0cce6b]"
initial={{ strokeDasharray: "0, 100" }}
whileInView={{ strokeDasharray: `${metric.score}, 100` }}
viewport={{ once: true }}
transition={{
duration: 1.5,
ease: "easeOut",
delay: i * 0.15 + 0.2,
}}
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
/>
</svg>
<div className="font-mono text-[24px] font-bold tracking-tight text-[#0cce6b]">
{metric.score}
</div>
</div>
<div className="ds-section-shell">
<div className="quality-intro">
<header className="quality-copy">
<p className="ds-eyebrow">Quality &amp; Performance</p>
<h2 id="quality-title" className="ds-section-title">
Engenharia de ponta a ponta.
<span>Qualidade em cada camada.</span>
</h2>
<p>
Sistemas precisam mais do que uma boa aparência. Explore as
decisões que tornam uma interface rápida, acessível, confiável e
fácil de encontrar.
</p>
<a
className="ds-link ds-link-accent"
href="/servicos/consultoria-nextjs"
>
Conversar sobre a qualidade do seu projeto
</a>
</header>
{/* Content Details */}
<div className="flex-1" data-reel-stagger="bottom">
<h3 className="font-display text-[17px] font-bold text-[rgba(255,255,255,0.92)]">
{metric.label}
</h3>
<p className="mt-sp-3 text-[13px] leading-[1.8] text-[rgba(255,255,255,.75)]">
{metric.description}
</p>
<figure className="quality-visual bg-grid-light">
<div className="quality-visual-label">
<span>Laboratório de qualidade</span>
<span>Modelo interativo</span>
</div>
</motion.article>
))}
{BLUEPRINT}
<figcaption>
{METRICS.map((metric) => (
<span key={metric.id} aria-hidden={active.id !== metric.id}>
{metric.caption}
</span>
))}
</figcaption>
</figure>
</div>
<fieldset className="quality-controls" aria-describedby="quality-hint">
<legend className="sr-only">Escolha um aspecto da qualidade</legend>
{METRICS.map((metric) => (
<label key={metric.id} className="quality-control">
<input
type="radio"
name="quality-pillar"
value={metric.id}
checked={active.id === metric.id}
onChange={() => setActive(metric)}
aria-controls="quality-report"
/>
<span className="quality-control-content">
<span className="quality-control-name">{metric.label}</span>
<span className="quality-control-target">
Meta <strong>100</strong>
<span>/100</span>
</span>
</span>
</label>
))}
</fieldset>
<p id="quality-hint" className="quality-hint">
Selecione um pilar para explorar. No teclado, use as setas.
</p>
<section
id="quality-report"
className="quality-report-stack"
aria-label="Inspeção de qualidade"
aria-live="polite"
aria-atomic="true"
>
{METRICS.map((metric) => (
<div
key={metric.id}
className="quality-report"
aria-hidden={active.id !== metric.id}
>
<div className="quality-report-copy">
<p className="quality-code">{metric.code}</p>
<h3>{metric.title}</h3>
<p>{metric.description}</p>
</div>
<ul>
{metric.checks.map((check) => (
<li key={check}>
<span aria-hidden="true">/</span>
{check}
</li>
))}
</ul>
</div>
))}
</section>
<p className="quality-disclaimer">
Metas de qualidade, não uma auditoria em tempo real. Resultados
Lighthouse variam conforme página, dispositivo e condições do teste.
</p>
</div>
</Section>
</section>
);
}