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

+29 -7
View File
@@ -77,11 +77,27 @@ describe("CasesPage: cases de cliente", () => {
).toBeNull();
});
it("path: contact -> 'Falar sobre meu projeto', and the list of clients follows the showcase: the SEV first, then Lavação 047, SC Plus, AS Serviços and Julia Beauty", () => {
it("path: an odd number of client cases -> the last card takes the whole row on wide screens, so the grid ends with no empty cell", () => {
const { container } = render(<CasesPage />);
const wide = clientCasesForListing.map((caseStudy) =>
clientCard(container, caseStudy.id).className.includes(
"min-[1025px]:col-span-2",
),
);
const odd = clientCasesForListing.length % 2 === 1;
expect(wide).toEqual(
clientCasesForListing.map(
(_, index) => odd && index === clientCasesForListing.length - 1,
),
);
});
it("path: contact -> 'Chamar no WhatsApp', and the list of clients follows the showcase: the SEV first, then Lavação 047, SC Plus, AS Serviços and Julia Beauty", () => {
const { container } = render(<CasesPage />);
expect(
screen.getByRole("link", { name: buttonLabels.contact }),
screen.getByRole("link", { name: buttonLabels.whatsapp }),
).toHaveAttribute("href", expect.stringContaining("https://wa.me/"));
const clientTitles = Array.from(
container.querySelectorAll("#cases-clientes ~ div article .ds-kicker"),
@@ -98,18 +114,24 @@ describe("CasesPage: cases de cliente", () => {
]);
// um botão de contato só na página: o do fecho
expect(
screen.getAllByRole("link", { name: buttonLabels.contact }),
screen.getAllByRole("link", { name: buttonLabels.whatsapp }),
).toHaveLength(1);
});
});
describe("CasesPage: topo sem mosaico", () => {
it("path: captures found -> no poster mosaic (the cards below already carry the covers), a lower one-column strip, no 'Veja o que já' twice", () => {
it("path: captures found -> no poster mosaic (the cards below already carry the covers), only the block piece beside the text, a lower strip, no 'Veja o que já' twice", () => {
const { container } = render(<CasesPage />);
const hero = container.querySelector("section");
expect(hero?.querySelector("img")).toBeNull();
expect(hero?.querySelector(".ds-section-shell")).not.toHaveClass("grid");
const images = hero?.querySelectorAll("img") ?? [];
expect(images).toHaveLength(1);
expect(images[0].getAttribute("src")).toContain("estado-tela.webp");
// decorativa e só de 1024 px em diante
expect(images[0].closest('[aria-hidden="true"]')).toHaveClass(
"hidden",
"lg:block",
);
expect(hero?.className).toContain("md:!pb-sp-12");
// o título e o subtítulo não repetem a mesma frase
const heading = within(hero as HTMLElement).getByRole("heading", {
@@ -130,7 +152,7 @@ describe("CasesPage: topo sem mosaico", () => {
const { container } = render(<CasesPage />);
const hero = container.querySelector("section");
expect(hero?.querySelector("img")).toBeNull();
expect(hero?.querySelectorAll("img")).toHaveLength(1);
const firstCover = clientCard(
container,
clientCasesForListing[0].id,