Componentes · superfície pública
Componentes da marca
Estes são os componentes que o cliente do Alemão vê: 39 exports em 18 arquivos, entre components/site-alemao/ (o site no ar) e components/site-cliente/ (um ícone vivo e onze peças de legado). O admin tem a linguagem dele, documentada em Primitivas & padrões. Aqui é a voz do Alemão — a que fala com quem tem o para-brisa trincado.
Este capítulo não tem cópias. Todo espécime abaixo é o componente real importado de components/site-alemao; os dados vêm de data.ts e as cores de tokens.ts. E os botões são de verdade: um clique aqui abre o WhatsApp da loja e dispara o evento GA4 — por isso cada espécime carrega origem="brand_doc_*", pra não contaminar o funil do site.
Duas famílias, uma pasta
Antes de usar qualquer coisa daqui: metade destes arquivos não é mais a linguagem da marca. Saber de qual metade você está importando é a primeira decisão.
| Família | Arquivos | Estado | Tokens |
|---|---|---|---|
| Site vivo | todo site-alemao/ + site-cliente/whatsapp-icon.tsx | Renderizado hoje em alemaoauto.com.br, em 13 páginas públicas. | CSS vars da marca (--az, --am, --tinta…) via TOKENS |
| Legado órfão | site-cliente/sections.tsx, site-premium.tsx, ab-toggle.tsx, fonts.ts | Nenhum import no repo. Preservado como referência histórica — o comentário de app/[slug]/page.tsx declara isso por escrito. | Hex hardcoded + props primaryColor/secondaryColor + .glass-* (namespace do admin) |
O legado não é uma variante da marca — é uma versão anterior dela. Ele viola duas leis: usa itálico nas headlines e usa o azul errado (#1F3D8E em vez de #1137A2). Está catalogado no fim desta página para que ninguém o adote por engano ao procurar "como o site do Alemão faz X".
Fundação — os três módulos que não são componente
Não renderizam nada, mas todo componente desta página depende deles. Dado, cor e caminho nascem aqui — nunca dentro de um componente.
| Módulo | Tipo | Exporta | Contrato |
|---|---|---|---|
tokens.ts | Isomórfico | TOKENS (11 CSS vars), PALETA, type Cor | Fonte única da paleta. Usa satisfies e não anotação Record<string,string>: com Record, TOKENS["--az"] viraria string | undefined sob noUncheckedIndexedAccess. Cor nova nasce aqui. |
data.ts | Isomórfico | MARCA, CONTATO, linkWhatsApp(), MSG_PADRAO, LINK_COMO_CHEGAR, LINK_MAPS_EMBED, SERVICOS, PASSOS, PILARES, LANDINGS, MODELOS_FREQUENTES, DIFERENCIAIS, DEPOIMENTOS, ROTAS, FAQ, type Servico | Telefone, endereço, horário, 8 serviços e 7 landings. As listas fechadas usam as const (tupla), o que torna DEPOIMENTOS[2].texto seguro sob noUncheckedIndexedAccess. Nada de placeholder entra aqui. |
base.ts | Server-only (headers()) | basePublica(slug) | Devolve "" no domínio próprio e /{slug} no host compartilhado/dev. Todo link interno do site passa por ele — é o que faz alemaoauto.com.br/servicos e /alemao/servicos serem a mesma página. |
Conversão — os três componentes que fazem o telefone tocar
São os únicos componentes do site com evento de medição embutido. Nenhum outro dispara GA4, e nenhum CTA do site é escrito fora deles.
o chip de vidro do hero — caso canônico do .vidro-cristal
O chip é o único uso legítimo de ! nesta família: os !border derrubam o border-2 da variante contorno pra deixar a borda de 1px do material valer. E note o fundo — vidro só sobre azul, gradiente, foto ou padrão (ver Materiais de vidro).
LinkTelefone sobre fundo rico
LinkComoChegar sobre branco
Os dois herdam a cor do container e não carregam token de cor próprio — quem chama diz a cor pelo className. Sobre azul: text-white. Sobre branco: text-[--az]. É a regra do amarelo aplicada por omissão: nunca text-[--am] num fundo claro.
| Componente | Props | Evento GA4 | Notas |
|---|---|---|---|
BotaoWhatsApp | mensagem? = MSG_PADRAO · rotulo? = "Pedir orçamento no WhatsApp" · origem obrigatória · variante? = "amarelo" | "contorno" · className? = "" | whatsapp_click | <a> para linkWhatsApp(mensagem) com target="_blank" rel="noopener noreferrer". min-h-[48px], rounded-full, font-extrabold, carrega .brilho. contorno só faz sentido sobre fundo escuro (é text-white fixo). |
LinkTelefone | origem obrigatória · className? = "" | phone_click | Ícone Phone 18px + CONTATO.telefone. Nunca digite o número: ele vem de data.ts ((11) 3691-4754). min-h-[44px]. |
LinkComoChegar | className? = "" — sem origem | maps_click | Rótulo fixo "Como chegar", href LINK_COMO_CHEGAR. A origem é hardcoded como "como_chegar": todo clique — do hero, do contato, do cartão do mapa — cai no mesmo balde. Assimetria conhecida da API. |
A convenção de origem
Um CTA sem origem única é um CTA que não existe no relatório. A prop é obrigatória em dois dos três componentes justamente por isso — e o nome tem gramática.
| Padrão | Origens reais no site | Onde |
|---|---|---|
| Posição na home | hero · localizacao · cta_final | home.tsx — a posição na página, não o rótulo do botão |
| Chip do hero | hero_chip_parabrisa · hero_chip_lateral · hero_chip_outro | home.tsx — o sufixo é a intenção declarada pelo visitante |
| Página de rota | contato_topo · servicos_meio · sobre | contato, servicos, sobre — rota + posição |
servico_<slug> | um por serviço, derivado de SERVICOS | servicos/page.tsx — o card de cada serviço na listagem |
pg_<slug> | pg_para-brisas, pg_vigias… (8 slugs) | servicos/[servico]/page.tsx — o CTA do topo da página do serviço |
faq_<slug> | faq_para-brisas, faq_vigias… | servicos/[servico]/page.tsx — o CTA depois do FAQ, na mesma página |
lp_<slug> | lp_auto-vidros-osasco · lp_vidro-eletrico-nao-sobe · lp_conserto_maquina_de_vidro · lp_reparo-de-para-brisa-trincado · lp_troca-de-para-brisa-osasco · lp_vidro-de-porta-e-vigia-traseiro | as 7 landings — uma constante no topo do arquivo (ORIGEM/GA/SLUG_GA), nunca string repetida |
lp_<slug>_<bloco> | _final · _faq · _local · _preco · _avaliacao · _urgencia · _agora · _resolve · _passos · _lei · _corsa · _conserto_troca | cada CTA repetido dentro da mesma landing ganha o sufixo do bloco — é assim que se sabe qual argumento converteu |
| Casca (fixas no componente) | topo · header · header_mobile · menu_mobile · flutuante · como_chegar · orcamento | header.tsx, whatsapp-flutuante.tsx, ui.tsx, form-orcamento.tsx — não recebem prop, a origem é do componente |
A regra em uma frase: a origem descreve de onde a pessoa clicou, não o que o botão diz. Dois CTAs na mesma página nunca compartilham origem — se compartilharem, o relatório soma dois argumentos diferentes e você perde a única informação que importa. Landing usa constante no topo do arquivo + interpolação; página de serviço deriva do slug. Nunca digite a mesma string duas vezes.
| Evento | Disparado por | Params |
|---|---|---|
whatsapp_click | BotaoWhatsApp, SiteHeader (4×), WhatsAppFlutuante | { origem } |
phone_click | LinkTelefone, SiteHeader (2×) | { origem } |
maps_click | LinkComoChegar | { origem: "como_chegar" } |
form_submit | FormOrcamento | { origem: "orcamento" } |
page_view | Ga4 → PageviewsSpa (interno) | { page_path, page_title } |
Identidade em uso
As peças do key visual que viraram componente. Cada uma lê os dados da marca sozinha — você nunca passa o ano de fundação nem o nome da cidade por prop.
SeloOrigem — pílula bicolor. Lê MARCA.desde, MARCA.cidade, MARCA.uf. Sem variantes: é sempre amarelo+azul.
o mesmo selo sobre fundo rico — ele é opaco, funciona nos dois
- Qualidade
- Agilidade
- Confiança
tom="claro" — ícone --az, rótulo --tinta, divisor --linha
- Qualidade
- Agilidade
- Confiança
tom="escuro" — ícone --am, rótulo branco, divisor border-white/25

Emblema é uma imagem, não SVG: /brand/alemao-emblema-circular.png a 800×800, num wrapper max-w-[520px] select-none. A reconstrução em código (anel + textPath + estrelas + filetes + símbolo + mascote + logo + 2 cartões) foi tentada duas vezes e quebrou nas duas — uma imagem única não desmonta. O style={{ maxWidth: "100%", height: "auto" }} inline é proposital: é a única guarda válida antes de o CSS carregar; sem ela a imagem renderiza nos 800px naturais e estoura a largura no celular. O alt transcreve por extenso o que está cravado no pixel, porque aqueles textos não são HTML.
Pendência na arte, não no componente: o telefone impresso no peito do mascote é fictício — (11) 3682-1998 em vez do real (11) 3691-4754. É resíduo da geração por IA, ilegível no tamanho renderizado, mas presente no asset em produção. A arte precisa ser regerada; o componente está certo.
SELO_STICKER aplicado ao mascote
- Para-brisas
- Vigias (vidro traseiro)
- Vidros laterais
- Vidros de porta
- Borrachas
- Conserto de fechaduras
- Recondicionamento de máquinas de vidro
- Acessórios
IconeServico — os 8 slugs de data.ts resolvidos pelo mapa
sobre branco — herda --az
sobre azul — herda --am
dentro do botão amarelo
WhatsAppIcon em currentColor: a cor nunca é do ícone, é do container. Nos três casos é a mesma regra do amarelo — sobre branco, azul; sobre azul, amarelo; sobre amarelo, azul.
| Símbolo | Tipo | Props | Contrato |
|---|---|---|---|
SELO_STICKER | const string (não é componente) | — | O recorte adesivo do mascote: contorno branco de 3px nos quatro lados + drop-shadow(0 18px 22px rgba(4,16,44,0.4)). Aplica-se por className na <img>. Branco e o rgba são literais dentro do filtro porque CSS var não é interpolável em arbitrary value do Tailwind. O contorno é obrigatório, não opcional. |
SeloOrigem | Client | className? = "" | "★ Desde 1998 | Osasco · SP". font-extrabold uppercase tracking-[0.14em] a 12px. Abre a peça, acima da headline. |
Pilares | Client | tom obrigatória: "claro" | "escuro" — sem default | Trio Qualidade · Agilidade · Confiança em <ul role="list">, ícones ShieldCheck/Gauge/Handshake (strokeWidth={1.8}, aria-hidden). Sem default de propósito: o tom errado num fundo errado é ilegível, então quem chama tem que decidir. Tracking responsivo 0.08em → sm:0.14em. |
IconeServico | Client | slug (obrigatória) · className? — sem default | Mapa slug → ícone lucide com fallback Car: para-brisas→CarFront · vigias→GalleryHorizontalEnd · vidros-laterais→Car · vidros-de-porta→DoorOpen · borrachas-e-frisos→Sparkles · maquinas-de-vidro→Cog · fechaduras→KeyRound · acessorios→Zap. Emite sempre strokeWidth={1.8} e aria-hidden; a cor herda do container. |
Emblema | Server | className? = "" | <img> 800×800 em wrapper max-w-[520px]. Peça única e indivisível. Não redesenhe — importe. |
WhatsAppIcon | Server (SVG puro) | className? — sem default | O glifo oficial da marca WhatsApp, viewBox="0 0 24 24", fill="currentColor", aria-hidden fixo no componente. Existe porque os CTAs usavam MessageCircle do lucide, que é um balão genérico e não a marca. Nunca substitua por ícone genérico. |
WhatsAppIcon mora do lado errado. É o único módulo de site-cliente/ consumido pelo site vivo — dependência de ui.tsx, header.tsx, footer.tsx, home.tsx, form-orcamento.tsx, whatsapp-flutuante.tsx, contato/page.tsx e not-found.tsx. Qualquer limpeza da pasta de legado quebra o site em produção se não migrar ele antes (para site-alemao/ ou um components/icones/).
Movimento — seis utilitários, uma regra
Todos respeitam prefers-reduced-motion, e cada um respeita de um jeito diferente. Saber qual mecanismo é qual é o que impede de quebrar o mais frágil deles.
Contador ate={1998} — anima ao entrar na viewport, uma vez só
Bloco dentro de Reveal
Acima da dobra ele nasce visível — é por isso que você o está vendo sem rolar
- Para-brisas
- Vigias (vidro traseiro)
- Vidros laterais
- Vidros de porta
- Borrachas
- Conserto de fechaduras
- Recondicionamento de máquinas de vidro
- Acessórios
Marquee — aqui aparece parado: a animação @keyframes alemao-marquee (28s linear infinite) mora no <style> do SiteShell, que este capítulo não carrega. Renderiza o mesmo bloco duas vezes — o primeiro com role="list" (lido), o segundo com aria-hidden só pra fechar o loop visual.
| Componente | Props | Reduced motion — o mecanismo |
|---|---|---|
Reveal | children · atraso? = 0 (ms) · className? = "" | Máquina de 3 estados "ssr" | "oculto" | "visivel": no SSR nasce visível — sem JS nada some. Depois de montado, se prefers-reduced-motion → visível e para. Se o topo já está a menos de innerHeight * 0.95 → visível (nunca esconde above-the-fold). Só então vira oculto e observa. rootMargin: "100000px 0px -8% 0px" — teto gigante de propósito, pra que um elemento que já passou pra cima da viewport (salto de scroll, âncora, "voltar") conte como visível. Também motion-reduce:transition-none. |
Marquee | itens: readonly string[] | A guarda é CSS, no shell: o bloco @media (prefers-reduced-motion: reduce) zera .marquee-trilho { animation: none; transform: none }. Pausa também em .marquee:hover e :focus-within. |
ParallaxY | fator? = 0.12 · className? = "" · children? opcional | matchMedia retorna cedo e nenhum listener é registrado — não é "anima e ignora", é não existir. Escreve el.style.transform direto (não via state), com requestAnimationFrame coalescido e listener { passive: true }. O wrapper é sempre aria-hidden: é decoração por contrato, não serve pra conteúdo. |
SegueMouse | intensidade? = 10 · className? = "" · children obrigatório | Retorna cedo em prefers-reduced-motion e também em (pointer: coarse) — é efeito de desktop, nunca roda no toque. Desloca x = (clientX/innerWidth − 0.5) × intensidade, y a 60% do X de propósito. Ao contrário do ParallaxY, não é aria-hidden: carrega conteúdo (o balão de fala do mascote). |
Contador | ate: number · className? = "" | Retorna antes de criar o observer, o valor fica parado no final. O useState(ate) nasce já no valor final: sem JS e no SSR o número correto aparece. Curva 1 − (1−t)³ em 1200ms partindo de max(0, ate − 60), roda uma vez (flag + disconnect()). tabular-nums evita salto de largura. |
TituloRotativo | frases: readonly { alvo, destaque }[] · intervalo? = 3600 (ms) | Não instala o setInterval — a primeira frase fica e nunca troca. Também motion-reduce:transition-none no crossfade. |
Os quatro mecanismos, e por que a distinção importa: (1) render estático — Reveal/Contador nascem no estado final, então sem JS o conteúdo está lá; (2) listener não registrado — ParallaxY/SegueMouse/TituloRotativo não gastam nada; (3) CSS no shell — Marquee e as classes .brilho, .entra-subindo, .entra-diagonal, .facho-vidro, .scroll-cue; (4) motion-reduce: do Tailwind nas transições de hover/active. Componente novo de motion escolhe um desses quatro — não invente um quinto.
TituloRotativo — o contrato que não pode ser esquecido
É o único componente do site cujo uso errado apaga o h1 da página. Ele é aria-hidden por inteiro: sem um irmão sr-only, o leitor de tela e o Google encontram um título vazio.
Vidro novo, segurança sempre.
Espécime ao vivo: as frases trocam a cada 3,6s. Todas vivem na mesma célula do grid (col-start-1 row-start-1) só pra reservar a altura da maior — o hero não pula. As inativas recebem invisible (visibility: hidden), não só opacity-0: nunca são pintadas, então é impossível duas se sobreporem. A troca é sai-antes-de-entrar: setVisivel(false) → 300ms → troca índice → setVisivel(true).
O <span> inteiro é aria-hidden. Um texto que muda sozinho a cada 3,6s é ruído para leitor de tela e conteúdo instável para indexação — por isso ele se esconde. A consequência é um contrato: todo <TituloRotativo> exige um <span className="sr-only"> irmão com o texto real, antes dele, dentro do mesmo <h1>. Sem isso o h1 da home fica sem conteúdo acessível — e sem headline pro SEO.
| Como fazer | |
|---|---|
| ✓ Certo | <h1 className="..."><span className="sr-only">Vidro novo, segurança sempre.</span><TituloRotativo frases={FRASES} /></h1> |
| ✕ Errado | <h1 className="..."><TituloRotativo frases={FRASES} /></h1> |
É exatamente assim em home.tsx: o sr-only traz "Vidro novo, segurança sempre." — a headline oficial de MARCA.headline, não uma paráfrase. Se as frases rotativas mudarem, o sr-only continua sendo a headline da marca.
A casca — quatro componentes que você nunca chama duas vezes
SiteShell é a raiz de todas as 13 páginas públicas; header, footer e o botão flutuante só existem dentro dele. Nenhum deles é usado solto.
| Componente | Tipo | Props | O que faz e o que não dá pra deduzir |
|---|---|---|---|
SiteShell | Server | base: string · children | Cinco coisas: (1) aplica Plus_Jakarta_Sans (pesos 400–800, display: "swap"); (2) injeta TOKENS como style inline — é aqui que --az, --am etc. passam a existir no DOM; (3) skip-link "Pular para o conteúdo" → #conteudo; (4) monta SiteHeader → <main id="conteudo"> → SiteFooter → WhatsAppFlutuante → Ga4; (5) injeta o <style> com as animações da marca. O <style> usa dangerouslySetInnerHTML e não template literal como filho: como texto filho, o React escapava as aspas de content: "" diferente no servidor (") e no cliente, o que quebrava a hidratação da página inteira. Anel de foco global: outline-[3px] outline-offset-2 outline-[--az-vivo] + box-shadow: 0 0 0 2px #fff. |
SiteHeader | Client | base: string | Duas faixas: contato --az-deep de 44px (telefone, horário em md:, WhatsApp) e nav branca bg-white/90 backdrop-blur-lg com logo, ROTAS, CTA amarelo e menu mobile. sticky -top-11 — o top negativo faz a faixa de contato rolar pra fora enquanto a nav gruda. Estados: scrollY > 24 encolhe a nav de h-[72px] para h-[60px] (motion-reduce:transition-none); item ativo bg-[--az-soft] text-[--az] + aria-current="page". O componente mais acessível do site: painel mobile role="dialog" aria-modal="true", Esc fecha, foco preso (Tab/Shift+Tab com wrap, implementação manual), document.body.style.overflow = "hidden" restaurado no cleanup, foco devolvido ao botão que abriu, aria-expanded + aria-controls="menu-mobile", fecha ao mudar de rota, todo alvo com min-h-[44px]/size-11. |
SiteFooter | Server | base: string | Rodapé --az-deep com o padrão gráfico em background-image (/brand/alemao-pattern.svg, 420px auto, repeat), filete amarelo no topo e 4 blocos: marca (pastilha + assinatura + Facebook/Instagram em SVG inline), <nav aria-label="Mapa do site"> com ROTAS + os 6 primeiros SERVICOS, <address className="not-italic"> com contato, e <nav aria-label="Mais procurados"> com as LANDINGS. É server de propósito: link instrumentado vive no client, aqui são âncoras simples. O not-italic é a guarda contra o itálico padrão do elemento — coerente com a lei da marca. As landings ficam aqui e não em ROTAS: seis itens a mais no menu competiriam com as rotas que convertem. |
WhatsAppFlutuante | Client | nenhuma | FAB verde de 56px (size-14) no canto inferior direito. Retorna null até scrollY > 420; depois de aparecer o listener é removido — aparece uma vez e fica. @keyframes alemao-poof (420ms, overshoot em 70%) pulsa uma vez, nunca em loop, e a media query do <style> local zera .zap-flutuante sob reduced motion. aria-label + aria-describedby="tooltip-zap", tooltip role="tooltip" só em md: e em group-hover/group-focus-within. Posição respeita o notch: bottom-[max(1.25rem,env(safe-area-inset-bottom))]. Origem GA fixa: flutuante. |
Por que o vidro não aparece na casca: a nav é bg-white/90 backdrop-blur-lg sobre conteúdo claro — vidro ali reprovaria contraste. O rodapé é --az-deep chapado com padrão: fundo rico, mas os cartões dentro dele são brancos com alfa, não material de vidro. Os três usos de vidro do site inteiro estão em home.tsx, todos sobre fundo rico.
AlemaoHome — a composição de referência
Não é um componente reutilizável: é a home inteira, e é o exemplo canônico de como as peças acima se montam. Quando houver dúvida sobre uma regra, é aqui que se olha.
| Seção | O que a torna exemplar |
|---|---|
| 1 · Hero | aria-labelledby="hero-titulo", min-h-[calc(100svh-116px)], fundo com o gradiente radial do hero; padrão em ParallaxY fator={0.1}; .facho-vidro; duas .entra-diagonal; marca-d'água do símbolo a 6%. Ordem: SeloOrigem → h1 com sr-only + TituloRotativo → BotaoWhatsApp + link → 3 chips .vidro-cristal → LinkTelefone → Pilares tom="escuro"; mascote (md:hidden no fluxo, SegueMouse intensidade={14} no desktop) com balão .vidro-fosco; fecha com Marquee de 7 itens. |
| 2 · Números | aria-label="A Alemão em números", 4 itens com border-l-4 border-[--am], cada um em Reveal atraso={i*80}. Só o item "1998" usa Contador ate={1998} — contador em número que não é ano soaria a métrica inventada. |
| 3 · Serviços | Coluna esquerda md:sticky md:top-28 com o card destaque Para-brisas (gradiente 150° + padrão + faixa -skew-x-[14deg] + .brilho) e <ol role="list"> numerada dos demais com IconeServico, barra amarela crescendo no hover (scale-y-0 → group-hover:scale-y-100). |
| 4 · Como funciona | Fundo linear-gradient(150deg, --az-vivo, --az 55%, --az-deep) + padrão + diagonal -skew-x-[14deg] bg-[--am]/15; PASSOS com numeral gigante text-[96px] text-[--am]/25 atrás. |
| 5 · Diferenciais | Emblema + DIFERENCIAIS com ícones em disco amarelo; ordem invertida no mobile (order-last md:order-first). Constante local ICONES_DIFERENCIAIS pareia 6 ícones por índice, com fallback BadgeCheck. |
| 6 · Depoimentos | id="avaliacoes" scroll-mt-24 bg-[--gelo]; quote hero em <figure>/<blockquote>/<figcaption> + 2 cards brancos sólidos — sobre gelo não vai vidro, e o código registra a decisão. |
| 7 · Localização | <iframe> do Google Maps full-bleed com title e loading="lazy"; cartão flutuante .vidro-fosco .vidro-reflexo — fosco e não fumê porque o mapa é claro (fumê sobre claro dá ~2,8:1, reprova AA) e porque o iframe é cross-origin, então o backdrop-filter pode nem amostrar o mapa. |
| 8 · CTA final | Radial invertido (foco em 20% 10%), diagonal -skew-x-[14deg], mascote + balão .vidro-fosco — fosco e não laminado porque é fala: legibilidade antes do efeito. |
Props: base: string, e nada mais — é server, toda a interatividade está nos filhos client. As quatro diagonais usam -skew-x-[14deg], coerentes com .entra-diagonal (skewX(-14deg)) do shell. Toda <section> tem aria-labelledby ou aria-label; toda lista tem role="list"; todo ícone decorativo tem aria-hidden. Escala fluida: clamp(2.6rem,7.5vw,5.2rem) no H1, clamp(2rem,4.5vw,3.2rem) nos H2.
FormOrcamento — o único formulário da marca
Cinco campos que não enviam nada pra servidor nosso: montam a mensagem e abrem o WhatsApp. É o padrão de formulário público — e o único lugar do site com estado de erro.
| Aspecto | Contrato |
|---|---|
| Props | Nenhuma. |
| Campos | nome (obrigatório) · telefone (opcional, type="tel" inputMode="tel") · carro (obrigatório) · servico (<select> populado por SERVICOS, opção vazia "Ainda não sei / outro") · mensagem (textarea rows={4}, resize-y) |
| Erro | noValidate com validação própria no submit. Por campo: aria-invalid + aria-describedby apontando pro <p id="…-erro">, e o foco vai pro primeiro campo inválido. |
| Sucesso | Substitui o form por um card role="status" com CheckCircle2 e um <h2 tabIndex={-1}> que recebe o foco via useEffect. Traz link de fallback ("se a janela não abriu, clique aqui") usando mensagemEnviada guardada em state — pro caso de popup bloqueado no iOS. O botão "Preencher outro orçamento" preserva nome/telefone/carro e limpa só servico e mensagem. |
| Campo (classe) | rounded-xl border border-[--linha] bg-white px-4 py-3 text-[15px] text-[--tinta] focus:border-[--az] |
| Dívida conhecida | O botão de envio é o BotaoWhatsApp amarelo reescrito à mão com min-h-[52px]. E há quatro hex fora de token: #15803D (sucesso), #B42318 (erro) e placeholder:text-[#5B6880] — que é o valor exato de --aco, escrito em hex. |
Lacuna real do DS: a paleta da marca não tem cor de estado. O produto tem (--warning, --danger); a marca não. É por isso que #15803D e #B42318 estão cravados neste arquivo — não havia token pra usar. Até existir, é o único formulário público, e o hex fica documentado aqui em vez de esquecido lá.
Medição e SEO
Sete exports que não desenham nada. Dois deles decidem se o site é medido; os outros cinco decidem como o Google o entende.
| Export | Arquivo | Tipo | Contrato |
|---|---|---|---|
Ga4 | ga.tsx | Client · sem props | Carrega o GA4 só se NEXT_PUBLIC_GA4_ID existir; sem a env retorna null e nenhum script de medição entra na página. Dois <Script strategy="afterInteractive"> + gtag('config', ID, { anonymize_ip: true }). Só eventos de interação — nenhum dado pessoal. Renderizado pelo SiteShell, nunca à mão. |
GA4_ID | ga.tsx | const string | undefined | O interruptor de toda a medição. Não é componente. |
track() | ga.tsx | (evento: string, params?: Record<string,string>) | A função que todos os componentes de conversão chamam. Silenciosa se window.gtag não existir. Eventos vivos: whatsapp_click, phone_click, maps_click, form_submit, page_view. Evento novo se registra na tabela acima. |
PageviewsSpa | ga.tsx | Interno · não exportado | Dispara page_view nas trocas de rota do App Router via usePathname, pulando a primeira renderização (useRef(true)) pra não duplicar o que o config já mandou. |
JsonLd | seo.tsx | Server · { dados: object } | <script type="application/ld+json"> com JSON.stringify(dados).replace(/</g, "\u003c") — o escape de < é a guarda contra quebra de tag. Nunca monte a tag à mão. |
metaSite() | seo.tsx | { titulo, descricao, caminho } → Metadata | Canonical sempre no domínio oficial (alemaoauto.com.br); favicon /brand/alemao-mark.svg (o símbolo sem placa, pra substituir o ícone amarelo do PWA só nas páginas públicas); OG /site-cliente/alemao/og.jpg 1200×630 (gerado por scripts/gerar-og.mjs); verificação do Search Console condicional a NEXT_PUBLIC_GSC_VERIFICATION — atrás de env porque o token é por propriedade. |
jsonLdNegocio() | seo.tsx | Server · sem args | @type: "AutoRepair", foundingDate de MARCA.desde, horários Seg–Sex 08–17 e Sáb 08–12, sameAs com Facebook/Instagram. ⚠️ O telefone +551136914754 está hardcoded aqui em vez de vir de CONTATO — é o número certo, mas é uma segunda fonte de verdade. |
jsonLdBreadcrumb() | seo.tsx | (itens: { nome, caminho }[]) | Trilha de navegação para as páginas internas de serviço e landing. |
Legado órfão — os onze que não são a marca
Documentados porque existem no repositório e porque alguém vai encontrá-los procurando exemplo. Nenhum arquivo do repo os importa. Não são componentes vivos do DS — são a versão anterior da marca.
Leia isto antes de copiar uma linha do que vem abaixo. Confirmado por grep em app/, components/ e lib/: nada importa site-cliente/sections, site-cliente/site-premium ou site-cliente/ab-toggle. Eles usam .glass-* (namespace do admin) numa superfície de marca, a escala tipográfica do produto (text-tiny, text-body-lg) com font-semibold capado em 600 — enquanto a marca é font-extrabold (800) — e, em site-premium.tsx, itálico em pelo menos 5 headlines mais o azul errado #1F3D8E em ~40 lugares. O comentário de cabeçalho daquele arquivo documenta "headline itálica em caixa alta" como se fosse regra: é veneno documental, e é a única fonte no repo que ainda afirma isso.
| Export | Props | O que é · por que não usar |
|---|---|---|
SitePremium | { brand: BrandSite } — a interface é exportada | A página inteira num arquivo (TopBar → Header → Hero → … → FloatingWhats), tudo em funções privadas. Três hex no topo em vez de tokens (AZUL = "#1F3D8E"). Fonte righteous. Duplica Reveal, SeloOrigem e Pilares como funções privadas. Viola duas leis: itálico e azul errado. |
NavCliente | 9 props de BrandProps | Top bar bg-[#07173f] que colapsa ao rolar além de 50px + header motion.header que vira glass-strong. Fallback wa.me/5511999999999 — WhatsApp fake, já catalogado em Pendências. |
HeroCliente | BrandProps inteiro (interface local, não exportada) | Hero de 92vh com framer-motion (useScroll + useTransform), 3 selos Trust e CarroIlustracao (carrossel de 6 fotos com placas fictícias). Reduced motion é indireto: MotionConfig reducedMotion="user" no app/layout.tsx cobre os motion.*. Mesmo fallback de WhatsApp fake. |
Servicos | { primaryColor, secondaryColor } | 5 cards de um array local — não bate com os 8 de data.ts, e sem slug. Card destaque glass-strong, os demais glass-shine. |
Sobre | { nome, primaryColor, secondaryColor } | Duas colunas com número grande sobre card glass-shine colorido. motion.div + whileInView, easing [0.16, 1, 0.3, 1]. |
Marcas | { primaryColor } | Faixa com 7 fabricantes em array local, fonte oswald. site-premium.tsx tem a mesma lista com 6 — duas listas, nenhuma fonte única. |
Depoimento | { primaryColor, secondaryColor } | Um único depoimento com o texto interno ao componente — não vem de DEPOIMENTOS (que tem 3, com nome real de avaliação do Google). |
Localizacao | 10 props de BrandProps | Mapa do OpenStreetMap (bbox ±0.0035 lat / ±0.0055 lng), com um comentário afirmando que o Google bloqueou o embed keyless via X-Frame-Options. O site vivo usa iframe do Google (LINK_MAPS_EMBED) e funciona em produção — duas soluções opostas, uma delas baseada num diagnóstico que a produção contradiz. Traz um TODO: confirmar horário de sábado/domingo e horários default embutidos. |
CtaFinal | 4 props de BrandProps | Faixa linear-gradient(135deg, primaryColor, primaryColor+"E0") com GlassTexture opacity={0.05} e CTA h-16 px-10. |
FooterCliente | { slug, nome, logoUrl, facebook, instagram } | Rodapé bg-gray-900 em 3 colunas, logo por prop e ícones sociais do lucide. Quebra o white-label: recebe nome por prop mas escreve "vidro automotivo em Osasco desde 1998" cravado no JSX. |
AbToggle | { current: "a" | "b" | "c" } | Pílula flutuante de teste A/B/C. O próprio comentário manda remover "quando o cliente escolher a definitiva" — o cliente escolheu, o site-alemao está em produção, e o toggle continua no repo. Usa neutral-* do Tailwind, nenhum token da marca. |
fonts.ts | — (módulo) | montserrat, oswald, righteous. Nenhuma é fonte de marca hoje — o site vivo é Plus Jakarta Sans. Único consumidor vivo: app/[slug]/apresentacoes/deck-plr.tsx. |
Lacunas — o que este DS ainda não nomeia
Não são bugs: são componentes e tokens que faltam. Cada linha aqui é um lugar onde alguém teve que escrever à mão o que devia ser importado.
| Lacuna | Onde aparece | O que falta |
|---|---|---|
| Não existe primitiva de botão | A classe amarela (bg-[--am] + shadow-[0_16px_36px_-10px_rgba(245,200,0,0.6)] + hover:bg-[--am-claro] + active:scale-[0.98]) está copiada em três lugares: BotaoWhatsApp (min-h-[48px]), FormOrcamento (min-h-[52px]) e o CTA "Começar orçamento" de home.tsx (min-h-[52px]). | Um <Botao> primitivo da marca, com BotaoWhatsApp por cima dele. Os min-h divergentes são a prova de que a cópia já derivou. |
| Marca não tem cor de estado | #15803D e #B42318 em form-orcamento.tsx | Tokens de sucesso e erro em tokens.ts. O produto tem --warning/--danger; a marca não. |
| Marca não tem token de sombra | rgba(7,27,69,0.35) (selo), rgba(4,16,44,0.4) (sticker), rgba(11,42,126,0.6) (backdrop do menu), rgba(16,32,63,0.18) (emblema — a única que é --tinta com alfa) | Três azuis-sombra sem nome. O produto tem --shadow-card etc.; a superfície de marca não tem nenhum. |
| Quarto azul claro sem nome | #F0F4FD no hover da nav (header.tsx, desktop e mobile) | Não é --az-soft (#E9EFFC) nem --gelo (#F3F6FB). Ou vira token, ou vira um dos dois existentes. |
| Cinza fora da paleta | text-[#8A93A6] nos numerais da lista de serviços (home.tsx); placeholder:text-[#5B6880] em form-orcamento.tsx | O segundo é o valor exato de --aco escrito em hex — troca direta. O primeiro é um cinza que a paleta não tem. |
PILARES duplicado | data.ts exporta PILARES (chave/rotulo); ui.tsx mantém PILARES_UI local (rotulo/Icone) | Nenhum dos dois é fonte única — o ícone só existe no local. O mapa devia morar junto do dado, como IconeServico faz. |
Marquee rotula uma div | aria-label="Serviços" num elemento sem role de região | O label não é exposto. Precisa de role="region" ou virar <section>. |
Assimetria ParallaxY × SegueMouse | children opcional em um, obrigatório no outro | Dois utilitários irmãos com contrato diferente. Idem Reveal.atraso vs o delay do Reveal do legado — mesma coisa, dois nomes. |
Reveal vivo não tem fail-safe | O Reveal do legado tem setTimeout de 900ms que revela mesmo se o observer não disparar, e a prop imediato (boa pra LCP) | Cada um resolveu metade: o vivo tem a guarda de above-the-fold que o legado não tem; o legado tem o fail-safe que o vivo não tem. A confirmar se vale portar. |
Fica de olho
O que mantém a marca inteira — e o que a quebra.
Sempre
- Importar de components/site-alemao — nunca de site-cliente (só o WhatsAppIcon)
- origem única em todo CTA, seguindo a gramática: de onde clicou, não o que o botão diz
- Todo TituloRotativo com um <span className="sr-only"> irmão antes dele, dentro do h1
- Todo componente novo de motion escolhendo um dos quatro mecanismos de reduced motion já usados
- Telefone, endereço, horário, serviços e landings sempre de data.ts
- Pilares com tom explícito — o tom errado num fundo errado é ilegível
- WhatsAppIcon nos CTAs, nunca MessageCircle do lucide
- Vidro só sobre azul, gradiente, foto ou padrão — sobre gelo, card sólido
Nunca
- Copiar markup ou hex de site-premium.tsx / sections.tsx — é a marca anterior, com itálico e azul errado
- Reescrever o botão amarelo à mão — já são três cópias divergindo no min-h
- Reutilizar a mesma origem em dois CTAs da mesma página — o relatório soma e você perde o argumento
- Usar .glass-* (namespace do admin) em peça de marca, ou text-tiny/text-body-lg do produto
- TituloRotativo sozinho no h1 — o título fica vazio pro leitor de tela e pro SEO
- Redesenhar o Emblema em SVG — já quebrou duas vezes; é <Emblema /> e ponto
- Chamar SiteHeader, SiteFooter, WhatsAppFlutuante ou Ga4 fora do SiteShell
- Renderizar espécime do site sem TOKENS no style — as vars não existem fora do SiteShell
Recursos
Use sempre os arquivos oficiais — nunca redesenhe.
- Materiais de vidroquando o vidro pode e quando é card sólido
- Motionas animações do SiteShell, em detalhe
- Primitivas & padrõesa outra superfície — os componentes do admin
- Key Visuala composição-assinatura montada com estas peças
- Iconografiao mapa de ícones e as regras de traço
- Pendênciasas dívidas catalogadas, incluindo as desta página
