feat(site): blocos 3D da marca, novas ofertas e hero mais abrangente
Hero e movimento - cena 3D em tempo real com os blocos da marca (kit feito no Blender, 3d/blocos.py): marca, tela, integrações e dados; segue a rolagem, tem vida em repouso, reage ao cursor e, parada no topo, passa sozinha pelos estados com legenda - três níveis de aparelho (full, lite, static); sem WebGL fica o pôster - título novo: "Sites e sistemas sob medida para o jeito que a sua empresa trabalha." - faixa de clientes com o ramo, linha do primeiro passo, hero alinhado às seções - rótulos digitados, entrada dos blocos, cortina de blocos entre páginas e capa compartilhada entre a lista de cases e o case Ofertas - atendimento no WhatsApp com IA e nota fiscal para a reforma tributária (páginas de serviço, cartões da home, formulário e aviso com data de validade) - página /ia atualizada, com fontes - peças 3D em loop nos cartões e topos de página (3d/pecas.py) Design - hover por papel (bloco nos botões, barra nos links), rótulos sem caixa alta - um destino por rótulo: "Falar sobre meu projeto" leva ao formulário, "Chamar no WhatsApp" abre o WhatsApp - logo montado em blocos, 404 redesenhado, grades sem célula vazia - ícones do PWA e manifesto novos Specs em specs/, contexto de design em DESIGN.md e PRODUCT.md.
This commit is contained in:
1 parent
4671837c44
commit
c7d09d63d4
165 files changed
+7940
-3790
No files matched your search
+121
-45
@@ -1,16 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { type CSSProperties, Fragment } from "react";
|
||||
import { trackEmailClick, trackSocialClick } from "@/lib/analytics";
|
||||
import { CONTACT_EMAIL, LINKEDIN_URL } from "@/lib/site-navigation";
|
||||
import { HOME_SUBJECT } from "@/lib/whatsapp";
|
||||
import { textLinkDark } from "./button-styles";
|
||||
import ClientStrip, { type ClientLink } from "./ClientStrip";
|
||||
import HeroField from "./cinematic/HeroField";
|
||||
import { CONTACT_FORM_HASH } from "./contact-subject";
|
||||
import { HERO_CTA_ID } from "./FloatingWhatsApp";
|
||||
import HeroWork, { type HeroWorkUrl } from "./HeroWork";
|
||||
import HeroWork from "./HeroWork";
|
||||
import HoverPrefetchLink from "./HoverPrefetchLink";
|
||||
import { HERO_CAPTIONS } from "./hero-captions";
|
||||
import WhatsAppLink from "./WhatsAppLink";
|
||||
import { WhatsAppMark } from "./WhatsAppMark";
|
||||
|
||||
const SOCIAL_LINKS = [
|
||||
{
|
||||
@@ -33,28 +37,58 @@ export type HeroStats = {
|
||||
years?: number;
|
||||
};
|
||||
|
||||
/** 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. */
|
||||
/** O destaque em limão fica num trecho curto: "sob medida" (palavras 3 e 4). */
|
||||
const ACCENT = new Set([3, 4]);
|
||||
|
||||
type TitleWord = { word: string; key: string; index: number };
|
||||
|
||||
/**
|
||||
* Título em palavras para a entrada em cascata; o texto segue inteiro para leitores de tela e buscadores. As palavras
|
||||
* do destaque seguidas viram um trecho só: elas nunca se separam numa quebra de linha, e o bloco do marcador as cobre
|
||||
* de uma vez.
|
||||
*/
|
||||
const TITLE =
|
||||
"Sites e sistemas que trazem pedidos pelo WhatsApp e tiram sua operação da planilha."
|
||||
"Sites e sistemas sob medida para o jeito que a sua empresa trabalha."
|
||||
.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),
|
||||
}));
|
||||
.reduce<{ key: string; accent: boolean; words: TitleWord[] }[]>(
|
||||
(runs, word, index) => {
|
||||
const accent = ACCENT.has(index);
|
||||
// a mesma palavra se repete ("e"), então a posição entra na chave
|
||||
const entry = { word, key: `${word}-${index}`, index };
|
||||
const last = runs.at(-1);
|
||||
if (accent && last?.accent) last.words.push(entry);
|
||||
else runs.push({ key: entry.key, accent, words: [entry] });
|
||||
return runs;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
function TitleWords({
|
||||
words,
|
||||
accent,
|
||||
}: {
|
||||
words: TitleWord[];
|
||||
accent: boolean;
|
||||
}) {
|
||||
return words.map(({ word, key, index }, position) => (
|
||||
<Fragment key={key}>
|
||||
{position > 0 && " "}
|
||||
<span
|
||||
className={`hero-word${accent ? " text-[var(--slash)]" : ""}`}
|
||||
style={{ "--i": index } as CSSProperties}
|
||||
>
|
||||
{word}
|
||||
</span>
|
||||
</Fragment>
|
||||
));
|
||||
}
|
||||
|
||||
export default function Hero({
|
||||
stats,
|
||||
clients,
|
||||
workUrl,
|
||||
}: {
|
||||
stats?: HeroStats;
|
||||
clients?: ClientLink[];
|
||||
/** Endereço mostrado na barra do navegador de HeroWork (o domínio próprio do site da SEV Moda Íntima). */
|
||||
workUrl?: HeroWorkUrl;
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
@@ -63,8 +97,9 @@ export default function Hero({
|
||||
>
|
||||
<div className="hero-stage">
|
||||
<div className="hero-glow" aria-hidden="true" />
|
||||
<HeroField />
|
||||
<div className="hero-scrim" aria-hidden="true" />
|
||||
{/* a cena 3D fica acima do véu (o mesmo brilho do pôster) e abaixo das legendas e do texto */}
|
||||
<HeroField />
|
||||
{/* Legendas do mergulho: aparecem enquanto a câmera atravessa cada camada (só no hero fixo, telas grandes). */}
|
||||
<ol className="hero-captions">
|
||||
{HERO_CAPTIONS.map(({ title, text, from, to }, index) => (
|
||||
@@ -81,38 +116,48 @@ export default function Hero({
|
||||
</ol>
|
||||
|
||||
{/* Os espaços verticais acompanham a altura da tela (svh): subtítulo, botões e faixa de clientes precisam caber no palco fixo. */}
|
||||
<div className="hero-content relative mx-auto flex w-full max-w-[max(1400px,var(--shell))] flex-1 flex-col justify-center px-sp-6 pt-[calc(var(--site-header-height)+clamp(8px,2.5svh,32px))] pb-[clamp(16px,3svh,32px)] lg:px-sp-12">
|
||||
<div className="hero-content relative mx-auto flex w-full max-w-[max(1400px,var(--shell))] flex-1 flex-col justify-center px-sp-6 pt-[calc(var(--site-header-height)+clamp(8px,2.5svh,32px))] pb-[clamp(16px,3svh,32px)]">
|
||||
{/* Space Grotesk (a fonte do título, já carregada), e não a Geist Mono: assim nada de fonte nova é baixado antes do LCP. */}
|
||||
<p className="hero-in mb-sp-4 flex items-center gap-sp-3 font-display text-[12px] uppercase leading-[1.5] tracking-[0.14em] text-[rgba(244,242,237,.72)]">
|
||||
<span aria-hidden="true" className="h-2 w-2 bg-[var(--slash)]" />
|
||||
Evandro Fonseca Junior · Engenharia de software
|
||||
{/* Abaixo de 1024 px o rótulo divide a linha com a peça: fica só a profissão, e o nome volta na tela larga. */}
|
||||
<p className="hero-in hero-kicker mb-sp-4 flex items-center gap-sp-3 font-display text-[13px] leading-[1.5] tracking-[0.02em] text-[rgba(244,242,237,.72)]">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-2 w-2 shrink-0 bg-[var(--slash)]"
|
||||
/>
|
||||
<span>
|
||||
<span className="max-lg:hidden">Evandro Fonseca Junior · </span>
|
||||
Engenharia de software
|
||||
</span>
|
||||
</p>
|
||||
{/* Sem opacity:0: as palavras pintam no primeiro quadro (LCP); a entrada anima só posição e foco. */}
|
||||
{/* 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) => (
|
||||
{/* No desktop o texto fica na coluna da esquerda (--hero-col, em globals.css); a peça (.hero-piece) vem logo à direita. */}
|
||||
<h1 className="hero-title hero-col max-w-[16em] text-[clamp(34px,min(8.4vw,5.2svh+14px),84px)] font-semibold leading-[1.02] tracking-[-0.045em]">
|
||||
{TITLE.map(({ key, accent, words }) => (
|
||||
<Fragment key={key}>
|
||||
<span
|
||||
className={`hero-word${accent ? " text-[var(--slash)]" : ""}`}
|
||||
style={{ "--i": index } as CSSProperties}
|
||||
>
|
||||
{word}
|
||||
</span>{" "}
|
||||
{accent ? (
|
||||
// o marcador (::after) vive no trecho e não em cada palavra: um bloco contínuo sobre "sob medida"
|
||||
<span
|
||||
className="hero-mark whitespace-nowrap"
|
||||
style={{ "--i": words[0].index } as CSSProperties}
|
||||
>
|
||||
<TitleWords words={words} accent />
|
||||
</span>
|
||||
) : (
|
||||
<TitleWords words={words} accent={false} />
|
||||
)}{" "}
|
||||
</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(36em,54%)]">
|
||||
Do catálogo com pedido no WhatsApp ao painel da sua equipe. Você
|
||||
<p className="hero-in hero-col mt-sp-4 max-w-[34em] text-[clamp(16px,0.95rem+0.4vw,22px)] leading-[1.55] text-[rgba(244,242,237,.8)] [--d:.4s]">
|
||||
Do site institucional ao sistema que a sua equipe usa todo dia. Você
|
||||
fala 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">
|
||||
{/* Abre o WhatsApp direto, com o assunto da home: o formulário continua na seção de contato, ao fim da página. */}
|
||||
<WhatsAppLink
|
||||
id={HERO_CTA_ID}
|
||||
service={HOME_SUBJECT}
|
||||
placement="hero"
|
||||
className="group inline-flex min-h-12 items-center justify-center gap-sp-3 bg-[var(--slash)] px-sp-4 py-sp-3 text-center text-[15px] font-semibold leading-6 text-[var(--ink)] no-underline transition-colors hover:bg-[#d9ff40] motion-reduce:transition-none sm:px-sp-6 sm:text-[16px]"
|
||||
<div className="hero-in hero-col hero-actions mt-sp-5 flex flex-col gap-sp-3 [--d:.48s] sm:flex-row sm:flex-wrap sm:items-center">
|
||||
{/* O formulário, como o botão do cabeçalho; quem prefere o WhatsApp tem o link logo depois dos botões. */}
|
||||
<HoverPrefetchLink
|
||||
href={`/${CONTACT_FORM_HASH}`}
|
||||
className="cv-fill group inline-flex min-h-12 items-center justify-center gap-sp-3 bg-[var(--slash)] px-sp-4 py-sp-3 text-center text-[15px] font-semibold leading-6 text-[var(--ink)] no-underline sm:px-sp-6 sm:text-[16px]"
|
||||
>
|
||||
Falar sobre meu projeto
|
||||
<svg
|
||||
@@ -125,28 +170,59 @@ export default function Hero({
|
||||
>
|
||||
<path d="M2 8h11M9 3.5 13.5 8 9 12.5" />
|
||||
</svg>
|
||||
</WhatsAppLink>
|
||||
</HoverPrefetchLink>
|
||||
<HoverPrefetchLink
|
||||
href="/cases"
|
||||
className="inline-flex min-h-12 items-center justify-center border border-white/25 bg-transparent px-sp-5 py-sp-3 text-center text-[15px] font-medium leading-6 text-[#f4f2ed] no-underline transition-colors hover:border-white/55 motion-reduce:transition-none sm:text-[16px]"
|
||||
className="cv-outline-dark inline-flex min-h-12 items-center justify-center border border-white/25 bg-transparent px-sp-5 py-sp-3 text-center text-[15px] font-medium leading-6 text-[#f4f2ed] no-underline sm:text-[16px]"
|
||||
>
|
||||
Ver projetos entregues
|
||||
</HoverPrefetchLink>
|
||||
{/* Abre o WhatsApp com o assunto da home; enquanto está à vista, o botão flutuante (que observa este id) some. */}
|
||||
<WhatsAppLink
|
||||
id={HERO_CTA_ID}
|
||||
service={HOME_SUBJECT}
|
||||
placement="hero"
|
||||
className={`${textLinkDark} gap-sp-2 self-start sm:self-auto`}
|
||||
>
|
||||
<WhatsAppMark className="h-4 w-4 shrink-0" />
|
||||
Chamar no WhatsApp
|
||||
</WhatsAppLink>
|
||||
</div>
|
||||
{/* O primeiro passo, dito antes de a pessoa clicar: tira a dúvida de "e depois, o que acontece?". */}
|
||||
<p className="hero-in hero-col mt-sp-3 font-display text-[15px] leading-6 text-[rgba(244,242,237,.64)] [--d:.54s]">
|
||||
Começa com uma conversa para entender o seu caso. A proposta vem por
|
||||
escrito.
|
||||
</p>
|
||||
|
||||
<ClientStrip
|
||||
clients={clients}
|
||||
years={stats?.years}
|
||||
className="hero-in mt-sp-3 [--d:.75s]"
|
||||
className="hero-in hero-col mt-sp-3 [--d:.6s]"
|
||||
/>
|
||||
|
||||
<HeroWork url={workUrl} />
|
||||
<HeroWork />
|
||||
|
||||
{/* A marca em blocos: pôster renderizado no Blender, decorativo. Carrega junto com a página (eager, sem prioridade
|
||||
alta). O título segue como o maior elemento medido (LCP) no celular e em tela larga: a imagem entra com
|
||||
fade a partir de opacidade zero e some por `visibility` quando a cena assume (globals.css). */}
|
||||
<div className="hero-piece hero-in [--d:.3s]" aria-hidden="true">
|
||||
<Image
|
||||
src="/3d/hero-poster.webp"
|
||||
alt=""
|
||||
width={1200}
|
||||
height={1200}
|
||||
sizes="(min-width: 1024px) min(56vw, 816px), 30vw"
|
||||
loading="eager"
|
||||
className="block h-auto w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="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">
|
||||
<div className="flex flex-wrap items-end gap-x-sp-8 gap-y-sp-4 border-t border-white/10 pt-sp-4">
|
||||
{/* 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">
|
||||
{/* no celular, serviços e redes ficam no menu e no rodapé: assim o hero cabe numa tela e pode ficar fixo; no
|
||||
computador ficam numa linha só, para o palco fixo caber em 1024x768 */}
|
||||
<div className="flex-wrap items-center gap-x-sp-8 max-lg:hidden lg:flex">
|
||||
<ul className="flex flex-wrap gap-x-sp-5 gap-y-sp-2 text-[14px] leading-[1.5] text-[rgba(244,242,237,.8)]">
|
||||
{["Sites", "Sistemas web", "Aplicativos", "Integrações"].map(
|
||||
(service) => (
|
||||
@@ -185,7 +261,7 @@ export default function Hero({
|
||||
</div>
|
||||
<p
|
||||
aria-hidden="true"
|
||||
className="hero-cue hidden items-center gap-sp-3 font-display text-[12px] uppercase tracking-[0.14em] text-[rgba(244,242,237,.55)] lg:flex"
|
||||
className="hero-cue hidden items-center gap-sp-3 font-display text-[13px] tracking-[0.02em] text-[rgba(244,242,237,.55)] xl:flex"
|
||||
>
|
||||
Role para mergulhar
|
||||
<span className="hero-cue-line" />
|
||||
|
||||
Reference in new issue
Block a user