feat(site): blocos 3D da marca, novas ofertas e hero mais abrangente
CI / audit (push) Successful in 10m38s
CI / verify (push) Successful in 13m10s
CI / deploy (push) Successful in 1m50s
CI / e2e (push) Successful in 18m48s

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:
evandro committed 2026-11-10 03:48:25 -03:00
1 parent 4671837c44
commit c7d09d63d4
165 files changed
+7940 -3790

No files matched your search

+1 -1
View File
@@ -126,7 +126,7 @@ describe("CasePage: caminhos que o teste principal não cobre", () => {
expect(screen.queryByText("Serviços ligados a este projeto")).toBeNull();
// o NutriScan tem "Como ficou", mas o botão de contato é um só, o do fecho (sem dois seguidos)
const contacts = screen.getAllByRole("link", {
name: "Falar sobre meu projeto",
name: "Chamar no WhatsApp",
});
expect(contacts).toHaveLength(1);
expect(contacts[0]).toBeVisible();
+10 -10
View File
@@ -62,8 +62,8 @@ function readJsonLdEntities(container: HTMLElement): Record<string, unknown>[] {
/**
* Nenhum link para /#contact nem para o GitHub; o contato é o WhatsApp com o contexto do case, com o mesmo rótulo
* ("Falar sobre meu projeto") no case de cliente e no de laboratório. A mensagem é a mesma frase nos dois, a do cabeçalho:
* `similarProjectSubject(título)`. Um botão só por página (o do fecho): sem dois "Falar sobre meu projeto" seguidos.
* ("Chamar no WhatsApp") no case de cliente e no de laboratório. A mensagem é a mesma frase nos dois, a do cabeçalho:
* `similarProjectSubject(título)`. Um botão só por página (o do fecho): sem dois "Chamar no WhatsApp" seguidos.
*/
function expectContactAndNoDeadLinks(caseStudy: { id: string; title: string }) {
const hrefs = screen
@@ -72,7 +72,7 @@ function expectContactAndNoDeadLinks(caseStudy: { id: string; title: string }) {
expect(hrefs).not.toContain("/#contact");
expect(hrefs.some((href) => /github/i.test(href))).toBe(false);
const ctas = screen.getAllByRole("link", { name: buttonLabels.contact });
const ctas = screen.getAllByRole("link", { name: buttonLabels.whatsapp });
expect(ctas).toHaveLength(1);
for (const cta of ctas) {
const destination = new URL(cta.getAttribute("href") ?? "");
@@ -179,13 +179,13 @@ it("renders a client case in the sales order, with media, contact and collapsed
const capabilitiesSection = sectionHeadings[4].closest("section");
expect(
within(capabilitiesSection as HTMLElement).queryByRole("link", {
name: buttonLabels.contact,
name: buttonLabels.whatsapp,
}),
).toBeNull();
const closingSection = sectionHeadings[5].closest("section");
expect(closingSection).toContainElement(
within(closingSection as HTMLElement).getByRole("link", {
name: buttonLabels.contact,
name: buttonLabels.whatsapp,
}),
);
for (const capability of caseStudy.capabilities ?? []) {
@@ -371,10 +371,10 @@ it("writes what the client can do now right after 'Como ficou', with no contact
expect(items.length).toBeLessThanOrEqual(3);
// sem número inventado
expect(items.join(" ")).not.toMatch(/\d/);
// o único botão de contato é o do fecho, depois da lista: não há dois "Falar sobre meu projeto" seguidos
// o único botão de contato é o do fecho, depois da lista: não há dois "Chamar no WhatsApp" seguidos
expect(
within(capabilitiesSection as HTMLElement).queryByRole("link", {
name: buttonLabels.contact,
name: buttonLabels.whatsapp,
}),
).toBeNull();
const closingSection = screen
@@ -386,7 +386,7 @@ it("writes what the client can do now right after 'Como ficou', with no contact
expect(capabilitiesSection?.nextElementSibling).toBe(closingSection);
expect(
within(closingSection as HTMLElement).getAllByRole("link", {
name: buttonLabels.contact,
name: buttonLabels.whatsapp,
}),
).toHaveLength(1);
unmount();
@@ -477,7 +477,7 @@ it("shows at most two related services, as cards under the contact button", asyn
screen.queryByRole("link", { name: /^Ver serviço relacionado:/ }),
).toBeNull();
// o botão do fecho (o único) vem antes dos cartões
const contact = screen.getByRole("link", { name: buttonLabels.contact });
const contact = screen.getByRole("link", { name: buttonLabels.whatsapp });
expect(isBefore(contact, cards[0])).toBe(true);
unmount();
@@ -748,7 +748,7 @@ it("lists client cases with a cover first, then the laboratory, and keeps the sc
expect(hrefs).not.toContain("/#contact");
expect(hrefs.some((href) => /github/i.test(href))).toBe(false);
expect(hrefs).toContain("/servicos");
const cta = screen.getByRole("link", { name: buttonLabels.contact });
const cta = screen.getByRole("link", { name: buttonLabels.whatsapp });
expect(new URL(cta.getAttribute("href") ?? "").origin).toBe("https://wa.me");
const visibleBreadcrumb = screen.getByRole("navigation", {