feat(home): posicionamento com prova, menu sem recarregar e WhatsApp em todo o site

H1 'Sites e sistemas que trazem pedidos pelo WhatsApp e tiram sua operação da planilha.'; HeroWork maior com a SEV; menu Cases, Serviços, Como trabalho, Sobre e IA com next/link e pré-carregamento no hover; menu do celular em tela cheia; WhatsApp flutuante e no rodapé com a mensagem da rota; rodapé em todas as páginas; FAQ da home; /ia como oferta de revisão de app feito com IA; formulário com noscript, validação do mailto e plano B do pop-up; cartões sem foco indevido; sem a fonte Monument.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 14:15:25 -03:00
1 parent e80f73d075
commit 10f170abf4
56 files changed
+3099 -906

No files matched your search

+24 -43
View File
@@ -23,28 +23,25 @@ const SOCIAL_LINKS = [
];
/**
* Números reais, calculados em app/page.tsx (nada digitado à mão): os anos vêm de public/portfolio-data.json.
* Números reais, calculados em app/page.tsx (nada digitado à mão): os anos vêm de public/portfolio-data.json
* e aparecem na mesma linha de "Já entregue para" (ClientStrip).
*/
export type HeroStats = {
years?: number;
};
type StatItem = {
value: string;
label: string;
};
function statItems(stats?: HeroStats): StatItem[] {
if (!stats?.years) return [];
return [{ value: String(stats.years), label: "anos de experiência" }];
}
/** O destaque em limão fica num trecho curto: "pedidos pelo WhatsApp" (palavras 5 a 7). */
const ACCENT = new Set([5, 6, 7]);
/** Título em palavras para a entrada em cascata; o texto segue inteiro para leitores de tela e buscadores. */
const TITLE =
"Sistemas sob medida para operações que não cabem em soluções prontas.".split(
" ",
);
const ACCENT = new Set([1, 2]);
"Sites e sistemas que trazem pedidos pelo WhatsApp e tiram sua operação da planilha."
.split(" ")
.map((word, index) => ({
word,
// a mesma palavra se repete ("e"), então a posição entra na chave
key: `${word}-${index}`,
accent: ACCENT.has(index),
}));
/** O que cada camada da cena significa, em linguagem simples. */
const CAPTIONS = [
@@ -63,11 +60,9 @@ export default function Hero({
}: {
stats?: HeroStats;
clients?: ClientLink[];
/** Endereço mostrado na barra do navegador de HeroWork (o do site da Lavação 047). */
/** Endereço mostrado na barra do navegador de HeroWork (o domínio próprio do site da SEV Moda Íntima). */
workUrl?: HeroWorkUrl;
}) {
const items = statItems(stats);
return (
<section
id="home"
@@ -94,12 +89,12 @@ export default function Hero({
Evandro Fonseca Junior · Engenharia de software
</p>
{/* Sem opacity:0: as palavras pintam no primeiro quadro (LCP); a entrada anima só posição e foco. */}
{/* No desktop o título para antes da coluna HeroWork (largura da coluna: clamp(300px,24vw,400px)). */}
<h1 className="hero-title max-w-[12.5em] lg:max-w-[min(11em,54vw,calc(100%-clamp(300px,24vw,400px)-24px))] text-[clamp(38px,min(6vw,9svh),124px)] font-semibold leading-[0.98] tracking-[-0.05em]">
{TITLE.map((word, index) => (
<Fragment key={word}>
{/* No desktop o título ocupa até 54% da largura; a coluna HeroWork fica com 40%, à direita. */}
<h1 className="hero-title max-w-[16em] text-[clamp(34px,min(8.4vw,5.2svh+14px),84px)] font-semibold leading-[1.02] tracking-[-0.045em] lg:max-w-[54%]">
{TITLE.map(({ word, key, accent }, index) => (
<Fragment key={key}>
<span
className={`hero-word${ACCENT.has(index) ? " text-[var(--slash)]" : ""}`}
className={`hero-word${accent ? " text-[var(--slash)]" : ""}`}
style={{ "--i": index } as CSSProperties}
>
{word}
@@ -107,10 +102,9 @@ export default function Hero({
</Fragment>
))}
</h1>
<p className="hero-in mt-sp-4 max-w-[34em] text-[clamp(16px,0.95rem+0.4vw,22px)] leading-[1.55] text-[rgba(244,242,237,.8)] [--d:.55s] lg:max-w-[min(40em,calc(100vw-clamp(300px,24vw,400px)-9rem))]">
Para empresas que cresceram além das planilhas e dos sistemas
prontos: sites, sistemas web, aplicativos e integrações, com contato
direto com quem desenvolve.
<p className="hero-in mt-sp-4 max-w-[34em] text-[clamp(16px,0.95rem+0.4vw,22px)] leading-[1.55] text-[rgba(244,242,237,.8)] [--d:.55s] lg:max-w-[min(36em,54%)]">
Do catálogo com pedido no WhatsApp ao painel da sua equipe. Você
fala direto com quem desenvolve.
</p>
<div className="hero-in mt-sp-5 flex flex-col gap-sp-3 [--d:.65s] sm:flex-row sm:flex-wrap sm:items-center">
@@ -141,7 +135,8 @@ export default function Hero({
<ClientStrip
clients={clients}
className="hero-in mt-sp-4 [--d:.75s] lg:max-w-[min(44rem,calc(100vw-clamp(300px,24vw,400px)-9rem))]"
years={stats?.years}
className="hero-in mt-sp-3 [--d:.75s]"
/>
<HeroWork url={workUrl} />
@@ -149,24 +144,10 @@ export default function Hero({
<div className="hero-foot relative mx-auto flex w-full max-w-[max(1400px,var(--shell))] flex-wrap items-end justify-between gap-sp-5 px-sp-6 pb-sp-5 lg:px-sp-12">
<div className="flex flex-wrap items-end gap-x-sp-8 gap-y-sp-4 border-t border-white/10 pt-sp-4">
{items.length > 0 && (
<dl className="flex gap-x-sp-5 gap-y-sp-4 sm:flex-wrap sm:gap-x-sp-8">
{items.map(({ value, label }) => (
<div key={label} className="flex flex-col-reverse">
<dt className="text-[13px] leading-[1.4] text-[rgba(244,242,237,.64)]">
{label}
</dt>
<dd className="font-mono text-[clamp(22px,1.1rem+0.8vw,36px)] font-semibold leading-none tracking-[-0.03em] text-[var(--slash)]">
{value}
</dd>
</div>
))}
</dl>
)}
{/* no celular, serviços e redes ficam no menu e no rodapé: assim o hero cabe numa tela e pode ficar fixo */}
<div className="max-lg:hidden">
<ul className="flex flex-wrap gap-x-sp-5 gap-y-sp-2 text-[14px] leading-[1.5] text-[rgba(244,242,237,.8)]">
{["Sistemas web", "Aplicativos", "Integrações"].map(
{["Sites", "Sistemas web", "Aplicativos", "Integrações"].map(
(service) => (
<li key={service} className="flex items-center gap-sp-2">
<span