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
+95
-8
@@ -1,5 +1,7 @@
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { offerLabels } from "@/lib/offer-labels";
|
||||
import { buttonLabels } from "@/lib/site-content";
|
||||
import { IA_PAGE_LABEL } from "@/lib/site-navigation";
|
||||
import AiPage, { metadata } from "./page";
|
||||
|
||||
it("uses a short search title: up to 44 characters, and 60 with the layout suffix", () => {
|
||||
@@ -50,15 +52,15 @@ it("keeps the full AI content, with the cited incidents summarized only here", (
|
||||
).toHaveAttribute("rel", "noopener noreferrer");
|
||||
});
|
||||
|
||||
it("offers WhatsApp conversations about reviewing an AI-built system", () => {
|
||||
it("offers WhatsApp conversations about reviewing an AI-built system, each one named 'Chamar no WhatsApp'", () => {
|
||||
render(<AiPage />);
|
||||
|
||||
const links = screen
|
||||
.getAllByRole("link")
|
||||
.filter((link) => link.getAttribute("href")?.startsWith("https://wa.me/"));
|
||||
expect(links.map((link) => link.textContent)).toEqual([
|
||||
"Pedir a revisão do meu app",
|
||||
"Avaliar meu projeto",
|
||||
buttonLabels.whatsapp,
|
||||
buttonLabels.whatsapp,
|
||||
]);
|
||||
for (const link of links) {
|
||||
const text = new URL(link.getAttribute("href") ?? "").searchParams.get(
|
||||
@@ -80,9 +82,10 @@ it("has breadcrumb navigation back to the home", () => {
|
||||
expect(
|
||||
within(breadcrumb).getByRole("link", { name: "Início" }),
|
||||
).toHaveAttribute("href", "/");
|
||||
expect(
|
||||
within(breadcrumb).getByText(offerLabels.revisao.name),
|
||||
).toHaveAttribute("aria-current", "page");
|
||||
expect(within(breadcrumb).getByText(IA_PAGE_LABEL)).toHaveAttribute(
|
||||
"aria-current",
|
||||
"page",
|
||||
);
|
||||
});
|
||||
|
||||
it("presents the review as an offer: what the person receives, with no price or deadline", () => {
|
||||
@@ -239,7 +242,86 @@ it("keeps the incident news collapsed in a <details>, closed by default", () =>
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("names the review like the offer on the home: the trail and the structured data use the offer name, not the search title", () => {
|
||||
it("lists the five incidents from 2025 and 2026, with the new sources opening in a new tab", () => {
|
||||
render(<AiPage />);
|
||||
|
||||
for (const title of [
|
||||
"Lovable: dados expostos em aplicações criadas na plataforma",
|
||||
"Replit: um agente apagou dados de uma aplicação",
|
||||
"Base44: aplicações privadas com falha de acesso",
|
||||
"Lovable, 2026: conversas e código de projetos públicos ficaram abertos",
|
||||
"RedAccess, 2026: aplicações internas abertas na internet",
|
||||
]) {
|
||||
expect(screen.getByRole("heading", { name: title })).toBeInTheDocument();
|
||||
}
|
||||
expect(
|
||||
screen.getByText(/São episódios de 2025 e 2026, não avaliações/),
|
||||
).toBeInTheDocument();
|
||||
for (const [name, href] of [
|
||||
[
|
||||
"Relato da Lovable (22/04/2026, em inglês)",
|
||||
"https://lovable.dev/blog/our-response-to-the-april-2026-incident",
|
||||
],
|
||||
[
|
||||
"Reportagem do Security Boulevard (07/05/2026, em inglês)",
|
||||
"https://securityboulevard.com/2026/05/thousands-of-vibe-coded-apps-exposing-corporate-personal-data-redaccess/",
|
||||
],
|
||||
[
|
||||
"Reportagem do Axios (em inglês)",
|
||||
"https://www.axios.com/2026/05/07/loveable-replit-vibe-coding-privacy",
|
||||
],
|
||||
]) {
|
||||
const link = screen.getByRole("link", { name });
|
||||
expect(link).toHaveAttribute("href", href);
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
expect(link.getAttribute("rel")).toContain("noopener");
|
||||
}
|
||||
});
|
||||
|
||||
it("adds the incident deadline to the LGPD card and a third card on who answers for what the AI writes", () => {
|
||||
render(<AiPage />);
|
||||
|
||||
const lgpd = screen
|
||||
.getByRole("heading", { name: "E a LGPD?" })
|
||||
.closest("article") as HTMLElement;
|
||||
expect(lgpd).toHaveTextContent(
|
||||
"a empresa precisa comunicar a ANPD e as pessoas afetadas em três dias úteis depois de saber do fato; agentes de pequeno porte têm o prazo em dobro.",
|
||||
);
|
||||
|
||||
const card = screen
|
||||
.getByRole("heading", { name: "Quem responde pelo que a IA escreve?" })
|
||||
.closest("article") as HTMLElement;
|
||||
expect(card).toHaveClass("ds-card");
|
||||
expect(card).toHaveTextContent("quem assina responde pelo que entrega.");
|
||||
// os três cartões dividem a mesma grade
|
||||
expect(card.parentElement).toBe(lgpd.parentElement);
|
||||
expect(card.parentElement?.querySelectorAll("article.ds-card")).toHaveLength(
|
||||
3,
|
||||
);
|
||||
expect(card.parentElement).toHaveClass("lg:grid-cols-3");
|
||||
// alinhados pelo topo: o .ds-copy (flex-grow: 1) não estica o cartão mais curto com um buraco no meio
|
||||
expect(card.parentElement).toHaveClass("items-start");
|
||||
|
||||
for (const [scope, name, href] of [
|
||||
[
|
||||
lgpd,
|
||||
"ANPD: Resolução nº 15/2024, arts. 6º e 9º",
|
||||
"https://www.in.gov.br/en/web/dou/-/resolucao-cd/anpd-n-15-de-24-de-abril-de-2024-556243024",
|
||||
],
|
||||
[
|
||||
card,
|
||||
"TST: notícia de 26/03/2026",
|
||||
"https://www.tst.jus.br/-/empresa-e-advogado-sao-multados-por-uso-de-ia-com-decisoes-falsas",
|
||||
],
|
||||
] as const) {
|
||||
const link = within(scope).getByRole("link", { name });
|
||||
expect(link).toHaveAttribute("href", href);
|
||||
expect(link).toHaveAttribute("target", "_blank");
|
||||
expect(link.getAttribute("rel")).toContain("noopener");
|
||||
}
|
||||
});
|
||||
|
||||
it("names the page with its own label in the trail and the structured data (the one in the menu and the footer), and keeps the offer name for the offer", () => {
|
||||
const { container } = render(<AiPage />);
|
||||
|
||||
const script = container.querySelector('script[type="application/ld+json"]');
|
||||
@@ -249,6 +331,11 @@ it("names the review like the offer on the home: the trail and the structured da
|
||||
);
|
||||
expect(
|
||||
breadcrumb.itemListElement.map(({ name }: { name: string }) => name),
|
||||
).toEqual(["Início", offerLabels.revisao.name]);
|
||||
).toEqual(["Início", "Revisão de app com IA"]);
|
||||
expect(IA_PAGE_LABEL).toBe("Revisão de app com IA");
|
||||
expect(offerLabels.revisao.name).toBe("Revisão de sistema que já existe");
|
||||
// sem rótulo sobre o título: a trilha logo acima já nomeia a página
|
||||
expect(
|
||||
container.querySelector("section")?.querySelector(".ds-eyebrow"),
|
||||
).toBeNull();
|
||||
});
|
||||
Reference in new issue
Block a user