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
+100
-7
@@ -1,14 +1,16 @@
|
||||
import {
|
||||
isCoarsePointer,
|
||||
isKnownSlowScene,
|
||||
isKnownSoftwareWebGL,
|
||||
isLowPowerDevice,
|
||||
isMobileViewport,
|
||||
isSaveData,
|
||||
isSoftwareRenderer,
|
||||
isSoftwareWebGL,
|
||||
isWebKit,
|
||||
MOBILE_QUERY,
|
||||
rememberSlowScene,
|
||||
rememberSoftwareWebGL,
|
||||
sceneTier,
|
||||
} from "./device";
|
||||
|
||||
const originalMatchMedia = window.matchMedia;
|
||||
@@ -40,9 +42,11 @@ afterEach(() => {
|
||||
window.matchMedia = originalMatchMedia;
|
||||
});
|
||||
|
||||
const TOUCH = "(pointer: coarse)";
|
||||
|
||||
describe("device hints", () => {
|
||||
it("treats a capable desktop as full power", () => {
|
||||
expect(isLowPowerDevice()).toBe(false);
|
||||
expect(sceneTier()).toBe("full");
|
||||
expect(isCoarsePointer()).toBe(false);
|
||||
expect(isSaveData()).toBe(false);
|
||||
expect(isMobileViewport()).toBe(false);
|
||||
@@ -55,26 +59,80 @@ describe("device hints", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
["a touch pointer", () => mockMedia(["(pointer: coarse)"])],
|
||||
["reduced motion", () => mockMedia(["(prefers-reduced-motion: reduce)"])],
|
||||
["the data saver", () => mockNavigator({ connection: { saveData: true } })],
|
||||
["four cores", () => mockNavigator({ hardwareConcurrency: 4 })],
|
||||
["two cores", () => mockNavigator({ hardwareConcurrency: 2 })],
|
||||
["4 GB of memory", () => mockNavigator({ deviceMemory: 4 })],
|
||||
["2 GB of memory", () => mockNavigator({ deviceMemory: 2 })],
|
||||
])("is low power with %s", (_label, arrange) => {
|
||||
])("shows only the poster with %s", (_label, arrange) => {
|
||||
arrange();
|
||||
expect(isLowPowerDevice()).toBe(true);
|
||||
expect(sceneTier()).toBe("static");
|
||||
});
|
||||
|
||||
it("does not penalize a browser that hides the hardware hints", () => {
|
||||
mockNavigator({ hardwareConcurrency: undefined, deviceMemory: undefined });
|
||||
expect(isLowPowerDevice()).toBe(false);
|
||||
expect(sceneTier()).toBe("full");
|
||||
});
|
||||
|
||||
it("keeps 8 GB and 6 cores on the full path", () => {
|
||||
mockNavigator({ hardwareConcurrency: 6, deviceMemory: 8 });
|
||||
expect(isLowPowerDevice()).toBe(false);
|
||||
expect(sceneTier()).toBe("full");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sceneTier(): one case per row of the decision table", () => {
|
||||
it("row 1: no navigator (server) -> static", () => {
|
||||
const original = Object.getOwnPropertyDescriptor(globalThis, "navigator");
|
||||
Object.defineProperty(globalThis, "navigator", {
|
||||
configurable: true,
|
||||
value: undefined,
|
||||
});
|
||||
try {
|
||||
expect(sceneTier()).toBe("static");
|
||||
} finally {
|
||||
if (original) Object.defineProperty(globalThis, "navigator", original);
|
||||
else Reflect.deleteProperty(globalThis, "navigator");
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
["reduced motion", [TOUCH, "(prefers-reduced-motion: reduce)"], {}],
|
||||
["the data saver", [TOUCH], { connection: { saveData: true } }],
|
||||
])("row 2: a capable phone with %s -> static", (_label, media, hints) => {
|
||||
mockMedia(media);
|
||||
mockNavigator({ hardwareConcurrency: 8, deviceMemory: 8, ...hints });
|
||||
expect(sceneTier()).toBe("static");
|
||||
});
|
||||
|
||||
it("row 3: 4 cores with plenty of memory -> static even with a mouse", () => {
|
||||
mockNavigator({ hardwareConcurrency: 4, deviceMemory: 8 });
|
||||
expect(sceneTier()).toBe("static");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["8 cores and 4 GB", { hardwareConcurrency: 8, deviceMemory: 4 }, "lite"],
|
||||
["8 cores and 2 GB", { hardwareConcurrency: 8, deviceMemory: 2 }, "static"],
|
||||
["5 cores", { hardwareConcurrency: 5 }, "static"],
|
||||
["6 cores and 4 GB", { hardwareConcurrency: 6, deviceMemory: 4 }, "lite"],
|
||||
[
|
||||
"no hints reported",
|
||||
{ hardwareConcurrency: undefined, deviceMemory: undefined },
|
||||
"lite",
|
||||
],
|
||||
])("row 4: touch with %s -> %s", (_label, hints, tier) => {
|
||||
mockMedia([TOUCH]);
|
||||
mockNavigator(hints);
|
||||
expect(sceneTier()).toBe(tier);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["8 cores and 4 GB", { hardwareConcurrency: 8, deviceMemory: 4 }, "static"],
|
||||
["8 cores and 8 GB", { hardwareConcurrency: 8, deviceMemory: 8 }, "full"],
|
||||
["5 cores and no memory hint", { hardwareConcurrency: 5 }, "full"],
|
||||
])("row 5: mouse with %s -> %s", (_label, hints, tier) => {
|
||||
mockNavigator(hints);
|
||||
expect(sceneTier()).toBe(tier);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -162,6 +220,41 @@ describe("the software WebGL verdict kept for the session", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("the slow scene verdict kept for the session", () => {
|
||||
beforeEach(() => sessionStorage.clear());
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
it("is unknown until someone remembers it, then known for the rest of the session", () => {
|
||||
expect(isKnownSlowScene()).toBe(false);
|
||||
|
||||
rememberSlowScene();
|
||||
|
||||
expect(isKnownSlowScene()).toBe(true);
|
||||
expect(sessionStorage.getItem("scene-slow")).toBe("1");
|
||||
});
|
||||
|
||||
it("ignores any other value in the key (only the verdict this module writes counts)", () => {
|
||||
sessionStorage.setItem("scene-slow", "0");
|
||||
|
||||
expect(isKnownSlowScene()).toBe(false);
|
||||
});
|
||||
|
||||
it("counts a blocked sessionStorage as unknown, and remembering then does not throw", () => {
|
||||
jest.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
|
||||
throw new DOMException("blocked", "SecurityError");
|
||||
});
|
||||
jest.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
|
||||
throw new DOMException("blocked", "SecurityError");
|
||||
});
|
||||
|
||||
expect(isKnownSlowScene()).toBe(false);
|
||||
expect(() => rememberSlowScene()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isWebKit (the only engine that needs the showcase video to play once)", () => {
|
||||
const iphone =
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1";
|
||||
|
||||
Reference in new issue
Block a user