chore: dependências e teste de ponta a ponta

Saem gsap, motion, clsx e tailwind-merge; entram server-only, @playwright/test e @axe-core/playwright. Suíte e2e (rotas, axe, teclado, navegação, sem JS, rolagem) rodável com pnpm test:e2e contra BASE_URL.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 14:15:50 -03:00
1 parent 318a764190
commit 7b633f0f4a
12 files changed
+459 -94

No files matched your search

+2
View File
@@ -12,6 +12,8 @@
# testing
/coverage
/test-results/
/playwright-report/
# next.js
/.next/
+1 -1
View File
@@ -54,7 +54,7 @@ Arquivos de cada projeto em `public/videos/<id>/`:
| Arquivo | Para quê |
| --- | --- |
| `desktop.{webm,mp4,webp}` e `mobile.{webm,mp4,webp}` | cards e vídeo ampliado (até 3 MB cada vídeo) |
| `scrub-desktop.mp4` e `scrub-mobile.mp4` | vitrine animada: quadro-chave a cada 6 quadros, sem B-frames, para o tempo mudar a cada quadro da rolagem |
| `scrub-desktop.mp4` | vitrine animada: quadro-chave a cada 6 quadros, sem B-frames, para o tempo mudar a cada quadro da rolagem (o celular da moldura mostra só o pôster) |
| `og.jpg` | capa 1200x630 de compartilhamento |
Para gravar ou regravar um projeto, use `scripts/record-project-videos.mjs`; para gerar os `scrub-*.mp4`, `scripts/encode-scrub-video.mjs` (as instruções de instalação do Playwright e do ffmpeg estão no cabeçalho de cada script). Depois de gravar, marque os capítulos do projeto em `lib/project-showcase.ts`.
+38
View File
@@ -0,0 +1,38 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { APIRequestContext } from "@playwright/test";
/** Caminhos (`/`, `/cases/…`) de todas as rotas do /sitemap.xml, sem o domínio de produção. */
export async function sitemapPaths(request: APIRequestContext) {
const response = await request.get("/sitemap.xml");
if (response.status() !== 200)
throw new Error(`/sitemap.xml respondeu ${response.status()}`);
const xml = await response.text();
const paths = [...xml.matchAll(/<loc>\s*([^<\s]+)\s*<\/loc>/g)].map(
([, loc]) => {
const { pathname } = new URL(loc);
return pathname === "" ? "/" : pathname;
},
);
return [...new Set(paths)];
}
/** Nomes dos projetos de cliente, como a home os lista (public/portfolio-data.json, kind "cliente"). */
export function clientProjectNames() {
const data = JSON.parse(
readFileSync(
join(__dirname, "..", "public", "portfolio-data.json"),
"utf8",
),
) as { projects: Array<{ title: string; kind?: string }> };
return data.projects
.filter((project) => project.kind === "cliente")
.map((project) => project.title);
}
/** Texto do cabeçalho da seção de contato da home. */
export const CONTACT_HEADING =
"Vamos conversar sobre o que sua empresa precisa.";
export const DESKTOP = { width: 1440, height: 900 } as const;
export const PHONE = { width: 390, height: 844 } as const;
+62
View File
@@ -0,0 +1,62 @@
import { expect, test } from "@playwright/test";
import { DESKTOP } from "./helpers";
/**
* (c) Do topo da home, o Tab chega ao campo de nome do contato em no máximo 30 toques.
* O teste continua apertando Tab (até 120) depois do limite só para dizer, na falha, quantos toques faltavam.
*/
const MAX_TABS = 30;
const GIVE_UP_AFTER = 120;
test.use({ viewport: DESKTOP });
test(`Tab do topo da home chega ao campo de nome do contato em até ${MAX_TABS} toques`, async ({
page,
}) => {
await page.goto("/", { waitUntil: "load" });
await page.waitForTimeout(800);
// o foco começa no documento, como numa visita nova
await page.evaluate(() => (document.activeElement as HTMLElement)?.blur());
await page.evaluate(() => window.scrollTo({ top: 0, behavior: "instant" }));
const visited: string[] = [];
let reached = 0;
for (let tab = 1; tab <= GIVE_UP_AFTER; tab++) {
await page.keyboard.press("Tab");
const where = await page.evaluate(() => {
const element = document.activeElement as HTMLElement | null;
if (!element) return { isName: false, label: "(nada)" };
const label = (
element.getAttribute("aria-label") ||
element.textContent ||
element.getAttribute("name") ||
element.tagName
)
.trim()
.replace(/\s+/g, " ")
.slice(0, 40);
return {
isName:
element.getAttribute("name") === "name" &&
Boolean(element.closest("#contact")),
label: `${element.tagName.toLowerCase()} "${label}"`,
};
});
visited.push(`${tab}. ${where.label}`);
if (where.isName) {
reached = tab;
break;
}
}
expect(
reached,
`o campo de nome não apareceu nem em ${GIVE_UP_AFTER} Tabs`,
).toBeGreaterThan(0);
expect(
reached,
`o campo de nome só foi alcançado no Tab ${reached} (limite ${MAX_TABS}); passou por:\n${visited.join("\n")}`,
).toBeLessThanOrEqual(MAX_TABS);
await expect(page.locator("#contact input[name='name']")).toBeFocused();
});
+60
View File
@@ -0,0 +1,60 @@
import { expect, test } from "@playwright/test";
import { DESKTOP } from "./helpers";
/**
* (d) Clicar em "Cases" no menu e depois num case navega sem recarregar o documento (um marcador em `window`
* sobrevive) e cada troca de página dispara uma startViewTransition; a carga inicial, nenhuma.
*/
type Probe = { __marker?: string; __viewTransitions?: number };
test.use({ viewport: DESKTOP });
test("menu Cases e depois um case: sem recarregar o documento, uma transição de página por navegação", async ({
page,
}) => {
// conta as chamadas de document.startViewTransition, desde antes do primeiro script da página
await page.addInitScript(() => {
const probe = window as unknown as Probe;
probe.__viewTransitions = 0;
const original = document.startViewTransition?.bind(document);
if (!original) return;
document.startViewTransition = ((...args: Parameters<typeof original>) => {
(probe.__viewTransitions as number) += 1;
return original(...args);
}) as typeof document.startViewTransition;
});
const transitions = () =>
page.evaluate(() => (window as unknown as Probe).__viewTransitions);
await page.goto("/", { waitUntil: "load" });
await page.waitForTimeout(1500);
await page.evaluate(() => {
(window as unknown as Probe).__marker = "mesmo-documento";
});
expect(
await page.evaluate(() => "startViewTransition" in document),
"o navegador precisa ter startViewTransition",
).toBe(true);
expect(await transitions(), "carga inicial").toBe(0);
await page
.getByRole("navigation", { name: "Navegação principal" })
.getByRole("link", { name: "Cases" })
.click();
await page.waitForURL("**/cases");
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
await page.waitForTimeout(500);
expect(await transitions(), "depois de Cases").toBe(1);
await page.getByRole("link", { name: "Ver o case" }).first().click();
await page.waitForURL(/\/cases\/[^/]+$/);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
await page.waitForTimeout(500);
expect(await transitions(), "depois de abrir um case").toBe(2);
expect(
await page.evaluate(() => (window as unknown as Probe).__marker),
"o documento foi recarregado (o marcador sumiu)",
).toBe("mesmo-documento");
});
+30
View File
@@ -0,0 +1,30 @@
import { expect, test } from "@playwright/test";
import { CONTACT_HEADING, clientProjectNames, DESKTOP } from "./helpers";
/** (e) Com JavaScript desligado, a home mostra o texto do contato e o nome dos clientes. */
test.use({ viewport: DESKTOP, javaScriptEnabled: false });
test("sem JavaScript a home mostra o contato e os nomes dos 4 clientes", async ({
page,
}) => {
const clients = clientProjectNames();
expect(clients, "clientes em public/portfolio-data.json").toHaveLength(4);
const response = await page.goto("/", { waitUntil: "load" });
expect(response?.status()).toBe(200);
await expect(
page.getByRole("heading", { level: 2, name: CONTACT_HEADING }),
).toBeVisible();
for (const name of clients) {
await expect(
page.getByText(name, { exact: false }).first(),
`cliente "${name}"`,
).toBeVisible();
}
// o texto está no HTML entregue, não só na tela: vale também para quem lê sem renderizar
const html = await (await page.request.get("/")).text();
expect(html).toContain(CONTACT_HEADING);
for (const name of clients) expect(html).toContain(name);
});
+91
View File
@@ -0,0 +1,91 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
import { DESKTOP, PHONE, sitemapPaths } from "./helpers";
/**
* (a) Todas as rotas do /sitemap.xml respondem 200, sem erro de console e sem rolagem horizontal, no computador
* (1440x900) e no celular (390x844).
* (b) O axe não acha violação serious ou critical em nenhuma delas.
* As rotas vêm do próprio sitemap: uma página nova entra no teste sozinha.
*/
const VIEWPORTS = [
{ name: "computador 1440x900", size: DESKTOP },
{ name: "celular 390x844", size: PHONE },
] as const;
for (const { name, size } of VIEWPORTS) {
test.describe(`rotas do sitemap, ${name}`, () => {
test.use({ viewport: size });
test("respondem 200, sem erro de console e sem rolagem horizontal", async ({
page,
request,
}) => {
const paths = await sitemapPaths(request);
expect(paths.length, "rotas no sitemap").toBeGreaterThan(10);
for (const path of paths) {
await test.step(path, async () => {
const problems: string[] = [];
const onConsole = (
message: import("@playwright/test").ConsoleMessage,
) => {
if (message.type() === "error")
problems.push(`console.error: ${message.text()}`);
};
const onPageError = (error: Error) =>
problems.push(`erro não tratado: ${error.message}`);
page.on("console", onConsole);
page.on("pageerror", onPageError);
const response = await page.goto(path, { waitUntil: "load" });
// dá tempo de a hidratação e das primeiras animações acontecerem (e de errarem)
await page.waitForTimeout(800);
page.off("console", onConsole);
page.off("pageerror", onPageError);
expect.soft(response?.status(), `${path}: status`).toBe(200);
expect.soft(problems, `${path}: console`).toEqual([]);
const overflow = await page.evaluate(
() =>
Math.max(
document.documentElement.scrollWidth,
document.body.scrollWidth,
) - document.documentElement.clientWidth,
);
expect
.soft(overflow, `${path}: px de rolagem horizontal`)
.toBeLessThanOrEqual(0);
});
}
});
});
}
test.describe("acessibilidade (axe), computador", () => {
test.use({ viewport: DESKTOP });
test("nenhuma violação serious ou critical em nenhuma rota do sitemap", async ({
page,
request,
}) => {
const paths = await sitemapPaths(request);
for (const path of paths) {
await test.step(path, async () => {
await page.goto(path, { waitUntil: "load" });
await page.waitForTimeout(800);
const { violations } = await new AxeBuilder({ page }).analyze();
const serious = violations
.filter(({ impact }) => impact === "serious" || impact === "critical")
.map(
({ id, impact, nodes }) =>
`${id} (${impact}): ${nodes.length} elemento(s), por exemplo ${nodes[0]?.target.join(" ")}`,
);
expect.soft(serious, `${path}: axe`).toEqual([]);
});
}
});
});
+72
View File
@@ -0,0 +1,72 @@
import { expect, test } from "@playwright/test";
import { DESKTOP } from "./helpers";
/**
* (f) No computador, nenhuma das paradas a cada 10 % da rolagem da home tem tela vazia: sempre há ao menos um
* elemento de texto visível na janela. Cabeçalho, avisos, leitores de tela e itens ocultos não contam
* (o cabeçalho fixo estaria em todas as paradas e esconderia uma tela vazia).
*/
test.use({ viewport: DESKTOP });
test("nenhuma parada a cada 10 % da home tem tela vazia", async ({ page }) => {
await page.goto("/", { waitUntil: "load" });
await page.waitForTimeout(1500);
const empty: string[] = [];
const stops: string[] = [];
for (let step = 0; step <= 10; step++) {
await page.evaluate((fraction) => {
const max = document.documentElement.scrollHeight - innerHeight;
window.scrollTo({ top: Math.round(max * fraction), behavior: "instant" });
}, step / 10);
// a rolagem suave, as animações ligadas à rolagem e as imagens preguiçosas assentam
await page.waitForTimeout(700);
const result = await page.evaluate(() => {
const ignored =
"header, footer [aria-hidden], [aria-hidden='true'], [role='dialog'], .sr-only, .skip-link, script, style, noscript";
const effectiveOpacity = (element: Element | null) => {
let opacity = 1;
for (let node = element; node; node = node.parentElement)
opacity *= Number.parseFloat(getComputedStyle(node).opacity);
return opacity;
};
const visible: string[] = [];
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
);
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
const text = node.textContent?.trim() ?? "";
const parent = node.parentElement;
if (text.length < 3 || !parent || parent.closest(ignored)) continue;
const style = getComputedStyle(parent);
if (style.visibility !== "visible" || style.display === "none")
continue;
if (effectiveOpacity(parent) < 0.2) continue;
const range = document.createRange();
range.selectNodeContents(node);
const rect = range.getBoundingClientRect();
const inside =
rect.width > 1 &&
rect.height > 1 &&
rect.bottom > 0 &&
rect.top < innerHeight &&
rect.right > 0 &&
rect.left < innerWidth;
if (inside) visible.push(text.slice(0, 40));
}
return { count: visible.length, sample: visible.slice(0, 2) };
});
const label = `${step * 10}%`;
stops.push(`${label}: ${result.count} trecho(s) de texto`);
if (result.count === 0) empty.push(label);
}
expect(
empty,
`paradas com tela vazia (nenhum texto visível):\n${stops.join("\n")}`,
).toEqual([]);
});
+3 -2
View File
@@ -5,12 +5,13 @@ const config: Config = {
testEnvironment: "jsdom",
roots: ["<rootDir>/app", "<rootDir>/lib"],
modulePathIgnorePatterns: ["<rootDir>/.worktrees/"],
// o teste de fumaça (Playwright) roda com `pnpm test:e2e`, não no Jest
testPathIgnorePatterns: ["/node_modules/", "<rootDir>/e2e/"],
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/$1",
"^.+\\.(css|sass|scss)$": "identity-obj-proxy",
"^.+/animations/PointCloud$":
"<rootDir>/app/components/animations/__mocks__/PointCloud.tsx",
"^server-only$": "<rootDir>/lib/__mocks__/server-only.ts",
},
testMatch: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"],
transform: {
+4 -4
View File
@@ -26,24 +26,24 @@
"format": "biome format --write",
"test": "jest",
"test:watch": "jest --watch",
"test:e2e": "playwright test",
"check:projects": "node scripts/check-projects.mjs"
},
"dependencies": {
"@react-three/drei": "10.7.7",
"@react-three/fiber": "9.5.0",
"clsx": "2.1.1",
"gsap": "3.13.0",
"lenis": "^1.3.26",
"lucide-react": "0.542.0",
"motion": "12.23.12",
"next": "16.3.8",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwind-merge": "3.3.1",
"server-only": "0.0.1",
"three": "0.182.0"
},
"devDependencies": {
"@axe-core/playwright": "^4.13.0",
"@biomejs/biome": "2.2.0",
"@playwright/test": "^1.64.1",
"@tailwindcss/postcss": "4.1.12",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.2",
+38
View File
@@ -0,0 +1,38 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Teste de fumaça de ponta a ponta (pasta e2e/), rodado contra um servidor que já está no ar:
* BASE_URL endereço do site (padrão http://localhost:3001)
* PW_CHROMIUM caminho do chrome.exe, para não baixar navegador do Playwright
* (ex.: %LOCALAPPDATA%\ms-playwright\chromium-1248\chrome-win64\chrome.exe)
* Este arquivo não sobe servidor nenhum: `pnpm build && pnpm start -p 3001` (ou `pnpm dev`) antes de rodar.
*/
const baseURL = process.env.BASE_URL ?? "http://localhost:3001";
const executablePath = process.env.PW_CHROMIUM || undefined;
export default defineConfig({
testDir: "./e2e",
// um servidor de desenvolvimento compila cada rota na primeira visita: com folga
timeout: 120_000,
expect: { timeout: 10_000 },
// um navegador por vez: o servidor local (principalmente o dev) não aguenta várias compilações juntas
workers: 1,
fullyParallel: false,
retries: 0,
reporter: [["list"]],
use: {
baseURL,
trace: "retain-on-failure",
screenshot: "only-on-failure",
launchOptions: { executablePath },
},
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
launchOptions: { executablePath },
},
},
],
});
+58 -87
View File
@@ -18,40 +18,37 @@ importers:
'@react-three/fiber':
specifier: 9.5.0
version: 9.5.0(@types/[email protected])([email protected]([email protected]))([email protected])([email protected])
clsx:
specifier: 2.1.1
version: 2.1.1
gsap:
specifier: 3.13.0
version: 3.13.0
lenis:
specifier: ^1.3.26
version: 1.3.26([email protected])
lucide-react:
specifier: 0.542.0
version: 0.542.0([email protected])
motion:
specifier: 12.23.12
version: 12.23.12([email protected]([email protected]))([email protected])
next:
specifier: 16.3.8
version: 16.3.8(@babel/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
version: 16.3.8(@babel/[email protected])(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
react:
specifier: 19.1.0
version: 19.1.0
react-dom:
specifier: 19.1.0
version: 19.1.0([email protected])
tailwind-merge:
specifier: 3.3.1
version: 3.3.1
server-only:
specifier: 0.0.1
version: 0.0.1
three:
specifier: 0.182.0
version: 0.182.0
devDependencies:
'@axe-core/playwright':
specifier: ^4.13.0
version: 4.13.0([email protected])
'@biomejs/biome':
specifier: 2.2.0
version: 2.2.0
'@playwright/test':
specifier: ^1.64.1
version: 1.64.1
'@tailwindcss/postcss':
specifier: 4.1.12
version: 4.1.12
@@ -113,6 +110,11 @@ packages:
'@asamuzakjp/[email protected]':
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
'@axe-core/[email protected]':
resolution: {integrity: sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==}
peerDependencies:
playwright-core: '>= 1.0.0'
'@babel/[email protected]':
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
engines: {node: '>=6.9.0'}
@@ -769,6 +771,11 @@ packages:
resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==}
engines: {node: ^14.18.0 || >=16.0.0}
'@playwright/[email protected]':
resolution: {integrity: sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==}
engines: {node: '>=20'}
hasBin: true
'@react-three/[email protected]':
resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==}
peerDependencies:
@@ -1203,6 +1210,10 @@ packages:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==}
engines: {node: '>=4'}
[email protected]:
resolution: {integrity: sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -1329,10 +1340,6 @@ packages:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
[email protected]:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
[email protected]:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@@ -1507,20 +1514,6 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
[email protected]:
resolution: {integrity: sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
[email protected]:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@@ -1560,9 +1553,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
[email protected]:
resolution: {integrity: sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==}
[email protected]:
resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==}
engines: {node: '>=0.4.7'}
@@ -2059,26 +2049,6 @@ packages:
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
engines: {node: '>= 18'}
[email protected]:
resolution: {integrity: sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==}
[email protected]:
resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==}
[email protected]:
resolution: {integrity: sha512-8jCD8uW5GD1csOoqh1WhH1A6j5APHVE15nuBkFeRiMzYBdRwyAHmSP/oXSuW0WJPZRXTFdBoG4hY9TFWNhhwng==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
[email protected]:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -2205,6 +2175,16 @@ packages:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'}
[email protected]:
resolution: {integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==}
engines: {node: '>=20'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==}
engines: {node: '>=20'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==}
engines: {node: ^10 || ^12 || >=14}
@@ -2310,6 +2290,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
[email protected]:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
[email protected]:
resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==}
engines: {node: '>=20.9.0'}
@@ -2434,9 +2417,6 @@ packages:
resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==}
engines: {node: ^14.18.0 || >=16.0.0}
[email protected]:
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
[email protected]:
resolution: {integrity: sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==}
@@ -2739,6 +2719,11 @@ snapshots:
'@csstools/css-tokenizer': 3.0.4
lru-cache: 10.4.3
'@axe-core/[email protected]([email protected])':
dependencies:
axe-core: 4.13.0
playwright-core: 1.64.1
'@babel/[email protected]':
dependencies:
'@babel/helper-validator-identifier': 7.29.7
@@ -3468,6 +3453,10 @@ snapshots:
'@pkgr/[email protected]': {}
'@playwright/[email protected]':
dependencies:
playwright: 1.64.1
'@react-three/[email protected](@react-three/[email protected](@types/[email protected])([email protected]([email protected]))([email protected])([email protected]))(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])([email protected])':
dependencies:
'@babel/runtime': 7.29.7
@@ -3866,6 +3855,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected](@babel/[email protected]):
dependencies:
'@babel/core': 7.29.7
@@ -4019,8 +4010,6 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
[email protected]: {}
[email protected]: {}
[email protected]: {}
@@ -4168,15 +4157,6 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
[email protected]([email protected]([email protected]))([email protected]):
dependencies:
motion-dom: 12.43.0
motion-utils: 12.39.0
tslib: 2.8.1
optionalDependencies:
react: 19.1.0
react-dom: 19.1.0([email protected])
[email protected]: {}
[email protected]:
@@ -4212,8 +4192,6 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
minimist: 1.2.8
@@ -4905,20 +4883,6 @@ snapshots:
dependencies:
minipass: 7.1.3
[email protected]:
dependencies:
motion-utils: 12.39.0
[email protected]: {}
[email protected]([email protected]([email protected]))([email protected]):
dependencies:
framer-motion: 12.43.0([email protected]([email protected]))([email protected])
tslib: 2.8.1
optionalDependencies:
react: 19.1.0
react-dom: 19.1.0([email protected])
[email protected]: {}
[email protected]: {}
@@ -4929,7 +4893,7 @@ snapshots:
[email protected]: {}
[email protected](@babel/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]):
[email protected](@babel/[email protected])(@playwright/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected]):
dependencies:
'@next/env': 16.3.8
'@swc/helpers': 0.5.23
@@ -4948,6 +4912,7 @@ snapshots:
'@next/swc-linux-x64-musl': 16.3.8
'@next/swc-win32-arm64-msvc': 16.3.8
'@next/swc-win32-x64-msvc': 16.3.8
'@playwright/test': 1.64.1
sharp: 0.35.4(@types/[email protected])
transitivePeerDependencies:
- '@babel/core'
@@ -5024,6 +4989,12 @@ snapshots:
dependencies:
find-up: 4.1.0
[email protected]: {}
[email protected]:
dependencies:
playwright-core: 1.64.1
[email protected]:
dependencies:
nanoid: 3.3.18
@@ -5116,6 +5087,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected](@types/[email protected]):
dependencies:
'@img/colour': 1.1.0
@@ -5244,8 +5217,6 @@ snapshots:
dependencies:
'@pkgr/core': 0.3.6
[email protected]: {}
[email protected]: {}
[email protected]: {}