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
@@ -7,28 +7,35 @@ import {
|
||||
} from "@/lib/site-content";
|
||||
import { primaryButton, textLink } from "./button-styles";
|
||||
import ContactOfferLink from "./ContactOfferLink";
|
||||
import { FiscalNotice } from "./FiscalNotice";
|
||||
import { isFiscalNoticeActive } from "./fiscal-notice";
|
||||
import "./conversion.css";
|
||||
import HoverPrefetchLink from "./HoverPrefetchLink";
|
||||
import { OfferVideo } from "./OfferVideo";
|
||||
import { Reveal } from "./Reveal";
|
||||
import { Section } from "./Section";
|
||||
import { WhatsAppMark } from "./WhatsAppMark";
|
||||
|
||||
/** O que só a home diz de cada oferta; o nome e "O que você recebe" vêm de `serviceOffers`, a fonte única que /servicos também usa. */
|
||||
interface OfferExtras {
|
||||
description: string;
|
||||
ctaLabel: string;
|
||||
/** Case cujo pôster ilustra a oferta; sem ele, o cartão traz uma ilustração própria (ver OfferArt). */
|
||||
/** Case cujo pôster ilustra a oferta; sem ele, o cartão traz um vídeo ou uma ilustração própria (ver OfferArt). */
|
||||
poster?: { id: string; title: string };
|
||||
moreLabel: string;
|
||||
moreHref: string;
|
||||
}
|
||||
|
||||
/** Ofertas com cartão na home; a da nota fiscal entra na faixa datada acima dos cartões (FiscalNotice). */
|
||||
type HomeOfferId = Exclude<ServiceOffer["id"], "fiscal">;
|
||||
|
||||
/**
|
||||
* Os textos da home para as três ofertas, na ordem de `serviceOffers` (a do H1). Cada texto só diz o que o case ou a
|
||||
* página de /ia citados já sustentam; sem prazo nem preço. Só o site tem foto: a Lavação 047 é um site de catálogo e
|
||||
* tabela de preços, o que o texto descreve. O sistema e a revisão não têm um case que mostre o que descrevem (a vitrine
|
||||
* da SEV é uma loja, não um painel), então cada um traz uma ilustração feita em HTML e SVG, do tamanho da foto.
|
||||
* Os textos da home para as quatro ofertas, na ordem de `serviceOffers`. Cada texto só diz o que o case, a página de /ia
|
||||
* ou a página da oferta citados já sustentam; sem prazo nem preço. Só o site tem foto: a Lavação 047 é um site de
|
||||
* catálogo e tabela de preços, o que o texto descreve. O sistema, o atendimento no WhatsApp e a revisão trazem uma peça
|
||||
* 3D em vídeo (OfferVideo); por cima, o atendimento tem o selo do aplicativo e a revisão, um recorte do relatório.
|
||||
*/
|
||||
const OFFER_EXTRAS: Record<ServiceOffer["id"], OfferExtras> = {
|
||||
const OFFER_EXTRAS: Record<HomeOfferId, OfferExtras> = {
|
||||
site: {
|
||||
description:
|
||||
"O cliente vê o que você oferece e vai para a conversa no WhatsApp, como na Lavação 047.",
|
||||
@@ -44,6 +51,13 @@ const OFFER_EXTRAS: Record<ServiceOffer["id"], OfferExtras> = {
|
||||
moreLabel: "Ver case SEV Moda Íntima",
|
||||
moreHref: "/cases/sev-modaintima",
|
||||
},
|
||||
whatsapp: {
|
||||
description:
|
||||
"Para quem atende e vende pelo WhatsApp e quer responder na hora sem deixar a IA inventar preço ou prazo.",
|
||||
ctaLabel: "Começar pelo atendimento",
|
||||
moreLabel: "Ver como funciona o atendimento",
|
||||
moreHref: "/servicos/atendimento-whatsapp-ia",
|
||||
},
|
||||
revisao: {
|
||||
description:
|
||||
"Para quem já tem um sistema, como um app feito no Lovable, no Bolt ou no Replit, e quer saber se ele está pronto para receber clientes.",
|
||||
@@ -53,6 +67,10 @@ const OFFER_EXTRAS: Record<ServiceOffer["id"], OfferExtras> = {
|
||||
},
|
||||
};
|
||||
|
||||
const homeOffers = serviceOffers.flatMap((offer) =>
|
||||
offer.id === "fiscal" ? [] : [{ offer, extras: OFFER_EXTRAS[offer.id] }],
|
||||
);
|
||||
|
||||
const PROCESS = [
|
||||
{
|
||||
title: "Entender a operação",
|
||||
@@ -76,127 +94,6 @@ const PROCESS = [
|
||||
},
|
||||
];
|
||||
|
||||
/** Colunas e linhas da planilha do esquema do sistema (a primeira linha é o cabeçalho) e a largura de cada traço de dado. */
|
||||
const SHEET_COLUMNS = [0, 1, 2];
|
||||
const SHEET_DATA_ROWS = [0, 1, 2, 3, 4];
|
||||
const SHEET_DASH_WIDTHS = [18, 12, 15];
|
||||
/** Alturas das cinco barras do painel do esquema. */
|
||||
const PANEL_BARS = [14, 26, 20, 34, 28];
|
||||
|
||||
/**
|
||||
* Ilustração do sistema: as linhas de uma planilha, uma seta e um painel com três indicadores e um gráfico de barras,
|
||||
* sem números nem nomes de cliente. É um SVG com `viewBox` (cresce com o cartão) do mesmo tamanho da foto do site, só
|
||||
* em traços claros sobre o fundo escuro, e não tem texto: o nome do cartão e a lista dizem o que é.
|
||||
*/
|
||||
function SystemArt() {
|
||||
return (
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="Esquema ilustrativo: as linhas de uma planilha viram um painel com três indicadores e um gráfico de barras"
|
||||
viewBox="0 0 240 150"
|
||||
className="block h-full w-full text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<g data-art="planilha" strokeOpacity="0.45" strokeWidth="1.5">
|
||||
<rect x="14" y="30" width="80" height="92" />
|
||||
<path d="M14 46h80M40.7 30v92M67.3 30v92" strokeWidth="1" />
|
||||
</g>
|
||||
<rect
|
||||
x="15"
|
||||
y="31"
|
||||
width="78"
|
||||
height="14"
|
||||
fill="currentColor"
|
||||
fillOpacity="0.16"
|
||||
stroke="none"
|
||||
/>
|
||||
<g fill="currentColor" fillOpacity="0.5" stroke="none">
|
||||
{SHEET_DATA_ROWS.flatMap((row) =>
|
||||
SHEET_COLUMNS.map((column) => (
|
||||
<rect
|
||||
key={`${row}-${column}`}
|
||||
x={19 + column * 26.6}
|
||||
y={52 + row * 14.6}
|
||||
width={
|
||||
SHEET_DASH_WIDTHS[(row + column) % SHEET_DASH_WIDTHS.length]
|
||||
}
|
||||
height="4"
|
||||
rx="1"
|
||||
/>
|
||||
)),
|
||||
)}
|
||||
</g>
|
||||
|
||||
<path
|
||||
data-art="seta"
|
||||
d="M104 76h24M119 66l11 10-11 10"
|
||||
strokeOpacity="0.85"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
<g data-art="painel">
|
||||
<rect
|
||||
x="140"
|
||||
y="22"
|
||||
width="92"
|
||||
height="108"
|
||||
rx="3"
|
||||
fill="currentColor"
|
||||
fillOpacity="0.06"
|
||||
strokeOpacity="0.45"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
{[147, 175, 203].map((x) => (
|
||||
<g key={x}>
|
||||
<rect
|
||||
x={x}
|
||||
y="30"
|
||||
width="25"
|
||||
height="32"
|
||||
strokeOpacity="0.35"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<rect
|
||||
x={x + 4}
|
||||
y="35"
|
||||
width="12"
|
||||
height="3"
|
||||
fill="currentColor"
|
||||
fillOpacity="0.5"
|
||||
stroke="none"
|
||||
/>
|
||||
<rect
|
||||
x={x + 4}
|
||||
y="43"
|
||||
width="16"
|
||||
height="9"
|
||||
fill="currentColor"
|
||||
fillOpacity="0.92"
|
||||
stroke="none"
|
||||
/>
|
||||
</g>
|
||||
))}
|
||||
<path d="M147 118h81" strokeOpacity="0.3" strokeWidth="1" />
|
||||
{PANEL_BARS.map((height, index) => (
|
||||
<rect
|
||||
key={height}
|
||||
x={151 + index * 15}
|
||||
y={118 - height}
|
||||
width="10"
|
||||
height={height}
|
||||
fill="currentColor"
|
||||
fillOpacity="0.6"
|
||||
stroke="none"
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Três achados do "Modelo do relatório" de /ia (#ia-modelo), em versão curta para caber no cartão: a falha, em poucas
|
||||
* palavras, e a gravidade. Como lá, são exemplos ilustrativos: não são de nenhum cliente nem de um app real.
|
||||
@@ -209,28 +106,35 @@ const REPORT_EXCERPT = [
|
||||
|
||||
/** O mesmo tamanho nos dois selos: a gravidade fica no texto, não só na cor ou no enchimento. */
|
||||
const SEVERITY_BADGE =
|
||||
"inline-flex min-w-12 shrink-0 items-center justify-center border border-[var(--ink)] px-1.5 font-display text-[15px] leading-5";
|
||||
"inline-flex min-w-14 shrink-0 items-center justify-center border border-[var(--ink)] px-2 font-display text-[18px] @[29rem]:text-[15px] leading-6";
|
||||
|
||||
/**
|
||||
* Ilustração da revisão: um recorte do "Modelo do relatório" de /ia, com três linhas de achado, cada uma com o selo
|
||||
* da gravidade, e a legenda "Exemplo ilustrativo". O texto tem 15 px também aqui; os achados são curtos para caberem
|
||||
* numa linha só no cartão mais estreito (três colunas em 1024 px).
|
||||
* Recorte do "Modelo do relatório" de /ia junto do vídeo da revisão: três linhas de achado, cada uma com o selo da
|
||||
* gravidade, e a legenda "Exemplo ilustrativo", em 18 px. A peça 3D (a janela e a lupa) ocupa a metade esquerda do
|
||||
* quadro, então numa caixa larga (528 px ou mais) o recorte fica por cima da metade direita, centrado na vertical. Numa
|
||||
* caixa mais estreita (celular, e as duas colunas logo acima de 1024 px) ele não cabe ao lado da janela sem cobri-la:
|
||||
* desce para baixo do quadro, no mesmo fundo escuro. A largura vem da caixa (container query), não da tela.
|
||||
*/
|
||||
/*
|
||||
* Onde o trecho do relatório fica: ao lado da janela quando o quadro tem 29rem ou mais (a janela do vídeo acaba em 51%
|
||||
* da largura); embaixo do vídeo quando o cartão ocupa a linha inteira e é estreito (celular); fora da tela entre 1024
|
||||
* e 1279 px, em que os cartões já estão lado a lado mas o quadro é estreito demais e empilhar desalinharia os títulos.
|
||||
*/
|
||||
function ReviewArt() {
|
||||
return (
|
||||
<div
|
||||
role="img"
|
||||
aria-label="Exemplo ilustrativo de relatório da revisão: três achados, dois de gravidade alta e um de gravidade média"
|
||||
className="flex h-full flex-col items-center justify-center gap-sp-2 px-sp-3"
|
||||
className="pointer-events-none flex flex-col items-center gap-sp-2 px-sp-3 pt-sp-1 pb-sp-4 lg:max-xl:hidden @[29rem]:absolute @[29rem]:inset-y-0 @[29rem]:right-0 @[29rem]:items-end @[29rem]:justify-center @[29rem]:p-sp-4"
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
className="w-full max-w-[300px] divide-y divide-[var(--border)] bg-[var(--paper)] px-sp-2 py-0.5 text-[var(--ink)]"
|
||||
className="w-max max-w-full divide-y divide-[var(--border)] bg-[var(--paper)] px-sp-3 py-0.5 text-[var(--ink)]"
|
||||
>
|
||||
{REPORT_EXCERPT.map(({ finding, severity }) => (
|
||||
<li
|
||||
key={finding}
|
||||
className="flex items-center justify-between gap-sp-2 py-0.5 font-display text-[15px] leading-6"
|
||||
className="flex items-center justify-between gap-sp-4 py-1 font-display text-[18px] @[29rem]:text-[15px] leading-7"
|
||||
>
|
||||
<span className="min-w-0">{finding}</span>
|
||||
<span
|
||||
@@ -243,7 +147,7 @@ function ReviewArt() {
|
||||
</ul>
|
||||
<p
|
||||
aria-hidden="true"
|
||||
className="font-display text-[15px] leading-5 text-white/75"
|
||||
className="font-display text-[18px] @[29rem]:text-[15px] leading-6 text-white/80"
|
||||
>
|
||||
Exemplo ilustrativo
|
||||
</p>
|
||||
@@ -251,7 +155,11 @@ function ReviewArt() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Foto do case, quando há um; senão a ilustração própria da oferta. Sempre dentro da mesma caixa do mesmo tamanho. */
|
||||
/** Classes do quadro da mídia, iguais nos quatro cartões: a foto ou o vídeo ocupam o quadro inteiro. */
|
||||
const MEDIA_FRAME =
|
||||
"relative aspect-[16/10] md:aspect-[21/9] lg:aspect-[16/10]";
|
||||
|
||||
/** Foto do case, quando há um; senão o vídeo da oferta, com o que vai junto dele. O quadro tem o mesmo tamanho em todos. */
|
||||
function OfferArt({
|
||||
offer,
|
||||
extras,
|
||||
@@ -262,21 +170,40 @@ function OfferArt({
|
||||
|
||||
if (poster && extras.poster) {
|
||||
return (
|
||||
<Image
|
||||
src={poster.poster}
|
||||
alt={`Site ${extras.poster.title} aberto no computador`}
|
||||
width={poster.width}
|
||||
height={poster.height}
|
||||
sizes="(min-width: 1024px) 33vw, 100vw"
|
||||
loading="lazy"
|
||||
className="block h-full w-full object-cover object-top"
|
||||
/>
|
||||
<div className={MEDIA_FRAME}>
|
||||
<Image
|
||||
src={poster.poster}
|
||||
alt={`Site ${extras.poster.title} aberto no computador`}
|
||||
width={poster.width}
|
||||
height={poster.height}
|
||||
sizes="(min-width: 1024px) 50vw, 100vw"
|
||||
loading="lazy"
|
||||
className="block h-full w-full object-cover object-top"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return offer.id === "sistema" ? <SystemArt /> : <ReviewArt />;
|
||||
return (
|
||||
<>
|
||||
<div className={MEDIA_FRAME}>
|
||||
<OfferVideo name={offer.id} />
|
||||
{offer.id === "whatsapp" ? (
|
||||
// de que aplicativo se trata; decorativo como o vídeo, porque o título do cartão já diz WhatsApp
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute top-2 left-2 inline-flex items-center gap-sp-2 bg-black/70 px-sp-2 py-1 font-display text-[15px] leading-5 text-white @[33rem]:top-3 @[33rem]:left-3"
|
||||
>
|
||||
<WhatsAppMark className="h-4 w-4 text-[var(--whatsapp)]" />
|
||||
WhatsApp
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{offer.id === "revisao" ? <ReviewArt /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Um único estilo de cartão para as três ofertas: foto ou ilustração, título com altura mínima, texto, o que você recebe, botão e link do case. */
|
||||
/** Um único estilo de cartão para as quatro ofertas: foto ou vídeo, título com altura mínima, texto, o que você recebe, botão e link. */
|
||||
function OfferCard({
|
||||
offer,
|
||||
extras,
|
||||
@@ -285,7 +212,7 @@ function OfferCard({
|
||||
<article className="ds-card cv-press cv-lift relative h-full min-w-0 overflow-hidden">
|
||||
<div
|
||||
data-testid={`offer-art-${offer.id}`}
|
||||
className="aspect-[16/10] overflow-hidden border border-[var(--border)] bg-[var(--ink)] md:aspect-[21/9] lg:aspect-[16/10]"
|
||||
className="@container relative overflow-hidden border border-[var(--border)] bg-[var(--ink)]"
|
||||
>
|
||||
<OfferArt offer={offer} extras={extras} />
|
||||
</div>
|
||||
@@ -346,16 +273,22 @@ function OfferCard({
|
||||
}
|
||||
|
||||
export function WorkModalities() {
|
||||
const fiscal = serviceOffers.find(({ id }) => id === "fiscal");
|
||||
return (
|
||||
<Section
|
||||
id="modalities"
|
||||
eyebrow="Soluções"
|
||||
title="Escolha por onde começar"
|
||||
description="Três pontos de partida. Escolha o que mais se parece com o seu caso: o formulário de contato já abre com o tipo de projeto marcado."
|
||||
description="Quatro pontos de partida. Escolha o que mais se parece com o seu caso: o formulário de contato já abre com o tipo de projeto marcado."
|
||||
disableChildrenAnimation
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-sp-5 lg:grid-cols-3 lg:gap-sp-6">
|
||||
{serviceOffers.map((offer, index) => (
|
||||
{/* a faixa só existe até 31/03/2027: o servidor a deixa de fora depois disso, e FiscalNotice confere a data no navegador */}
|
||||
{fiscal && isFiscalNoticeActive(new Date()) ? (
|
||||
<FiscalNotice href={fiscal.href} />
|
||||
) : null}
|
||||
|
||||
<div className="grid grid-cols-1 gap-sp-5 lg:grid-cols-2 lg:gap-sp-6">
|
||||
{homeOffers.map(({ offer, extras }, index) => (
|
||||
<Reveal
|
||||
key={offer.id}
|
||||
y={20}
|
||||
@@ -363,12 +296,12 @@ export function WorkModalities() {
|
||||
delay={0.08 * (index + 1)}
|
||||
className="min-w-0"
|
||||
>
|
||||
<OfferCard offer={offer} extras={OFFER_EXTRAS[offer.id]} />
|
||||
<OfferCard offer={offer} extras={extras} />
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Uma vez só, depois dos três cartões: o primeiro passo é o mesmo para qualquer oferta (uma conversa e, depois dela, escopo e proposta por escrito, sem prazo nem preço); a página de cada oferta diz o que se entende na conversa. */}
|
||||
{/* Uma vez só, depois dos cartões: o primeiro passo é o mesmo para qualquer oferta (uma conversa e, depois dela, escopo e proposta por escrito, sem prazo nem preço); a página de cada oferta diz o que se entende na conversa. */}
|
||||
<div className="mt-sp-6 flex flex-col gap-sp-1 border-t border-[var(--border)] pt-sp-4 md:flex-row md:items-baseline md:gap-sp-4">
|
||||
<p className="font-display text-[15px] font-semibold leading-6 text-[var(--ink)]">
|
||||
Primeiro passo
|
||||
|
||||
Reference in new issue
Block a user