diff --git a/.gitignore b/.gitignore index e11a0f22d..8d0c02629 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist/ coverage/ *.patch *-context.md +packages/web/screenshots/ diff --git a/packages/web/e2e/lib/browser.ts b/packages/web/e2e/lib/browser.ts new file mode 100644 index 000000000..7ba80a3c8 --- /dev/null +++ b/packages/web/e2e/lib/browser.ts @@ -0,0 +1,63 @@ +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { chromium } from "playwright"; + +interface ScreenshotOptions { + width?: number; + height?: number; +} + +interface PageSpec { + path: string; + name: string; +} + +function pathToName(path: string): string { + if (path === "/") return "dashboard"; + return path.replace(/^\//, "").replace(/\//g, "-"); +} + +const DEFAULT_PAGES: PageSpec[] = [ + { path: "/", name: "dashboard" }, + { path: "/sessions/backend-3", name: "session-detail" }, +]; + +export async function captureScreenshots( + baseUrl: string, + extraPaths: string[], + options: ScreenshotOptions = {}, +): Promise { + const { width = 1280, height = 900 } = options; + + const screenshotDir = join(new URL("../../", import.meta.url).pathname, "screenshots"); + await mkdir(screenshotDir, { recursive: true }); + + const pages: PageSpec[] = extraPaths.length > 0 + ? extraPaths.map((p) => ({ path: p, name: pathToName(p) })) + : DEFAULT_PAGES; + + const browser = await chromium.launch({ headless: true }); + const savedPaths: string[] = []; + + try { + const context = await browser.newContext({ viewport: { width, height } }); + const page = await context.newPage(); + + for (const spec of pages) { + const url = `${baseUrl}${spec.path}`; + console.log(`Navigating to ${url}`); + await page.goto(url, { waitUntil: "networkidle" }); + // Extra delay for React hydration + await page.waitForTimeout(1000); + + const filePath = join(screenshotDir, `${spec.name}.png`); + await page.screenshot({ path: filePath, fullPage: true }); + savedPaths.push(filePath); + console.log(`Saved ${filePath}`); + } + } finally { + await browser.close(); + } + + return savedPaths; +} diff --git a/packages/web/e2e/lib/server.ts b/packages/web/e2e/lib/server.ts new file mode 100644 index 000000000..f5688e174 --- /dev/null +++ b/packages/web/e2e/lib/server.ts @@ -0,0 +1,69 @@ +import { spawn, type ChildProcess } from "node:child_process"; +import { request } from "node:http"; + +interface ServerHandle { + baseUrl: string; + stop: () => void; +} + +function probePort(port: number): Promise { + return new Promise((resolve) => { + const req = request({ hostname: "127.0.0.1", port, path: "/", method: "HEAD", timeout: 2000 }, (res) => { + res.resume(); + resolve(true); + }); + req.on("error", () => resolve(false)); + req.on("timeout", () => { + req.destroy(); + resolve(false); + }); + req.end(); + }); +} + +async function waitForServer(port: number, timeoutMs: number): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (await probePort(port)) return; + await new Promise((r) => setTimeout(r, 500)); + } + throw new Error(`Server did not start on port ${port} within ${timeoutMs}ms`); +} + +export async function ensureServer(port: number): Promise { + const baseUrl = `http://127.0.0.1:${port}`; + + if (await probePort(port)) { + console.log(`Reusing existing server on port ${port}`); + return { baseUrl, stop: () => {} }; + } + + console.log(`Starting dev server on port ${port}...`); + const child: ChildProcess = spawn("npx", ["next", "dev", "--port", String(port)], { + cwd: new URL("../../", import.meta.url).pathname, + stdio: "pipe", + env: { ...process.env, NODE_ENV: "development" }, + }); + + child.stdout?.resume(); + child.stderr?.on("data", (chunk: Buffer) => { + const line = chunk.toString().trim(); + if (line) console.log(`[next] ${line}`); + }); + + try { + await waitForServer(port, 30_000); + } catch (err) { + child.kill("SIGTERM"); + throw err; + } + + console.log(`Dev server ready on ${baseUrl}`); + + return { + baseUrl, + stop: () => { + child.kill("SIGTERM"); + }, + }; +} diff --git a/packages/web/e2e/screenshot.ts b/packages/web/e2e/screenshot.ts new file mode 100644 index 000000000..0421ecc88 --- /dev/null +++ b/packages/web/e2e/screenshot.ts @@ -0,0 +1,49 @@ +import { ensureServer } from "./lib/server.js"; +import { captureScreenshots } from "./lib/browser.js"; + +function parseArgs(argv: string[]): { port: number; width: number; height: number; paths: string[] } { + const args = argv.slice(2); + let port = 3333; + let width = 1280; + let height = 900; + const paths: string[] = []; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === "--port" && args[i + 1]) { + const n = Number(args[++i]); + if (Number.isNaN(n)) throw new Error(`Invalid --port value: ${args[i]}`); + port = n; + } else if (arg === "--width" && args[i + 1]) { + const n = Number(args[++i]); + if (Number.isNaN(n)) throw new Error(`Invalid --width value: ${args[i]}`); + width = n; + } else if (arg === "--height" && args[i + 1]) { + const n = Number(args[++i]); + if (Number.isNaN(n)) throw new Error(`Invalid --height value: ${args[i]}`); + height = n; + } else if (arg?.startsWith("/")) { + paths.push(arg); + } + } + + return { port, width, height, paths }; +} + +async function main(): Promise { + const { port, width, height, paths } = parseArgs(process.argv); + + const server = await ensureServer(port); + + try { + const saved = await captureScreenshots(server.baseUrl, paths, { width, height }); + console.log(`\nDone — ${saved.length} screenshot(s) saved.`); + } finally { + server.stop(); + } +} + +main().catch((err: unknown) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/web/next-env.d.ts b/packages/web/next-env.d.ts new file mode 100644 index 000000000..830fb594c --- /dev/null +++ b/packages/web/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/packages/web/next.config.js b/packages/web/next.config.js new file mode 100644 index 000000000..caa41d18e --- /dev/null +++ b/packages/web/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + transpilePackages: ["@agent-orchestrator/core"], +}; + +export default nextConfig; diff --git a/packages/web/package.json b/packages/web/package.json index 48e6d6fb8..fb936f284 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -9,7 +9,11 @@ "build": "next build", "start": "next start", "typecheck": "tsc --noEmit", - "clean": "rm -rf .next" + "test": "vitest run", + "test:watch": "vitest", + "clean": "rm -rf .next", + "screenshot": "tsx e2e/screenshot.ts", + "screenshot:install": "npx playwright install chromium" }, "dependencies": { "@agent-orchestrator/core": "workspace:*", @@ -18,9 +22,17 @@ "react-dom": "^19.0.0" }, "devDependencies": { + "@tailwindcss/postcss": "^4.0.0", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.1.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "jsdom": "^25.0.0", "tailwindcss": "^4.0.0", - "typescript": "^5.7.0" + "typescript": "^5.7.0", + "playwright": "^1.49.0", + "tsx": "^4.19.0", + "vitest": "^2.1.0" } } diff --git a/packages/web/postcss.config.mjs b/packages/web/postcss.config.mjs new file mode 100644 index 000000000..c2ddf7482 --- /dev/null +++ b/packages/web/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/packages/web/screenshots/dashboard.png b/packages/web/screenshots/dashboard.png new file mode 100644 index 000000000..bd310cf3c Binary files /dev/null and b/packages/web/screenshots/dashboard.png differ diff --git a/packages/web/screenshots/session-detail.png b/packages/web/screenshots/session-detail.png new file mode 100644 index 000000000..b0ddd1f66 Binary files /dev/null and b/packages/web/screenshots/session-detail.png differ diff --git a/packages/web/screenshots/sessions-backend-3.png b/packages/web/screenshots/sessions-backend-3.png new file mode 100644 index 000000000..e5efb784f Binary files /dev/null and b/packages/web/screenshots/sessions-backend-3.png differ diff --git a/packages/web/screenshots/sessions-backend-7.png b/packages/web/screenshots/sessions-backend-7.png new file mode 100644 index 000000000..14ce63c67 Binary files /dev/null and b/packages/web/screenshots/sessions-backend-7.png differ diff --git a/packages/web/screenshots/sessions-backend-9.png b/packages/web/screenshots/sessions-backend-9.png new file mode 100644 index 000000000..3fbb96ff8 Binary files /dev/null and b/packages/web/screenshots/sessions-backend-9.png differ diff --git a/packages/web/screenshots/sessions-frontend-2.png b/packages/web/screenshots/sessions-frontend-2.png new file mode 100644 index 000000000..d61804feb Binary files /dev/null and b/packages/web/screenshots/sessions-frontend-2.png differ diff --git a/packages/web/src/__tests__/api-routes.test.ts b/packages/web/src/__tests__/api-routes.test.ts new file mode 100644 index 000000000..5c01fb9e2 --- /dev/null +++ b/packages/web/src/__tests__/api-routes.test.ts @@ -0,0 +1,297 @@ +import { describe, it, expect } from "vitest"; +import { NextRequest } from "next/server"; + +// Import route handlers directly +import { GET as sessionsGET } from "@/app/api/sessions/route"; +import { POST as spawnPOST } from "@/app/api/spawn/route"; +import { POST as sendPOST } from "@/app/api/sessions/[id]/send/route"; +import { POST as killPOST } from "@/app/api/sessions/[id]/kill/route"; +import { POST as restorePOST } from "@/app/api/sessions/[id]/restore/route"; +import { POST as mergePOST } from "@/app/api/prs/[id]/merge/route"; +import { GET as eventsGET } from "@/app/api/events/route"; + +function makeRequest(url: string, init?: RequestInit): NextRequest { + return new NextRequest( + new URL(url, "http://localhost:3000"), + init as ConstructorParameters[1], + ); +} + +describe("API Routes", () => { + // ── GET /api/sessions ────────────────────────────────────────────── + + describe("GET /api/sessions", () => { + it("returns sessions array and stats", async () => { + const res = await sessionsGET(); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.sessions).toBeDefined(); + expect(Array.isArray(data.sessions)).toBe(true); + expect(data.sessions.length).toBeGreaterThan(0); + expect(data.stats).toBeDefined(); + expect(data.stats.totalSessions).toBe(data.sessions.length); + }); + + it("stats include expected fields", async () => { + const res = await sessionsGET(); + const data = await res.json(); + expect(data.stats).toHaveProperty("totalSessions"); + expect(data.stats).toHaveProperty("workingSessions"); + expect(data.stats).toHaveProperty("openPRs"); + expect(data.stats).toHaveProperty("needsReview"); + }); + + it("sessions have expected shape", async () => { + const res = await sessionsGET(); + const data = await res.json(); + const session = data.sessions[0]; + expect(session).toHaveProperty("id"); + expect(session).toHaveProperty("projectId"); + expect(session).toHaveProperty("status"); + expect(session).toHaveProperty("activity"); + expect(session).toHaveProperty("createdAt"); + }); + }); + + // ── POST /api/spawn ──────────────────────────────────────────────── + + describe("POST /api/spawn", () => { + it("creates a session with valid input", async () => { + const req = makeRequest("/api/spawn", { + method: "POST", + body: JSON.stringify({ projectId: "my-app", issueId: "INT-100" }), + headers: { "Content-Type": "application/json" }, + }); + const res = await spawnPOST(req); + expect(res.status).toBe(201); + const data = await res.json(); + expect(data.session).toBeDefined(); + expect(data.session.projectId).toBe("my-app"); + expect(data.session.issueId).toBe("INT-100"); + expect(data.session.status).toBe("spawning"); + }); + + it("returns 400 when projectId is missing", async () => { + const req = makeRequest("/api/spawn", { + method: "POST", + body: JSON.stringify({}), + headers: { "Content-Type": "application/json" }, + }); + const res = await spawnPOST(req); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toMatch(/projectId/); + }); + + it("returns 400 with invalid JSON", async () => { + const req = makeRequest("/api/spawn", { + method: "POST", + body: "not json", + headers: { "Content-Type": "application/json" }, + }); + const res = await spawnPOST(req); + expect(res.status).toBe(400); + }); + + it("handles missing issueId gracefully", async () => { + const req = makeRequest("/api/spawn", { + method: "POST", + body: JSON.stringify({ projectId: "my-app" }), + headers: { "Content-Type": "application/json" }, + }); + const res = await spawnPOST(req); + expect(res.status).toBe(201); + const data = await res.json(); + expect(data.session.issueId).toBeNull(); + }); + }); + + // ── POST /api/sessions/:id/send ──────────────────────────────────── + + describe("POST /api/sessions/:id/send", () => { + it("sends a message to a valid session", async () => { + const req = makeRequest("/api/sessions/backend-3/send", { + method: "POST", + body: JSON.stringify({ message: "Fix the tests" }), + headers: { "Content-Type": "application/json" }, + }); + const res = await sendPOST(req, { params: Promise.resolve({ id: "backend-3" }) }); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.ok).toBe(true); + expect(data.message).toBe("Fix the tests"); + }); + + it("returns 404 for unknown session", async () => { + const req = makeRequest("/api/sessions/nonexistent/send", { + method: "POST", + body: JSON.stringify({ message: "hello" }), + headers: { "Content-Type": "application/json" }, + }); + const res = await sendPOST(req, { params: Promise.resolve({ id: "nonexistent" }) }); + expect(res.status).toBe(404); + }); + + it("returns 400 when message is missing", async () => { + const req = makeRequest("/api/sessions/backend-3/send", { + method: "POST", + body: JSON.stringify({}), + headers: { "Content-Type": "application/json" }, + }); + const res = await sendPOST(req, { params: Promise.resolve({ id: "backend-3" }) }); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toMatch(/message/); + }); + + it("returns 400 for invalid JSON body", async () => { + const req = makeRequest("/api/sessions/backend-3/send", { + method: "POST", + body: "not json", + headers: { "Content-Type": "application/json" }, + }); + const res = await sendPOST(req, { params: Promise.resolve({ id: "backend-3" }) }); + expect(res.status).toBe(400); + }); + + it("returns 400 for control-char-only message", async () => { + const req = makeRequest("/api/sessions/backend-3/send", { + method: "POST", + body: JSON.stringify({ message: "\x00\x01\x02" }), + headers: { "Content-Type": "application/json" }, + }); + const res = await sendPOST(req, { params: Promise.resolve({ id: "backend-3" }) }); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toMatch(/empty/); + }); + }); + + // ── POST /api/sessions/:id/kill ──────────────────────────────────── + + describe("POST /api/sessions/:id/kill", () => { + it("kills a valid session", async () => { + const req = makeRequest("/api/sessions/backend-3/kill", { method: "POST" }); + const res = await killPOST(req, { params: Promise.resolve({ id: "backend-3" }) }); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.ok).toBe(true); + expect(data.sessionId).toBe("backend-3"); + }); + + it("returns 404 for unknown session", async () => { + const req = makeRequest("/api/sessions/nonexistent/kill", { method: "POST" }); + const res = await killPOST(req, { params: Promise.resolve({ id: "nonexistent" }) }); + expect(res.status).toBe(404); + }); + }); + + // ── POST /api/sessions/:id/restore ───────────────────────────────── + + describe("POST /api/sessions/:id/restore", () => { + it("restores a killed session", async () => { + const req = makeRequest("/api/sessions/frontend-1/restore", { method: "POST" }); + const res = await restorePOST(req, { params: Promise.resolve({ id: "frontend-1" }) }); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.ok).toBe(true); + expect(data.sessionId).toBe("frontend-1"); + }); + + it("returns 404 for unknown session", async () => { + const req = makeRequest("/api/sessions/nonexistent/restore", { method: "POST" }); + const res = await restorePOST(req, { params: Promise.resolve({ id: "nonexistent" }) }); + expect(res.status).toBe(404); + }); + + it("returns 409 for active session", async () => { + const req = makeRequest("/api/sessions/backend-9/restore", { method: "POST" }); + const res = await restorePOST(req, { params: Promise.resolve({ id: "backend-9" }) }); + expect(res.status).toBe(409); + const data = await res.json(); + expect(data.error).toMatch(/not in a terminal state/); + }); + }); + + // ── POST /api/prs/:id/merge ──────────────────────────────────────── + + describe("POST /api/prs/:id/merge", () => { + it("merges a mergeable PR", async () => { + // PR #432 (backend-7) is mergeable in mock data + const req = makeRequest("/api/prs/432/merge", { method: "POST" }); + const res = await mergePOST(req, { params: Promise.resolve({ id: "432" }) }); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.ok).toBe(true); + expect(data.prNumber).toBe(432); + }); + + it("returns 404 for unknown PR", async () => { + const req = makeRequest("/api/prs/99999/merge", { method: "POST" }); + const res = await mergePOST(req, { params: Promise.resolve({ id: "99999" }) }); + expect(res.status).toBe(404); + }); + + it("returns 422 for non-mergeable PR", async () => { + // PR #428 (backend-5) has failing CI → not mergeable + const req = makeRequest("/api/prs/428/merge", { method: "POST" }); + const res = await mergePOST(req, { params: Promise.resolve({ id: "428" }) }); + expect(res.status).toBe(422); + const data = await res.json(); + expect(data.error).toMatch(/not mergeable/); + expect(data.blockers).toBeDefined(); + }); + + it("returns 400 for non-numeric PR id", async () => { + const req = makeRequest("/api/prs/abc/merge", { method: "POST" }); + const res = await mergePOST(req, { params: Promise.resolve({ id: "abc" }) }); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toMatch(/Invalid PR number/); + }); + + it("returns 422 for draft PR", async () => { + // PR #440 (frontend-9) is a draft + const req = makeRequest("/api/prs/440/merge", { method: "POST" }); + const res = await mergePOST(req, { params: Promise.resolve({ id: "440" }) }); + expect(res.status).toBe(422); + const data = await res.json(); + expect(data.error).toMatch(/draft/i); + }); + + it("returns 409 for merged PR", async () => { + // PR #410 (backend-1) is merged + const req = makeRequest("/api/prs/410/merge", { method: "POST" }); + const res = await mergePOST(req, { params: Promise.resolve({ id: "410" }) }); + expect(res.status).toBe(409); + const data = await res.json(); + expect(data.error).toMatch(/merged/); + }); + }); + + // ── GET /api/events (SSE) ────────────────────────────────────────── + + describe("GET /api/events", () => { + it("returns SSE content type", async () => { + const res = await eventsGET(); + expect(res.headers.get("Content-Type")).toBe("text/event-stream"); + expect(res.headers.get("Cache-Control")).toBe("no-cache"); + }); + + it("streams initial snapshot event", async () => { + const res = await eventsGET(); + const reader = res.body!.getReader(); + const { value } = await reader.read(); + reader.cancel(); + const text = new TextDecoder().decode(value); + expect(text).toContain("data: "); + const jsonStr = text.replace("data: ", "").trim(); + const event = JSON.parse(jsonStr); + expect(event.type).toBe("snapshot"); + expect(Array.isArray(event.sessions)).toBe(true); + expect(event.sessions.length).toBeGreaterThan(0); + expect(event.sessions[0]).toHaveProperty("id"); + expect(event.sessions[0]).toHaveProperty("attentionLevel"); + }); + }); +}); diff --git a/packages/web/src/__tests__/components.test.tsx b/packages/web/src/__tests__/components.test.tsx new file mode 100644 index 000000000..74efc403e --- /dev/null +++ b/packages/web/src/__tests__/components.test.tsx @@ -0,0 +1,432 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { CIBadge, CICheckList } from "@/components/CIBadge"; +import { PRStatus } from "@/components/PRStatus"; +import { SessionCard } from "@/components/SessionCard"; +import { AttentionZone } from "@/components/AttentionZone"; +import { makeSession, makePR } from "./helpers"; + +// ── CIBadge ────────────────────────────────────────────────────────── + +describe("CIBadge", () => { + it("renders passing status", () => { + render(); + expect(screen.getByText("CI passing")).toBeInTheDocument(); + }); + + it("renders failing status with check count", () => { + const checks = [ + { name: "build", status: "failed" as const }, + { name: "test", status: "failed" as const }, + { name: "lint", status: "passed" as const }, + ]; + render(); + expect(screen.getByText("2 checks failing")).toBeInTheDocument(); + }); + + it("renders single failing check without plural", () => { + const checks = [ + { name: "build", status: "failed" as const }, + { name: "lint", status: "passed" as const }, + ]; + render(); + expect(screen.getByText("1 check failing")).toBeInTheDocument(); + }); + + it("renders pending status", () => { + render(); + expect(screen.getByText("CI pending")).toBeInTheDocument(); + }); + + it("renders em-dash for none status", () => { + const { container } = render(); + expect(container.textContent).toContain("—"); + }); + + it("hides icon in compact mode", () => { + const { container } = render(); + // In compact mode, no icon span before the label + const spans = container.querySelectorAll("span > span"); + // Should only have the label text, no extra icon span + expect(spans.length).toBe(0); + }); +}); + +// ── CICheckList ────────────────────────────────────────────────────── + +describe("CICheckList", () => { + it("renders all checks", () => { + const checks = [ + { name: "build", status: "passed" as const }, + { name: "test", status: "failed" as const, url: "https://example.com/test" }, + { name: "lint", status: "pending" as const }, + ]; + render(); + expect(screen.getByText("build")).toBeInTheDocument(); + expect(screen.getByText("test")).toBeInTheDocument(); + expect(screen.getByText("lint")).toBeInTheDocument(); + }); + + it("sorts failed checks first", () => { + const checks = [ + { name: "lint", status: "passed" as const }, + { name: "build", status: "failed" as const }, + { name: "test", status: "running" as const }, + ]; + const { container } = render(); + const names = Array.from(container.querySelectorAll(".truncate")).map((el) => el.textContent); + expect(names[0]).toBe("build"); // failed first + expect(names[1]).toBe("test"); // running second + expect(names[2]).toBe("lint"); // passed last + }); + + it("renders view links for checks with URLs", () => { + const checks = [ + { name: "build", status: "passed" as const, url: "https://example.com/build" }, + { name: "test", status: "passed" as const }, + ]; + render(); + const links = screen.getAllByText("view"); + expect(links).toHaveLength(1); + expect(links[0]).toHaveAttribute("href", "https://example.com/build"); + }); +}); + +// ── PRStatus ───────────────────────────────────────────────────────── + +describe("PRStatus", () => { + it("renders PR number as link", () => { + const pr = makePR({ number: 42 }); + render(); + expect(screen.getByText("#42")).toBeInTheDocument(); + expect(screen.getByText("#42").closest("a")).toHaveAttribute("href", pr.url); + }); + + it("renders size label", () => { + const pr = makePR({ additions: 50, deletions: 10 }); + render(); + expect(screen.getByText("+50 -10 S")).toBeInTheDocument(); + }); + + it("computes XL size label for large PRs", () => { + const pr = makePR({ additions: 800, deletions: 300 }); + render(); + expect(screen.getByText("+800 -300 XL")).toBeInTheDocument(); + }); + + it("shows merged badge for merged PRs", () => { + const pr = makePR({ state: "merged" }); + render(); + expect(screen.getByText("merged")).toBeInTheDocument(); + }); + + it("shows draft badge for draft PRs", () => { + const pr = makePR({ isDraft: true, state: "open" }); + render(); + expect(screen.getByText("draft")).toBeInTheDocument(); + }); + + it("shows approved badge", () => { + const pr = makePR({ reviewDecision: "approved", state: "open" }); + render(); + expect(screen.getByText("approved")).toBeInTheDocument(); + }); + + it("does not show CI badge for draft PRs", () => { + const pr = makePR({ isDraft: true, state: "open", ciStatus: "passing" }); + render(); + expect(screen.queryByText("CI passing")).not.toBeInTheDocument(); + }); + + it("does not show CI badge for merged PRs", () => { + const pr = makePR({ state: "merged", ciStatus: "passing" }); + render(); + expect(screen.queryByText("CI passing")).not.toBeInTheDocument(); + }); +}); + +// ── SessionCard ────────────────────────────────────────────────────── + +describe("SessionCard", () => { + it("renders session id and summary", () => { + const session = makeSession({ id: "backend-1", summary: "Fixing auth" }); + render(); + expect(screen.getByText("backend-1")).toBeInTheDocument(); + expect(screen.getByText("Fixing auth")).toBeInTheDocument(); + }); + + it("shows PR title instead of summary when PR exists", () => { + const pr = makePR({ title: "feat: add auth" }); + const session = makeSession({ summary: "Fixing auth", pr }); + render(); + expect(screen.getByText("feat: add auth")).toBeInTheDocument(); + }); + + it("renders branch name", () => { + const session = makeSession({ branch: "feat/cool-thing" }); + render(); + expect(screen.getByText("feat/cool-thing")).toBeInTheDocument(); + }); + + it("renders terminal link", () => { + const session = makeSession({ id: "backend-5" }); + render(); + const link = screen.getByText("terminal"); + expect(link).toHaveAttribute("href", "/sessions/backend-5"); + }); + + it("shows restore button when agent has exited", () => { + const session = makeSession({ activity: "exited" }); + render(); + expect(screen.getByText("restore session")).toBeInTheDocument(); + }); + + it("does not show restore button when agent is active", () => { + const session = makeSession({ activity: "active" }); + render(); + expect(screen.queryByText("restore session")).not.toBeInTheDocument(); + }); + + it("calls onRestore when restore button is clicked", () => { + const onRestore = vi.fn(); + const session = makeSession({ id: "backend-1", activity: "exited" }); + render(); + fireEvent.click(screen.getByText("restore session")); + expect(onRestore).toHaveBeenCalledWith("backend-1"); + }); + + it("shows merge button when PR is mergeable", () => { + const pr = makePR({ + number: 42, + state: "open", + mergeability: { + mergeable: true, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: [], + }, + }); + const session = makeSession({ status: "mergeable", activity: "idle", pr }); + render(); + expect(screen.getByText("merge PR #42")).toBeInTheDocument(); + }); + + it("calls onMerge when merge button is clicked", () => { + const onMerge = vi.fn(); + const pr = makePR({ + number: 42, + state: "open", + mergeability: { + mergeable: true, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: [], + }, + }); + const session = makeSession({ status: "mergeable", activity: "idle", pr }); + render(); + fireEvent.click(screen.getByText("merge PR #42")); + expect(onMerge).toHaveBeenCalledWith(42); + }); + + it("shows CI failing alert", () => { + const pr = makePR({ + state: "open", + ciStatus: "failing", + ciChecks: [ + { name: "build", status: "passed" }, + { name: "test", status: "failed" }, + ], + reviewDecision: "approved", + mergeability: { + mergeable: false, + ciPassing: false, + approved: true, + noConflicts: true, + blockers: [], + }, + }); + const session = makeSession({ status: "ci_failed", activity: "idle", pr }); + render(); + expect(screen.getByText("1 CI check failing")).toBeInTheDocument(); + }); + + it("shows changes requested alert", () => { + const pr = makePR({ + state: "open", + ciStatus: "passing", + reviewDecision: "changes_requested", + mergeability: { + mergeable: false, + ciPassing: true, + approved: false, + noConflicts: true, + blockers: [], + }, + }); + const session = makeSession({ activity: "idle", pr }); + render(); + expect(screen.getByText("changes requested")).toBeInTheDocument(); + }); + + it("shows needs review alert", () => { + const pr = makePR({ + state: "open", + ciStatus: "passing", + reviewDecision: "pending", + mergeability: { + mergeable: false, + ciPassing: true, + approved: false, + noConflicts: true, + blockers: [], + }, + }); + const session = makeSession({ activity: "idle", pr }); + render(); + expect(screen.getByText("needs review")).toBeInTheDocument(); + }); + + it("shows unresolved comments alert with count", () => { + const pr = makePR({ + state: "open", + ciStatus: "passing", + reviewDecision: "approved", + unresolvedThreads: 3, + unresolvedComments: [ + { url: "https://example.com/1", path: "src/a.ts", author: "alice", body: "fix" }, + { url: "https://example.com/2", path: "src/b.ts", author: "bob", body: "fix" }, + { url: "https://example.com/3", path: "src/c.ts", author: "carol", body: "fix" }, + ], + mergeability: { + mergeable: false, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: [], + }, + }); + const session = makeSession({ activity: "idle", pr }); + render(); + expect(screen.getByText("3")).toBeInTheDocument(); + expect(screen.getByText("unresolved comments")).toBeInTheDocument(); + }); + + it("shows action buttons when agent is idle", () => { + const pr = makePR({ + state: "open", + ciStatus: "failing", + ciChecks: [{ name: "test", status: "failed" }], + reviewDecision: "approved", + mergeability: { + mergeable: false, + ciPassing: false, + approved: true, + noConflicts: true, + blockers: [], + }, + }); + const session = makeSession({ activity: "idle", pr }); + render(); + expect(screen.getByText("ask to fix CI")).toBeInTheDocument(); + }); + + it("hides action buttons when agent is active", () => { + const pr = makePR({ + state: "open", + ciStatus: "failing", + ciChecks: [{ name: "test", status: "failed" }], + reviewDecision: "approved", + mergeability: { + mergeable: false, + ciPassing: false, + approved: true, + noConflicts: true, + blockers: [], + }, + }); + const session = makeSession({ activity: "active", pr }); + render(); + expect(screen.queryByText("ask to fix CI")).not.toBeInTheDocument(); + }); + + it("expands detail panel on click", () => { + const session = makeSession({ id: "test-1", issueId: "INT-100", pr: null }); + const { container } = render(); + expect(screen.queryByText("INT-100")).not.toBeInTheDocument(); + // Click the card (not a button/link) + fireEvent.click(container.firstElementChild!); + expect(screen.getByText("INT-100")).toBeInTheDocument(); + expect(screen.getByText("No PR associated with this session.")).toBeInTheDocument(); + }); + + it("shows terminate session button in expanded view", () => { + const session = makeSession({ pr: null }); + const { container } = render(); + fireEvent.click(container.firstElementChild!); + expect(screen.getByText("terminate session")).toBeInTheDocument(); + }); +}); + +// ── AttentionZone ──────────────────────────────────────────────────── + +describe("AttentionZone", () => { + it("renders zone label and session count", () => { + const sessions = [makeSession({ id: "s1" }), makeSession({ id: "s2" })]; + render(); + expect(screen.getByText("RESPOND")).toBeInTheDocument(); + expect(screen.getByText("2")).toBeInTheDocument(); + expect(screen.getByText("Agents waiting for your input")).toBeInTheDocument(); + }); + + it("renders nothing when sessions array is empty", () => { + const { container } = render(); + expect(container.firstElementChild).toBeNull(); + }); + + it("shows session cards when not collapsed", () => { + const sessions = [makeSession({ id: "s1" })]; + render(); + // respond is defaultCollapsed: false, so cards should be visible + expect(screen.getByText("s1")).toBeInTheDocument(); + }); + + it("working zone is collapsed by default", () => { + const sessions = [makeSession({ id: "s1" })]; + render(); + // working is defaultCollapsed: true, so session id should not be visible + expect(screen.queryByText("s1")).not.toBeInTheDocument(); + expect(screen.getByText("WORKING")).toBeInTheDocument(); + }); + + it("done zone is collapsed by default", () => { + const sessions = [makeSession({ id: "s1" })]; + render(); + expect(screen.queryByText("s1")).not.toBeInTheDocument(); + expect(screen.getByText("DONE")).toBeInTheDocument(); + }); + + it("toggles collapsed state on click", () => { + const sessions = [makeSession({ id: "s1" })]; + render(); + expect(screen.queryByText("s1")).not.toBeInTheDocument(); + + // Click the zone header to expand + fireEvent.click(screen.getByText("WORKING")); + expect(screen.getByText("s1")).toBeInTheDocument(); + + // Click again to collapse + fireEvent.click(screen.getByText("WORKING")); + expect(screen.queryByText("s1")).not.toBeInTheDocument(); + }); + + it("passes callbacks to SessionCards", () => { + const onRestore = vi.fn(); + const sessions = [makeSession({ id: "s1", activity: "exited" })]; + render(); + fireEvent.click(screen.getByText("restore session")); + expect(onRestore).toHaveBeenCalledWith("s1"); + }); +}); diff --git a/packages/web/src/__tests__/get-attention-level.test.ts b/packages/web/src/__tests__/get-attention-level.test.ts new file mode 100644 index 000000000..0874cb739 --- /dev/null +++ b/packages/web/src/__tests__/get-attention-level.test.ts @@ -0,0 +1,259 @@ +import { describe, it, expect } from "vitest"; +import { getAttentionLevel } from "@/lib/types"; +import { makeSession, makePR } from "./helpers"; + +describe("getAttentionLevel", () => { + // ── MERGE (green zone — PRs ready to merge) ──────────────────────── + + describe("merge zone", () => { + it("returns merge when status is mergeable", () => { + const pr = makePR({ + mergeability: { + mergeable: true, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: [], + }, + }); + const session = makeSession({ status: "mergeable", activity: "idle", pr }); + expect(getAttentionLevel(session)).toBe("merge"); + }); + + it("returns merge when PR mergeability is true", () => { + const pr = makePR({ + mergeability: { + mergeable: true, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: [], + }, + }); + const session = makeSession({ status: "pr_open", activity: "idle", pr }); + expect(getAttentionLevel(session)).toBe("merge"); + }); + }); + + // ── RESPOND (red zone — agent needs human input) ─────────────────── + + describe("respond zone", () => { + it("returns respond when activity is waiting_input", () => { + const session = makeSession({ activity: "waiting_input" }); + expect(getAttentionLevel(session)).toBe("respond"); + }); + + it("returns respond when activity is blocked", () => { + const session = makeSession({ activity: "blocked" }); + expect(getAttentionLevel(session)).toBe("respond"); + }); + + it("returns respond when status is needs_input", () => { + const session = makeSession({ status: "needs_input", activity: "idle" }); + expect(getAttentionLevel(session)).toBe("respond"); + }); + + it("returns respond when status is stuck", () => { + const session = makeSession({ status: "stuck", activity: "idle" }); + expect(getAttentionLevel(session)).toBe("respond"); + }); + + it("returns respond when status is errored", () => { + const session = makeSession({ status: "errored", activity: "idle" }); + expect(getAttentionLevel(session)).toBe("respond"); + }); + + it("returns respond when agent has exited unexpectedly (non-terminal status)", () => { + const session = makeSession({ status: "working", activity: "exited", pr: null }); + expect(getAttentionLevel(session)).toBe("respond"); + }); + + it("merge takes priority over respond (mergeable PR + blocked agent)", () => { + const pr = makePR({ + mergeability: { + mergeable: true, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: [], + }, + }); + const session = makeSession({ status: "mergeable", activity: "blocked", pr }); + expect(getAttentionLevel(session)).toBe("merge"); + }); + }); + + // ── REVIEW (orange zone — needs investigation) ───────────────────── + + describe("review zone", () => { + it("returns review when CI is failing", () => { + const pr = makePR({ + ciStatus: "failing", + ciChecks: [{ name: "test", status: "failed" }], + reviewDecision: "approved", + mergeability: { + mergeable: false, + ciPassing: false, + approved: true, + noConflicts: true, + blockers: ["CI failing"], + }, + }); + const session = makeSession({ status: "ci_failed", activity: "idle", pr }); + expect(getAttentionLevel(session)).toBe("review"); + }); + + it("returns review when changes are requested", () => { + const pr = makePR({ + ciStatus: "passing", + reviewDecision: "changes_requested", + mergeability: { + mergeable: false, + ciPassing: true, + approved: false, + noConflicts: true, + blockers: ["Changes requested"], + }, + }); + const session = makeSession({ status: "changes_requested", activity: "idle", pr }); + expect(getAttentionLevel(session)).toBe("review"); + }); + + it("returns review when there are merge conflicts", () => { + const pr = makePR({ + ciStatus: "passing", + reviewDecision: "approved", + mergeability: { + mergeable: false, + ciPassing: true, + approved: true, + noConflicts: false, + blockers: ["Merge conflict"], + }, + }); + const session = makeSession({ status: "pr_open", activity: "idle", pr }); + expect(getAttentionLevel(session)).toBe("review"); + }); + }); + + // ── PENDING (yellow zone — waiting on external) ──────────────────── + + describe("pending zone", () => { + it("returns pending when review is pending", () => { + const pr = makePR({ + reviewDecision: "pending", + mergeability: { + mergeable: false, + ciPassing: true, + approved: false, + noConflicts: true, + blockers: ["Needs review"], + }, + }); + const session = makeSession({ status: "review_pending", activity: "idle", pr }); + expect(getAttentionLevel(session)).toBe("pending"); + }); + + it("returns pending when review decision is none", () => { + const pr = makePR({ + reviewDecision: "none", + mergeability: { + mergeable: false, + ciPassing: true, + approved: false, + noConflicts: true, + blockers: ["Needs review"], + }, + }); + const session = makeSession({ status: "pr_open", activity: "idle", pr }); + expect(getAttentionLevel(session)).toBe("pending"); + }); + + it("returns pending when there are unresolved threads", () => { + const pr = makePR({ + reviewDecision: "approved", + unresolvedThreads: 2, + unresolvedComments: [ + { url: "https://example.com/1", path: "src/foo.ts", author: "bob", body: "fix this" }, + { url: "https://example.com/2", path: "src/bar.ts", author: "bob", body: "also this" }, + ], + mergeability: { + mergeable: false, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: ["Unresolved comments"], + }, + }); + const session = makeSession({ status: "pr_open", activity: "idle", pr }); + expect(getAttentionLevel(session)).toBe("pending"); + }); + }); + + // ── WORKING (blue zone — agents doing their thing) ───────────────── + + describe("working zone", () => { + it("returns working when actively working with no PR", () => { + const session = makeSession({ status: "working", activity: "active", pr: null }); + expect(getAttentionLevel(session)).toBe("working"); + }); + + it("returns working when spawning", () => { + const session = makeSession({ status: "spawning", activity: "active", pr: null }); + expect(getAttentionLevel(session)).toBe("working"); + }); + + it("returns working for idle session with no PR", () => { + const session = makeSession({ status: "working", activity: "idle", pr: null }); + expect(getAttentionLevel(session)).toBe("working"); + }); + + it("returns working for draft PR with reviewDecision none", () => { + const pr = makePR({ + isDraft: true, + reviewDecision: "none", + mergeability: { + mergeable: false, + ciPassing: true, + approved: false, + noConflicts: true, + blockers: ["Draft PR"], + }, + }); + const session = makeSession({ status: "working", activity: "active", pr }); + expect(getAttentionLevel(session)).toBe("working"); + }); + }); + + // ── DONE (grey zone — archived) ──────────────────────────────────── + + describe("done zone", () => { + it("returns done when PR is merged", () => { + const pr = makePR({ state: "merged" }); + const session = makeSession({ status: "merged", activity: "exited", pr }); + expect(getAttentionLevel(session)).toBe("done"); + }); + + it("returns done when PR is closed (not merged)", () => { + const pr = makePR({ state: "closed" }); + const session = makeSession({ status: "working", activity: "idle", pr }); + expect(getAttentionLevel(session)).toBe("done"); + }); + + it("returns done when session status is merged (even with open PR state)", () => { + const pr = makePR({ state: "merged" }); + const session = makeSession({ status: "merged", activity: "idle", pr }); + expect(getAttentionLevel(session)).toBe("done"); + }); + + it("returns done when session is killed", () => { + const session = makeSession({ status: "killed", activity: "exited", pr: null }); + expect(getAttentionLevel(session)).toBe("done"); + }); + + it("returns done when agent has exited with cleanup status", () => { + const session = makeSession({ status: "cleanup", activity: "exited", pr: null }); + expect(getAttentionLevel(session)).toBe("done"); + }); + }); +}); diff --git a/packages/web/src/__tests__/helpers.ts b/packages/web/src/__tests__/helpers.ts new file mode 100644 index 000000000..2ede98d75 --- /dev/null +++ b/packages/web/src/__tests__/helpers.ts @@ -0,0 +1,52 @@ +import type { DashboardSession, DashboardPR } from "@/lib/types"; + +/** Create a minimal mock session with overrides */ +export function makeSession(overrides: Partial = {}): DashboardSession { + return { + id: "test-1", + projectId: "my-app", + status: "working", + activity: "active", + branch: "feat/test", + issueId: "INT-100", + summary: "Test session", + createdAt: new Date().toISOString(), + lastActivityAt: new Date().toISOString(), + pr: null, + metadata: {}, + ...overrides, + }; +} + +/** Create a minimal mock PR with overrides */ +export function makePR(overrides: Partial = {}): DashboardPR { + return { + number: 100, + url: "https://github.com/acme/app/pull/100", + title: "feat: test PR", + owner: "acme", + repo: "app", + branch: "feat/test", + baseBranch: "main", + isDraft: false, + state: "open", + additions: 50, + deletions: 10, + ciStatus: "passing", + ciChecks: [ + { name: "build", status: "passed" }, + { name: "test", status: "passed" }, + ], + reviewDecision: "approved", + mergeability: { + mergeable: true, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: [], + }, + unresolvedThreads: 0, + unresolvedComments: [], + ...overrides, + }; +} diff --git a/packages/web/src/__tests__/setup.ts b/packages/web/src/__tests__/setup.ts new file mode 100644 index 000000000..f149f27ae --- /dev/null +++ b/packages/web/src/__tests__/setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/packages/web/src/app/api/events/route.ts b/packages/web/src/app/api/events/route.ts new file mode 100644 index 000000000..b537b04f8 --- /dev/null +++ b/packages/web/src/app/api/events/route.ts @@ -0,0 +1,76 @@ +import { mockSessions } from "@/lib/mock-data"; +import { getAttentionLevel } from "@/lib/types"; + +export const dynamic = "force-dynamic"; + +/** + * GET /api/events — SSE stream for real-time lifecycle events + * + * Sends session state updates to connected clients. + * In production, this will be wired to the core EventBus. + */ +export async function GET(): Promise { + const encoder = new TextEncoder(); + let heartbeat: ReturnType | undefined; + let updates: ReturnType | undefined; + + const stream = new ReadableStream({ + start(controller) { + // Send initial state + const initialEvent = { + type: "snapshot", + sessions: mockSessions.map((s) => ({ + id: s.id, + status: s.status, + activity: s.activity, + attentionLevel: getAttentionLevel(s), + lastActivityAt: s.lastActivityAt, + })), + }; + controller.enqueue(encoder.encode(`data: ${JSON.stringify(initialEvent)}\n\n`)); + + // Send periodic heartbeat + heartbeat = setInterval(() => { + try { + controller.enqueue(encoder.encode(`: heartbeat\n\n`)); + } catch { + clearInterval(heartbeat); + clearInterval(updates); + } + }, 15000); + + // Simulate activity updates every 5 seconds + updates = setInterval(() => { + try { + if (mockSessions.length === 0) return; + const randomSession = mockSessions[Math.floor(Math.random() * mockSessions.length)]; + const event = { + type: "session.activity", + sessionId: randomSession.id, + activity: randomSession.activity, + status: randomSession.status, + attentionLevel: getAttentionLevel(randomSession), + timestamp: new Date().toISOString(), + }; + controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`)); + } catch { + clearInterval(updates); + clearInterval(heartbeat); + } + }, 5000); + }, + cancel() { + clearInterval(heartbeat); + clearInterval(updates); + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + "X-Accel-Buffering": "no", + }, + }); +} diff --git a/packages/web/src/app/api/prs/[id]/merge/route.ts b/packages/web/src/app/api/prs/[id]/merge/route.ts new file mode 100644 index 000000000..4069cb63a --- /dev/null +++ b/packages/web/src/app/api/prs/[id]/merge/route.ts @@ -0,0 +1,34 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { mockSessions } from "@/lib/mock-data"; + +/** POST /api/prs/:id/merge — Merge a PR */ +export async function POST(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + if (!/^\d+$/.test(id)) { + return NextResponse.json({ error: "Invalid PR number" }, { status: 400 }); + } + const prNumber = Number(id); + + const session = mockSessions.find((s) => s.pr?.number === prNumber); + if (!session?.pr) { + return NextResponse.json({ error: "PR not found" }, { status: 404 }); + } + + if (session.pr.state !== "open") { + return NextResponse.json({ error: `PR is ${session.pr.state}, not open` }, { status: 409 }); + } + + if (session.pr.isDraft) { + return NextResponse.json({ error: "Cannot merge a draft PR" }, { status: 422 }); + } + + if (!session.pr.mergeability.mergeable) { + return NextResponse.json( + { error: "PR is not mergeable", blockers: session.pr.mergeability.blockers }, + { status: 422 }, + ); + } + + // TODO: wire to core SCM.mergePR() + return NextResponse.json({ ok: true, prNumber, method: "squash" }); +} diff --git a/packages/web/src/app/api/sessions/[id]/kill/route.ts b/packages/web/src/app/api/sessions/[id]/kill/route.ts new file mode 100644 index 000000000..333a61674 --- /dev/null +++ b/packages/web/src/app/api/sessions/[id]/kill/route.ts @@ -0,0 +1,20 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { getMockSession } from "@/lib/mock-data"; +import { validateIdentifier } from "@/lib/validation"; + +/** POST /api/sessions/:id/kill — Kill a session */ +export async function POST(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const idErr = validateIdentifier(id, "id"); + if (idErr) { + return NextResponse.json({ error: idErr }, { status: 400 }); + } + + const session = getMockSession(id); + if (!session) { + return NextResponse.json({ error: "Session not found" }, { status: 404 }); + } + + // TODO: wire to core SessionManager.kill() + return NextResponse.json({ ok: true, sessionId: id }); +} diff --git a/packages/web/src/app/api/sessions/[id]/restore/route.ts b/packages/web/src/app/api/sessions/[id]/restore/route.ts new file mode 100644 index 000000000..f7fe63ff4 --- /dev/null +++ b/packages/web/src/app/api/sessions/[id]/restore/route.ts @@ -0,0 +1,38 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { getMockSession } from "@/lib/mock-data"; +import { validateIdentifier } from "@/lib/validation"; + +/** Terminal states that can be restored */ +const RESTORABLE_STATUSES = new Set(["killed", "cleanup"]); +const RESTORABLE_ACTIVITIES = new Set(["exited"]); + +/** Statuses that must never be restored (e.g. already merged) */ +const NON_RESTORABLE_STATUSES = new Set(["merged"]); + +/** POST /api/sessions/:id/restore — Restore a terminated session */ +export async function POST(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const idErr = validateIdentifier(id, "id"); + if (idErr) { + return NextResponse.json({ error: idErr }, { status: 400 }); + } + + const session = getMockSession(id); + if (!session) { + return NextResponse.json({ error: "Session not found" }, { status: 404 }); + } + + if (NON_RESTORABLE_STATUSES.has(session.status)) { + return NextResponse.json({ error: "Cannot restore a merged session" }, { status: 409 }); + } + + const isTerminal = + RESTORABLE_STATUSES.has(session.status) || RESTORABLE_ACTIVITIES.has(session.activity); + + if (!isTerminal) { + return NextResponse.json({ error: "Session is not in a terminal state" }, { status: 409 }); + } + + // TODO: wire to core SessionManager.restore() + return NextResponse.json({ ok: true, sessionId: id }); +} diff --git a/packages/web/src/app/api/sessions/[id]/send/route.ts b/packages/web/src/app/api/sessions/[id]/send/route.ts new file mode 100644 index 000000000..9eb7db6db --- /dev/null +++ b/packages/web/src/app/api/sessions/[id]/send/route.ts @@ -0,0 +1,39 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { getMockSession } from "@/lib/mock-data"; +import { validateIdentifier, validateString, stripControlChars } from "@/lib/validation"; + +const MAX_MESSAGE_LENGTH = 10_000; + +/** POST /api/sessions/:id/send — Send a message to a session */ +export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const idErr = validateIdentifier(id, "id"); + if (idErr) { + return NextResponse.json({ error: idErr }, { status: 400 }); + } + + const session = getMockSession(id); + if (!session) { + return NextResponse.json({ error: "Session not found" }, { status: 404 }); + } + + const body = (await request.json().catch(() => null)) as Record | null; + const messageErr = validateString(body?.message, "message", MAX_MESSAGE_LENGTH); + if (messageErr) { + return NextResponse.json({ error: messageErr }, { status: 400 }); + } + + // Strip control characters to prevent injection when passed to shell-based runtimes + const message = stripControlChars(String(body?.message ?? "")); + + // Re-validate after stripping — a control-char-only message becomes empty + if (message.trim().length === 0) { + return NextResponse.json( + { error: "message must not be empty after sanitization" }, + { status: 400 }, + ); + } + + // TODO: wire to core SessionManager.send() + return NextResponse.json({ ok: true, sessionId: id, message }); +} diff --git a/packages/web/src/app/api/sessions/route.ts b/packages/web/src/app/api/sessions/route.ts new file mode 100644 index 000000000..7f5cde601 --- /dev/null +++ b/packages/web/src/app/api/sessions/route.ts @@ -0,0 +1,10 @@ +import { NextResponse } from "next/server"; +import { mockSessions, getMockStats } from "@/lib/mock-data"; + +/** GET /api/sessions — List all sessions with full state */ +export async function GET() { + return NextResponse.json({ + sessions: mockSessions, + stats: getMockStats(), + }); +} diff --git a/packages/web/src/app/api/spawn/route.ts b/packages/web/src/app/api/spawn/route.ts new file mode 100644 index 000000000..787490150 --- /dev/null +++ b/packages/web/src/app/api/spawn/route.ts @@ -0,0 +1,39 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { validateIdentifier } from "@/lib/validation"; + +/** POST /api/spawn — Spawn a new session */ +export async function POST(request: NextRequest) { + const body = (await request.json().catch(() => null)) as Record | null; + if (!body) { + return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); + } + + const projectErr = validateIdentifier(body.projectId, "projectId"); + if (projectErr) { + return NextResponse.json({ error: projectErr }, { status: 400 }); + } + + if (body.issueId !== undefined && body.issueId !== null) { + const issueErr = validateIdentifier(body.issueId, "issueId"); + if (issueErr) { + return NextResponse.json({ error: issueErr }, { status: 400 }); + } + } + + // TODO: wire to core SessionManager.spawn() + const mockSession = { + id: `session-${Date.now()}`, + projectId: body.projectId as string, + issueId: (body.issueId as string) ?? null, + status: "spawning", + activity: "active", + branch: null, + summary: `Spawning session for ${(body.issueId as string) ?? body.projectId}`, + createdAt: new Date().toISOString(), + lastActivityAt: new Date().toISOString(), + pr: null, + metadata: {}, + }; + + return NextResponse.json({ session: mockSession }, { status: 201 }); +} diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css new file mode 100644 index 000000000..f17157b3f --- /dev/null +++ b/packages/web/src/app/globals.css @@ -0,0 +1,46 @@ +@import "tailwindcss"; + +@theme { + --color-bg-primary: #0d1117; + --color-bg-secondary: #161b22; + --color-bg-tertiary: #1c2128; + --color-border-default: #30363d; + --color-border-muted: #21262d; + --color-border-emphasis: #484f58; + --color-text-primary: #e6edf3; + --color-text-secondary: #7d8590; + --color-text-muted: #484f58; + --color-accent-blue: #58a6ff; + --color-accent-purple: #bc8cff; + --color-accent-green: #3fb950; + --color-accent-yellow: #d29922; + --color-accent-orange: #d18616; + --color-accent-red: #f85149; + --color-accent-violet: #a371f7; + --font-mono: "SF Mono", "Menlo", "Consolas", monospace; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "SF Pro", system-ui, sans-serif; + background: var(--color-bg-primary); + color: var(--color-text-primary); + min-height: 100vh; +} + +a { + color: var(--color-accent-blue); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} + +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.4; + } +} diff --git a/packages/web/src/app/layout.tsx b/packages/web/src/app/layout.tsx index a095d6d31..a5f4369a5 100644 --- a/packages/web/src/app/layout.tsx +++ b/packages/web/src/app/layout.tsx @@ -1,8 +1,17 @@ -// Web dashboard layout — to be implemented +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Agent Orchestrator", + description: "Dashboard for managing parallel AI coding agents", +}; + export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - {children} + + + {children} + ); } diff --git a/packages/web/src/app/page.tsx b/packages/web/src/app/page.tsx index 2080bb04c..37adc554a 100644 --- a/packages/web/src/app/page.tsx +++ b/packages/web/src/app/page.tsx @@ -1,4 +1,6 @@ -// Dashboard home — to be implemented +import { Dashboard } from "@/components/Dashboard"; +import { mockSessions, getMockStats } from "@/lib/mock-data"; + export default function Home() { - return
Agent Orchestrator Dashboard
; + return ; } diff --git a/packages/web/src/app/sessions/[id]/page.tsx b/packages/web/src/app/sessions/[id]/page.tsx new file mode 100644 index 000000000..9105b456c --- /dev/null +++ b/packages/web/src/app/sessions/[id]/page.tsx @@ -0,0 +1,18 @@ +import { notFound } from "next/navigation"; +import { getMockSession } from "@/lib/mock-data"; +import { SessionDetail } from "@/components/SessionDetail"; + +interface Props { + params: Promise<{ id: string }>; +} + +export default async function SessionPage({ params }: Props) { + const { id } = await params; + const session = getMockSession(id); + + if (!session) { + notFound(); + } + + return ; +} diff --git a/packages/web/src/components/AttentionZone.tsx b/packages/web/src/components/AttentionZone.tsx new file mode 100644 index 000000000..ba0a2b66c --- /dev/null +++ b/packages/web/src/components/AttentionZone.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { useState } from "react"; +import type { DashboardSession, AttentionLevel } from "@/lib/types"; +import { SessionCard } from "./SessionCard"; + +interface AttentionZoneProps { + level: AttentionLevel; + sessions: DashboardSession[]; + onSend?: (sessionId: string, message: string) => void; + onKill?: (sessionId: string) => void; + onMerge?: (prNumber: number) => void; + onRestore?: (sessionId: string) => void; +} + +const zoneConfig: Record< + AttentionLevel, + { + label: string; + description: string; + color: string; + defaultCollapsed: boolean; + } +> = { + merge: { + label: "MERGE", + description: "PRs ready to merge", + color: "var(--color-accent-green)", + defaultCollapsed: false, + }, + respond: { + label: "RESPOND", + description: "Agents waiting for your input", + color: "var(--color-accent-red)", + defaultCollapsed: false, + }, + review: { + label: "REVIEW", + description: "CI failures, changes requested, conflicts", + color: "var(--color-accent-orange)", + defaultCollapsed: false, + }, + pending: { + label: "PENDING", + description: "Waiting on reviewer or CI", + color: "var(--color-accent-yellow)", + defaultCollapsed: false, + }, + working: { + label: "WORKING", + description: "Agents working normally", + color: "var(--color-accent-blue)", + defaultCollapsed: true, + }, + done: { + label: "DONE", + description: "Merged or terminated", + color: "var(--color-text-muted)", + defaultCollapsed: true, + }, +}; + +export function AttentionZone({ + level, + sessions, + onSend, + onKill, + onMerge, + onRestore, +}: AttentionZoneProps) { + const config = zoneConfig[level]; + const [collapsed, setCollapsed] = useState(config.defaultCollapsed); + + if (sessions.length === 0) return null; + + return ( +
+ + + {!collapsed && ( +
+ {sessions.map((session) => ( + + ))} +
+ )} +
+ ); +} diff --git a/packages/web/src/components/CIBadge.tsx b/packages/web/src/components/CIBadge.tsx new file mode 100644 index 000000000..e8a056b9f --- /dev/null +++ b/packages/web/src/components/CIBadge.tsx @@ -0,0 +1,176 @@ +"use client"; + +import type { CIStatus, DashboardCICheck } from "@/lib/types"; + +interface CIBadgeProps { + status: CIStatus; + checks?: DashboardCICheck[]; + compact?: boolean; +} + +const statusConfig: Record = { + passing: { + label: "CI passing", + className: "bg-[rgba(63,185,80,0.1)] text-[var(--color-accent-green)]", + icon: "\u2713", + }, + failing: { + label: "CI failing", + className: "bg-[rgba(248,81,73,0.15)] text-[var(--color-accent-red)]", + icon: "\u2717", + }, + pending: { + label: "CI pending", + className: "bg-[rgba(210,153,34,0.1)] text-[var(--color-accent-yellow)]", + icon: "\u25CF", + }, + none: { + label: "\u2014", + className: "text-[var(--color-text-muted)]", + icon: "", + }, +}; + +export function CIBadge({ status, checks, compact }: CIBadgeProps) { + const config = statusConfig[status]; + const failedCount = checks?.filter((c) => c.status === "failed").length ?? 0; + + if (status === "none") { + return ; + } + + const label = + status === "failing" && failedCount > 0 + ? `${failedCount} check${failedCount > 1 ? "s" : ""} failing` + : config.label; + + return ( + + {!compact && {config.icon}} + {label} + + ); +} + +interface CICheckListProps { + checks: DashboardCICheck[]; + /** "vertical" (default): stacked list. "inline": horizontal wrap. "expanded": stacked with linked names. */ + layout?: "vertical" | "inline" | "expanded"; +} + +export const checkStatusIcon: Record = { + passed: { icon: "\u2713", color: "var(--color-accent-green)" }, + failed: { icon: "\u2717", color: "var(--color-accent-red)" }, + running: { icon: "\u25CF", color: "var(--color-accent-yellow)" }, + pending: { icon: "\u25CB", color: "var(--color-text-muted)" }, + skipped: { icon: "\u25CB", color: "var(--color-text-muted)" }, +}; + +/** Sort order for CI checks: failures first, then running, pending, passed, skipped. */ +export const ciCheckSortOrder: Record = { + failed: 0, + running: 1, + pending: 2, + passed: 3, + skipped: 4, +}; + +export function CICheckList({ checks, layout = "vertical" }: CICheckListProps) { + const sorted = [...checks].sort((a, b) => ciCheckSortOrder[a.status] - ciCheckSortOrder[b.status]); + + if (layout === "inline") { + return ( +
+ {sorted.map((check) => { + const { icon, color } = checkStatusIcon[check.status]; + const inner = ( + + {icon} + {check.name} + + ); + return check.url ? ( + + {inner} + + ) : ( + {inner} + ); + })} +
+ ); + } + + if (layout === "expanded") { + return ( +
+ {sorted.map((check) => { + const { icon, color } = checkStatusIcon[check.status]; + const inner = ( + + {icon} + {check.name} + + ); + return ( +
+ {check.url ? ( + + {inner} + + ) : ( + inner + )} + {check.url && ( + + view + + )} +
+ ); + })} +
+ ); + } + + return ( +
+ {sorted.map((check) => { + const { icon, color } = checkStatusIcon[check.status]; + return ( +
+ + {icon} + + + {check.name} + + {check.url && ( + + view + + )} +
+ ); + })} +
+ ); +} diff --git a/packages/web/src/components/Dashboard.tsx b/packages/web/src/components/Dashboard.tsx new file mode 100644 index 000000000..0b9aeecb6 --- /dev/null +++ b/packages/web/src/components/Dashboard.tsx @@ -0,0 +1,191 @@ +"use client"; + +import { useMemo, useState, useEffect } from "react"; +import { + type DashboardSession, + type DashboardStats, + type DashboardPR, + type AttentionLevel, + getAttentionLevel, +} from "@/lib/types"; +import { AttentionZone } from "./AttentionZone"; +import { PRTableRow } from "./PRStatus"; + +interface DashboardProps { + sessions: DashboardSession[]; + stats: DashboardStats; +} + +export function Dashboard({ sessions, stats }: DashboardProps) { + const grouped = useMemo(() => { + const zones: Record = { + merge: [], + respond: [], + review: [], + pending: [], + working: [], + done: [], + }; + for (const session of sessions) { + zones[getAttentionLevel(session)].push(session); + } + return zones; + }, [sessions]); + + const openPRs = useMemo(() => { + return sessions + .filter((s): s is DashboardSession & { pr: DashboardPR } => s.pr?.state === "open") + .map((s) => s.pr) + .sort((a, b) => mergeScore(a) - mergeScore(b)); + }, [sessions]); + + const handleSend = async (sessionId: string, message: string) => { + const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/send`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ message }), + }); + if (!res.ok) { + console.error(`Failed to send message to ${sessionId}:`, await res.text()); + } + }; + + const handleKill = async (sessionId: string) => { + if (!confirm(`Kill session ${sessionId}?`)) return; + const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/kill`, { + method: "POST", + }); + if (!res.ok) { + console.error(`Failed to kill ${sessionId}:`, await res.text()); + } + }; + + const handleMerge = async (prNumber: number) => { + if (!confirm(`Merge PR #${prNumber}?`)) return; + const res = await fetch(`/api/prs/${prNumber}/merge`, { method: "POST" }); + if (!res.ok) { + console.error(`Failed to merge PR #${prNumber}:`, await res.text()); + } + }; + + const handleRestore = async (sessionId: string) => { + if (!confirm(`Restore session ${sessionId}?`)) return; + const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/restore`, { + method: "POST", + }); + if (!res.ok) { + console.error(`Failed to restore ${sessionId}:`, await res.text()); + } + }; + + return ( +
+ {/* Header */} +
+

+ Agent Orchestrator +

+ +
+ + {/* Stats bar */} +
+ + + + +
+ + {/* Attention zones */} +
+

+ Sessions +

+ {(["merge", "respond", "review", "pending", "working", "done"] as AttentionLevel[]).map((level) => ( + + ))} +
+ + {/* PR Table */} + {openPRs.length > 0 && ( +
+

+ Pull Requests +

+
+ + + + + + + + + + + + + {openPRs.map((pr) => ( + + ))} + +
+ PR + + Title + + Size + + CI + + Review + + Unresolved +
+
+
+ )} +
+ ); +} + +/** Renders timestamp client-side only to avoid hydration mismatch. */ +function ClientTimestamp() { + const [time, setTime] = useState(""); + useEffect(() => { + setTime(new Date().toLocaleString()); + }, []); + return {time}; +} + +function Stat({ value, label, color }: { value: number; label: string; color: string }) { + return ( +
+ + {value} + + {label} +
+ ); +} + +function mergeScore( + pr: Pick, +): number { + let score = 0; + if (!pr.mergeability.noConflicts) score += 40; + if (pr.ciStatus === "failing") score += 30; + else if (pr.ciStatus === "pending") score += 5; + if (pr.reviewDecision === "changes_requested") score += 20; + else if (pr.reviewDecision !== "approved") score += 10; + score += pr.unresolvedThreads * 5; + return score; +} diff --git a/packages/web/src/components/PRStatus.tsx b/packages/web/src/components/PRStatus.tsx new file mode 100644 index 000000000..662396eed --- /dev/null +++ b/packages/web/src/components/PRStatus.tsx @@ -0,0 +1,112 @@ +"use client"; + +import type { DashboardPR } from "@/lib/types"; +import { CIBadge } from "./CIBadge"; + +function getSizeLabel(additions: number, deletions: number): string { + const size = additions + deletions; + return size > 1000 ? "XL" : size > 500 ? "L" : size > 200 ? "M" : size > 50 ? "S" : "XS"; +} + +interface PRStatusProps { + pr: DashboardPR; +} + +export function PRStatus({ pr }: PRStatusProps) { + const sizeLabel = getSizeLabel(pr.additions, pr.deletions); + + return ( +
+ {/* PR number */} + e.stopPropagation()} + > + #{pr.number} + + + {/* Size */} + + +{pr.additions} -{pr.deletions} {sizeLabel} + + + {/* Merged badge */} + {pr.state === "merged" && ( + + merged + + )} + + {/* Draft badge */} + {pr.isDraft && pr.state === "open" && ( + + draft + + )} + + {/* CI status (only for open PRs) */} + {pr.state === "open" && !pr.isDraft && } + + {/* Review decision (only for open PRs) */} + {pr.state === "open" && pr.reviewDecision === "approved" && ( + + approved + + )} +
+ ); +} + +interface PRTableRowProps { + pr: DashboardPR; +} + +export function PRTableRow({ pr }: PRTableRowProps) { + const sizeLabel = getSizeLabel(pr.additions, pr.deletions); + + const reviewLabel = + pr.isDraft + ? "draft" + : pr.reviewDecision === "approved" + ? "approved" + : pr.reviewDecision === "changes_requested" + ? "changes requested" + : "needs review"; + + const reviewClass = + pr.isDraft + ? "text-[var(--color-text-muted)]" + : pr.reviewDecision === "approved" + ? "text-[var(--color-accent-green)]" + : pr.reviewDecision === "changes_requested" + ? "text-[var(--color-accent-red)]" + : "text-[var(--color-accent-yellow)]"; + + return ( + + + + #{pr.number} + + + {pr.title} + + +{pr.additions}{" "} + -{pr.deletions}{" "} + {sizeLabel} + + + + + {reviewLabel} + 0 ? "text-[var(--color-accent-red)]" : "text-[var(--color-border-default)]"}`} + > + {pr.unresolvedThreads} + + + ); +} diff --git a/packages/web/src/components/SessionCard.tsx b/packages/web/src/components/SessionCard.tsx new file mode 100644 index 000000000..6f4bab433 --- /dev/null +++ b/packages/web/src/components/SessionCard.tsx @@ -0,0 +1,369 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { type DashboardSession, type AttentionLevel, getAttentionLevel } from "@/lib/types"; +import { cn } from "@/lib/cn"; +import { PRStatus } from "./PRStatus"; +import { CICheckList } from "./CIBadge"; + +interface SessionCardProps { + session: DashboardSession; + onSend?: (sessionId: string, message: string) => void; + onKill?: (sessionId: string) => void; + onMerge?: (prNumber: number) => void; + onRestore?: (sessionId: string) => void; +} + +const activityIcon: Record = { + active: "\u26A1", + idle: "\uD83D\uDCA4", + waiting_input: "\u2753", + blocked: "\uD83D\uDEA7", + exited: "\uD83D\uDC80", +}; + +const borderColorByLevel: Record = { + merge: "border-l-[var(--color-accent-green)]", + respond: "border-l-[var(--color-accent-red)]", + review: "border-l-[var(--color-accent-orange)]", + pending: "border-l-[var(--color-accent-yellow)]", + working: "border-l-[var(--color-accent-blue)]", + done: "border-l-[var(--color-border-default)]", +}; + +export function SessionCard({ session, onSend, onKill, onMerge, onRestore }: SessionCardProps) { + const [expanded, setExpanded] = useState(false); + const [sendingAction, setSendingAction] = useState(null); + const timerRef = useRef | null>(null); + const level = getAttentionLevel(session); + const pr = session.pr; + + useEffect(() => { + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, []); + + const handleAction = async (action: string, message: string) => { + setSendingAction(action); + onSend?.(session.id, message); + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => setSendingAction(null), 2000); + }; + + const alerts = getAlerts(session); + const isReadyToMerge = pr?.mergeability.mergeable && pr.state === "open"; + const isRestorable = + (session.status === "killed" || + session.status === "cleanup" || + session.activity === "exited") && + session.status !== "merged"; + + return ( +
{ + if ((e.target as HTMLElement).closest("a, button, textarea")) return; + setExpanded(!expanded); + }} + > + {/* Top row */} +
+ + {activityIcon[session.activity] ?? "\u2753"} + + + {session.id} + + + {pr?.title ?? session.summary ?? session.status} + + {isRestorable && ( + + )} + e.stopPropagation()} + className="shrink-0 rounded-md border border-[var(--color-border-default)] px-2.5 py-0.5 text-[11px] text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-accent-blue)] hover:text-[var(--color-accent-blue)]" + > + terminal + +
+ + {/* Meta row: branch + PR pills */} +
+ {session.branch && ( + + {session.branch} + + )} + {session.branch && pr && ( + · + )} + {pr && } +
+ + {/* Alert tags */} + {(alerts.length > 0 || isReadyToMerge) && ( +
+ {isReadyToMerge && pr ? ( + + ) : ( + alerts.map((alert) => ( + + e.stopPropagation()} + className={cn( + "inline-flex items-center gap-1.5 rounded-md border px-3 py-0.5 text-xs font-bold hover:brightness-125", + alert.className, + )} + > + {alert.count !== undefined && ( + {alert.count} + )} + {alert.label} + + {alert.actionLabel && session.activity !== "active" && ( + + )} + + )) + )} +
+ )} + + {/* Expandable detail panel */} + {expanded && ( +
+ {session.summary && pr?.title && session.summary !== pr.title && ( + +

+ {session.summary} +

+
+ )} + + {session.issueId && ( + + {session.issueId} + + )} + + {pr && pr.ciChecks.length > 0 && ( + + + + )} + + {pr && pr.unresolvedComments.length > 0 && ( + +
+ {pr.unresolvedComments.map((c) => ( +
+ + {"\u25CF"} + + + {c.path} + + + go to comment + +
+ ))} +
+
+ )} + + {pr && ( + +

+ + {pr.title} + +
+ +{pr.additions}{" "} + -{pr.deletions} + {" \u00B7 "}mergeable: {pr.mergeability.mergeable ? "yes" : "no"} + {" \u00B7 "}review: {pr.reviewDecision} +

+
+ )} + + {!pr && ( +

+ No PR associated with this session. +

+ )} + +
+ {isRestorable && ( + + )} + {!isRestorable && session.status !== "merged" && ( + + )} +
+
+ )} +
+ ); +} + +function DetailSection({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+
+ {label} +
+ {children} +
+ ); +} + +interface Alert { + key: string; + label: string; + className: string; + url: string; + count?: number; + actionLabel?: string; + actionMessage?: string; +} + +function getAlerts(session: DashboardSession): Alert[] { + const pr = session.pr; + if (!pr || pr.state !== "open") return []; + + const alerts: Alert[] = []; + + // CI failing + if (pr.ciStatus === "failing") { + const failedCheck = pr.ciChecks.find((c) => c.status === "failed"); + const failCount = pr.ciChecks.filter((c) => c.status === "failed").length; + alerts.push({ + key: "ci-fail", + label: `${failCount} CI check${failCount > 1 ? "s" : ""} failing`, + className: + "border-[rgba(248,81,73,0.3)] bg-[rgba(248,81,73,0.15)] text-[var(--color-accent-red)]", + url: failedCheck?.url ?? pr.url + "/checks", + actionLabel: "ask to fix CI", + actionMessage: `Please fix the failing CI checks on ${pr.url}`, + }); + } + + // Changes requested + if (pr.reviewDecision === "changes_requested") { + alerts.push({ + key: "changes", + label: "changes requested", + className: + "border-[rgba(248,81,73,0.3)] bg-[rgba(248,81,73,0.15)] text-[var(--color-accent-red)]", + url: pr.url, + }); + } else if ( + !pr.isDraft && + (pr.reviewDecision === "pending" || pr.reviewDecision === "none") + ) { + alerts.push({ + key: "review", + label: "needs review", + className: + "border-[rgba(210,153,34,0.3)] bg-[rgba(210,153,34,0.15)] text-[var(--color-accent-yellow)]", + url: pr.url, + actionLabel: "ask to post for review", + actionMessage: `Post ${pr.url} on slack asking for a review.`, + }); + } + + // Merge conflict + if (!pr.mergeability.noConflicts) { + alerts.push({ + key: "conflict", + label: "merge conflict", + className: + "border-[rgba(248,81,73,0.3)] bg-[rgba(248,81,73,0.15)] text-[var(--color-accent-red)]", + url: pr.url, + actionLabel: "ask to fix conflicts", + actionMessage: `Please resolve the merge conflicts on ${pr.url} by rebasing on the base branch`, + }); + } + + // Unresolved comments + if (pr.unresolvedThreads > 0) { + const firstUrl = pr.unresolvedComments[0]?.url ?? pr.url + "/files"; + alerts.push({ + key: "comments", + label: "unresolved comments", + count: pr.unresolvedThreads, + className: + "border-[rgba(248,81,73,0.3)] bg-[rgba(248,81,73,0.15)] text-[var(--color-accent-red)]", + url: firstUrl, + actionLabel: "ask to resolve", + actionMessage: `Please address all unresolved review comments on ${pr.url}`, + }); + } + + return alerts; +} diff --git a/packages/web/src/components/SessionDetail.tsx b/packages/web/src/components/SessionDetail.tsx new file mode 100644 index 000000000..70df53e54 --- /dev/null +++ b/packages/web/src/components/SessionDetail.tsx @@ -0,0 +1,480 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + type DashboardSession, + type DashboardPR, + getAttentionLevel, +} from "@/lib/types"; +import { CICheckList } from "./CIBadge"; +import { Terminal } from "./Terminal"; + +interface SessionDetailProps { + session: DashboardSession; +} + +// ── Helpers ────────────────────────────────────────────────────────── + +const activityLabel: Record = { + active: { label: "Active", color: "var(--color-accent-green)" }, + idle: { label: "Idle", color: "var(--color-text-muted)" }, + waiting_input: { label: "Waiting for input", color: "var(--color-accent-yellow)" }, + blocked: { label: "Blocked", color: "var(--color-accent-red)" }, + exited: { label: "Exited", color: "var(--color-accent-red)" }, +}; + +function levelColor(level: string): string { + switch (level) { + case "merge": + return "var(--color-accent-green)"; + case "respond": + return "var(--color-accent-red)"; + case "review": + return "var(--color-accent-orange)"; + case "pending": + return "var(--color-accent-yellow)"; + case "working": + return "var(--color-accent-blue)"; + default: + return "var(--color-text-muted)"; + } +} + +/** Converts snake_case status enum to Title Case display string. */ +function humanizeStatus(status: string): string { + return status + .replace(/_/g, " ") + .replace(/\bci\b/gi, "CI") + .replace(/\bpr\b/gi, "PR") + .replace(/\b\w/g, (c) => c.toUpperCase()); +} + +/** Converts ISO date string to relative time like "3h ago", "2m ago". Client-side only. */ +function relativeTime(iso: string): string { + const diff = Date.now() - new Date(iso).getTime(); + const seconds = Math.floor(diff / 1000); + if (seconds < 60) return "just now"; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +/** Builds a GitHub branch URL from PR owner/repo/branch. */ +function buildGitHubBranchUrl(pr: DashboardPR): string { + return `https://github.com/${pr.owner}/${pr.repo}/tree/${pr.branch}`; +} + +/** Builds a GitHub repo URL from PR owner/repo. */ +function buildGitHubRepoUrl(pr: DashboardPR): string { + return `https://github.com/${pr.owner}/${pr.repo}`; +} + +// ── Main Component ─────────────────────────────────────────────────── + +export function SessionDetail({ session }: SessionDetailProps) { + const pr = session.pr; + const level = getAttentionLevel(session); + const activity = activityLabel[session.activity] ?? { + label: session.activity, + color: "var(--color-text-muted)", + }; + + return ( +
+ {/* Nav bar */} + + +
+ {/* ── Header ─────────────────────────────────────────────── */} +
+ {/* Session ID + badges */} +
+

{session.id}

+ + {activity.label} + + + {level} + +
+ + {/* Summary */} + {session.summary && ( +

{session.summary}

+ )} + + {/* Meta chips: project · branch · issue */} +
+ {pr ? ( + + {session.projectId} + + ) : ( + + {session.projectId} + + )} + + {session.branch && ( + <> + · + {pr ? ( + + {session.branch} + + ) : ( + + {session.branch} + + )} + + )} + + {session.issueId && ( + <> + · + + {session.issueId} + + + )} +
+ + {/* Status · timestamps */} + +
+ + {/* ── PR Card ────────────────────────────────────────────── */} + {pr && } + + {/* ── Terminal ───────────────────────────────────────────── */} +
+

+ Terminal +

+ +
+
+
+ ); +} + +// ── Client-side timestamps (avoids hydration mismatch) ─────────────── + +function ClientTimestamps({ + status, + createdAt, + lastActivityAt, +}: { + status: string; + createdAt: string; + lastActivityAt: string; +}) { + const [created, setCreated] = useState(null); + const [lastActive, setLastActive] = useState(null); + + useEffect(() => { + setCreated(relativeTime(createdAt)); + setLastActive(relativeTime(lastActivityAt)); + }, [createdAt, lastActivityAt]); + + return ( +
+ {humanizeStatus(status)} + {created && ( + <> + · + Created {created} + + )} + {lastActive && ( + <> + · + Active {lastActive} + + )} +
+ ); +} + +// ── PR Card ────────────────────────────────────────────────────────── + +function PRCard({ pr }: { pr: DashboardPR }) { + const allGreen = + pr.mergeability.mergeable && + pr.mergeability.ciPassing && + pr.mergeability.approved && + pr.mergeability.noConflicts; + + const failedChecks = pr.ciChecks.filter((c) => c.status === "failed"); + const hasFailures = failedChecks.length > 0; + + return ( +
+ {/* Title row */} +
+ + PR #{pr.number}: {pr.title} + + + {/* Stats row */} +
+ + +{pr.additions}{" "} + -{pr.deletions} + + + {pr.isDraft && ( + <> + · + Draft + + )} + + {pr.state === "merged" && ( + <> + · + Merged + + )} + + {pr.state === "open" && ( + <> + · + + · + + + )} +
+
+ + {/* Body */} +
+ {/* Ready to merge or issues list */} + {allGreen ? ( +
+ {"\u2713"} + Ready to merge +
+ ) : ( + + )} + + {/* CI Checks — inline row */} + {pr.ciChecks.length > 0 && ( +
+ +
+ )} + + {/* Unresolved Comments */} + {pr.unresolvedComments.length > 0 && ( +
+

+ Unresolved Comments ({pr.unresolvedThreads}) +

+
+ {pr.unresolvedComments.map((c) => ( +
+
+ + {c.author} + + on + + {c.path} + + + view → + +
+

+ {c.body} +

+
+ ))} +
+
+ )} +
+
+ ); +} + +// ── Issues List (replaces merge readiness grid + blockers) ─────────── + +function IssuesList({ pr }: { pr: DashboardPR }) { + const issues: Array<{ icon: string; color: string; text: string }> = []; + + if (pr.ciStatus === "failing") { + const failCount = pr.ciChecks.filter((c) => c.status === "failed").length; + issues.push({ + icon: "\u2717", + color: "var(--color-accent-red)", + text: `CI failing \u2014 ${failCount} check${failCount !== 1 ? "s" : ""} failed`, + }); + } else if (pr.ciStatus === "pending") { + issues.push({ + icon: "\u25CF", + color: "var(--color-accent-yellow)", + text: "CI pending", + }); + } + + if (pr.reviewDecision === "changes_requested") { + issues.push({ + icon: "\u2717", + color: "var(--color-accent-red)", + text: "Changes requested", + }); + } else if (!pr.mergeability.approved) { + issues.push({ + icon: "\u25CB", + color: "var(--color-text-muted)", + text: "Not approved \u2014 awaiting reviewer", + }); + } + + if (!pr.mergeability.noConflicts) { + issues.push({ + icon: "\u2717", + color: "var(--color-accent-red)", + text: "Merge conflicts", + }); + } + + if (!pr.mergeability.mergeable && issues.length === 0) { + issues.push({ + icon: "\u25CB", + color: "var(--color-text-muted)", + text: "Not mergeable", + }); + } + + if (pr.unresolvedThreads > 0) { + issues.push({ + icon: "\u25CF", + color: "var(--color-accent-yellow)", + text: `${pr.unresolvedThreads} unresolved comment${pr.unresolvedThreads !== 1 ? "s" : ""}`, + }); + } + + if (pr.isDraft) { + issues.push({ + icon: "\u25CB", + color: "var(--color-text-muted)", + text: "Draft PR", + }); + } + + if (issues.length === 0) return null; + + return ( +
+

+ Issues +

+ {issues.map((issue) => ( +
+ {issue.icon} + {issue.text} +
+ ))} +
+ ); +} + +// ── Inline status pills ────────────────────────────────────────────── + +function CIStatusInline({ status, failedCount }: { status: string; failedCount: number }) { + if (status === "passing") { + return {"\u2713"} CI passing; + } + if (status === "failing") { + return ( + + {"\u2717"} {failedCount} check{failedCount !== 1 ? "s" : ""} failing + + ); + } + if (status === "pending") { + return {"\u25CF"} CI pending; + } + return null; +} + +function ReviewStatusInline({ decision }: { decision: string }) { + if (decision === "approved") { + return {"\u2713"} Approved; + } + if (decision === "changes_requested") { + return ( + {"\u2717"} Changes requested + ); + } + if (decision === "pending") { + return ( + + {"\u23F3"} Pending review + + ); + } + return No review; +} diff --git a/packages/web/src/components/Terminal.tsx b/packages/web/src/components/Terminal.tsx new file mode 100644 index 000000000..49f58e1cc --- /dev/null +++ b/packages/web/src/components/Terminal.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useState } from "react"; +import { cn } from "@/lib/cn"; + +interface TerminalProps { + sessionId: string; +} + +/** + * Terminal embed placeholder. + * Future: integrate xterm.js via the terminal-web plugin. + */ +export function Terminal({ sessionId }: TerminalProps) { + const [fullscreen, setFullscreen] = useState(false); + + return ( +
+
+
+
+
+
+
+ + {sessionId} + + +
+
+
+

Terminal embed

+

+ xterm.js integration coming soon +

+
+
+
+ ); +} diff --git a/packages/web/src/lib/cn.ts b/packages/web/src/lib/cn.ts new file mode 100644 index 000000000..eccbc98da --- /dev/null +++ b/packages/web/src/lib/cn.ts @@ -0,0 +1,4 @@ +/** Concatenate class names, filtering out falsy values. */ +export function cn(...classes: (string | false | null | undefined)[]): string { + return classes.filter(Boolean).join(" "); +} diff --git a/packages/web/src/lib/mock-data.ts b/packages/web/src/lib/mock-data.ts new file mode 100644 index 000000000..b09e50d11 --- /dev/null +++ b/packages/web/src/lib/mock-data.ts @@ -0,0 +1,538 @@ +import type { DashboardSession, DashboardStats } from "./types"; + +/** + * Mock data for the dashboard. + * Covers all attention zones: urgent, action, warning, ok, done. + * + * TODO: Remove this file when wiring to real data sources. These fixtures + * are imported by API routes and pages, so they end up in the server bundle. + * Consider moving to __fixtures__/ or gating behind an env check once + * real SessionManager integration is in place. + */ + +const now = new Date(); +const ago = (minutes: number) => new Date(now.getTime() - minutes * 60000).toISOString(); + +export const mockSessions: DashboardSession[] = [ + // URGENT: needs human input — agent is blocked asking a question + { + id: "backend-3", + projectId: "my-app", + status: "needs_input", + activity: "waiting_input", + branch: "feat/INT-1280-auth-refactor", + issueId: "INT-1280", + summary: "Refactoring authentication to use JWT — blocked on key rotation strategy", + createdAt: ago(180), + lastActivityAt: ago(2), + pr: { + number: 421, + url: "https://github.com/acme/my-app/pull/421", + title: "feat: refactor auth to use JWT with key rotation", + owner: "acme", + repo: "my-app", + branch: "feat/INT-1280-auth-refactor", + baseBranch: "main", + isDraft: false, + state: "open", + additions: 342, + deletions: 187, + ciStatus: "passing", + ciChecks: [ + { name: "build", status: "passed", url: "https://github.com/acme/my-app/actions/runs/1" }, + { name: "test", status: "passed", url: "https://github.com/acme/my-app/actions/runs/2" }, + { name: "lint", status: "passed", url: "https://github.com/acme/my-app/actions/runs/3" }, + ], + reviewDecision: "pending", + mergeability: { + mergeable: false, + ciPassing: true, + approved: false, + noConflicts: true, + blockers: ["Needs review"], + }, + unresolvedThreads: 0, + unresolvedComments: [], + }, + metadata: {}, + }, + + // URGENT: CI failing + { + id: "backend-5", + projectId: "my-app", + status: "ci_failed", + activity: "idle", + branch: "feat/INT-1295-rate-limiting", + issueId: "INT-1295", + summary: "Adding rate limiting middleware — CI failing on integration tests", + createdAt: ago(120), + lastActivityAt: ago(15), + pr: { + number: 428, + url: "https://github.com/acme/my-app/pull/428", + title: "feat: add rate limiting middleware with Redis backend", + owner: "acme", + repo: "my-app", + branch: "feat/INT-1295-rate-limiting", + baseBranch: "main", + isDraft: false, + state: "open", + additions: 567, + deletions: 23, + ciStatus: "failing", + ciChecks: [ + { name: "build", status: "passed", url: "https://github.com/acme/my-app/actions/runs/10" }, + { + name: "unit-tests", + status: "passed", + url: "https://github.com/acme/my-app/actions/runs/11", + }, + { + name: "integration-tests", + status: "failed", + url: "https://github.com/acme/my-app/actions/runs/12", + }, + { name: "lint", status: "passed", url: "https://github.com/acme/my-app/actions/runs/13" }, + ], + reviewDecision: "none", + mergeability: { + mergeable: false, + ciPassing: false, + approved: false, + noConflicts: true, + blockers: ["CI checks failing", "Needs review"], + }, + unresolvedThreads: 0, + unresolvedComments: [], + }, + metadata: {}, + }, + + // URGENT: changes requested + unresolved comments + { + id: "frontend-2", + projectId: "my-app", + status: "changes_requested", + activity: "idle", + branch: "feat/INT-1301-search-ui", + issueId: "INT-1301", + summary: "Implementing search UI — changes requested by reviewer", + createdAt: ago(300), + lastActivityAt: ago(45), + pr: { + number: 415, + url: "https://github.com/acme/my-app/pull/415", + title: "feat: add full-text search with autocomplete", + owner: "acme", + repo: "my-app", + branch: "feat/INT-1301-search-ui", + baseBranch: "main", + isDraft: false, + state: "open", + additions: 891, + deletions: 156, + ciStatus: "passing", + ciChecks: [ + { name: "build", status: "passed" }, + { name: "test", status: "passed" }, + { name: "lint", status: "passed" }, + { name: "e2e", status: "passed" }, + ], + reviewDecision: "changes_requested", + mergeability: { + mergeable: false, + ciPassing: true, + approved: false, + noConflicts: true, + blockers: ["Changes requested"], + }, + unresolvedThreads: 3, + unresolvedComments: [ + { + url: "https://github.com/acme/my-app/pull/415#discussion_r1", + path: "src/components/Search.tsx", + author: "reviewer1", + body: "This should debounce the API calls", + }, + { + url: "https://github.com/acme/my-app/pull/415#discussion_r2", + path: "src/hooks/useSearch.ts", + author: "reviewer1", + body: "Missing error handling for network failures", + }, + { + url: "https://github.com/acme/my-app/pull/415#discussion_r3", + path: "src/components/SearchResults.tsx", + author: "reviewer2", + body: "Accessibility: needs aria-live region for results", + }, + ], + }, + metadata: {}, + }, + + // ACTION: ready to merge + { + id: "backend-7", + projectId: "my-app", + status: "mergeable", + activity: "idle", + branch: "feat/INT-1310-health-check", + issueId: "INT-1310", + summary: "Added health check endpoint with dependency status", + createdAt: ago(90), + lastActivityAt: ago(30), + pr: { + number: 432, + url: "https://github.com/acme/my-app/pull/432", + title: "feat: add /health endpoint with dependency checks", + owner: "acme", + repo: "my-app", + branch: "feat/INT-1310-health-check", + baseBranch: "main", + isDraft: false, + state: "open", + additions: 145, + deletions: 8, + ciStatus: "passing", + ciChecks: [ + { name: "build", status: "passed" }, + { name: "test", status: "passed" }, + { name: "lint", status: "passed" }, + ], + reviewDecision: "approved", + mergeability: { + mergeable: true, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: [], + }, + unresolvedThreads: 0, + unresolvedComments: [], + }, + metadata: {}, + }, + + // ACTION: ready to merge (another one) + { + id: "frontend-4", + projectId: "my-app", + status: "mergeable", + activity: "idle", + branch: "fix/INT-1315-date-picker", + issueId: "INT-1315", + summary: "Fixed date picker timezone handling", + createdAt: ago(60), + lastActivityAt: ago(20), + pr: { + number: 435, + url: "https://github.com/acme/my-app/pull/435", + title: "fix: date picker timezone conversion for UTC users", + owner: "acme", + repo: "my-app", + branch: "fix/INT-1315-date-picker", + baseBranch: "main", + isDraft: false, + state: "open", + additions: 47, + deletions: 12, + ciStatus: "passing", + ciChecks: [ + { name: "build", status: "passed" }, + { name: "test", status: "passed" }, + { name: "lint", status: "passed" }, + ], + reviewDecision: "approved", + mergeability: { + mergeable: true, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: [], + }, + unresolvedThreads: 0, + unresolvedComments: [], + }, + metadata: {}, + }, + + // WARNING: PR open, needs review + { + id: "backend-8", + projectId: "my-app", + status: "review_pending", + activity: "idle", + branch: "feat/INT-1318-websocket", + issueId: "INT-1318", + summary: "WebSocket support for real-time notifications", + createdAt: ago(240), + lastActivityAt: ago(60), + pr: { + number: 430, + url: "https://github.com/acme/my-app/pull/430", + title: "feat: WebSocket server for real-time push notifications", + owner: "acme", + repo: "my-app", + branch: "feat/INT-1318-websocket", + baseBranch: "main", + isDraft: false, + state: "open", + additions: 723, + deletions: 45, + ciStatus: "passing", + ciChecks: [ + { name: "build", status: "passed" }, + { name: "test", status: "passed" }, + { name: "lint", status: "passed" }, + { name: "integration-tests", status: "passed" }, + ], + reviewDecision: "pending", + mergeability: { + mergeable: false, + ciPassing: true, + approved: false, + noConflicts: true, + blockers: ["Needs review"], + }, + unresolvedThreads: 0, + unresolvedComments: [], + }, + metadata: {}, + }, + + // WARNING: CI pending + { + id: "frontend-5", + projectId: "my-app", + status: "pr_open", + activity: "idle", + branch: "feat/INT-1322-dark-mode", + issueId: "INT-1322", + summary: "Implementing dark mode theme toggle", + createdAt: ago(45), + lastActivityAt: ago(10), + pr: { + number: 438, + url: "https://github.com/acme/my-app/pull/438", + title: "feat: dark mode with system preference detection", + owner: "acme", + repo: "my-app", + branch: "feat/INT-1322-dark-mode", + baseBranch: "main", + isDraft: false, + state: "open", + additions: 312, + deletions: 89, + ciStatus: "pending", + ciChecks: [ + { name: "build", status: "running" }, + { name: "test", status: "pending" }, + { name: "lint", status: "passed" }, + ], + reviewDecision: "none", + mergeability: { + mergeable: false, + ciPassing: false, + approved: false, + noConflicts: true, + blockers: ["CI checks pending", "Needs review"], + }, + unresolvedThreads: 0, + unresolvedComments: [], + }, + metadata: {}, + }, + + // OK: actively working + { + id: "backend-9", + projectId: "my-app", + status: "working", + activity: "active", + branch: "feat/INT-1325-caching", + issueId: "INT-1325", + summary: "Implementing Redis caching layer for API responses", + createdAt: ago(30), + lastActivityAt: ago(0), + pr: null, + metadata: {}, + }, + + // OK: actively working with PR + { + id: "frontend-6", + projectId: "my-app", + status: "working", + activity: "active", + branch: "feat/INT-1328-notifications", + issueId: "INT-1328", + summary: "Building notification center component", + createdAt: ago(55), + lastActivityAt: ago(0), + pr: { + number: 440, + url: "https://github.com/acme/my-app/pull/440", + title: "feat: notification center with mark-as-read", + owner: "acme", + repo: "my-app", + branch: "feat/INT-1328-notifications", + baseBranch: "main", + isDraft: true, + state: "open", + additions: 234, + deletions: 0, + ciStatus: "pending", + ciChecks: [ + { name: "build", status: "running" }, + { name: "test", status: "pending" }, + ], + reviewDecision: "none", + mergeability: { + mergeable: false, + ciPassing: false, + approved: false, + noConflicts: true, + blockers: ["Draft PR", "CI pending"], + }, + unresolvedThreads: 0, + unresolvedComments: [], + }, + metadata: {}, + }, + + // OK: actively working — has merge conflict on PR + { + id: "backend-11", + projectId: "my-app", + status: "working", + activity: "active", + branch: "feat/INT-1335-email-templates", + issueId: "INT-1335", + summary: "Implementing email template engine with MJML", + createdAt: ago(100), + lastActivityAt: ago(1), + pr: { + number: 442, + url: "https://github.com/acme/my-app/pull/442", + title: "feat: email template engine with MJML support", + owner: "acme", + repo: "my-app", + branch: "feat/INT-1335-email-templates", + baseBranch: "main", + isDraft: false, + state: "open", + additions: 489, + deletions: 32, + ciStatus: "passing", + ciChecks: [ + { name: "build", status: "passed", url: "https://github.com/acme/my-app/actions/runs/20" }, + { name: "test", status: "passed", url: "https://github.com/acme/my-app/actions/runs/21" }, + { name: "lint", status: "passed", url: "https://github.com/acme/my-app/actions/runs/22" }, + ], + reviewDecision: "pending", + mergeability: { + mergeable: false, + ciPassing: true, + approved: false, + noConflicts: false, + blockers: ["Merge conflict", "Needs review"], + }, + unresolvedThreads: 0, + unresolvedComments: [], + }, + metadata: {}, + }, + + // OK: spawning + { + id: "backend-10", + projectId: "my-app", + status: "spawning", + activity: "active", + branch: null, + issueId: "INT-1330", + summary: "Setting up workspace for database migration task", + createdAt: ago(1), + lastActivityAt: ago(0), + pr: null, + metadata: {}, + }, + + // DONE: merged + { + id: "backend-1", + projectId: "my-app", + status: "merged", + activity: "exited", + branch: "feat/INT-1260-logging", + issueId: "INT-1260", + summary: "Structured logging with correlation IDs", + createdAt: ago(480), + lastActivityAt: ago(120), + pr: { + number: 410, + url: "https://github.com/acme/my-app/pull/410", + title: "feat: structured logging with request correlation", + owner: "acme", + repo: "my-app", + branch: "feat/INT-1260-logging", + baseBranch: "main", + isDraft: false, + state: "merged", + additions: 456, + deletions: 234, + ciStatus: "passing", + ciChecks: [ + { name: "build", status: "passed" }, + { name: "test", status: "passed" }, + ], + reviewDecision: "approved", + mergeability: { + mergeable: false, + ciPassing: true, + approved: true, + noConflicts: true, + blockers: [], + }, + unresolvedThreads: 0, + unresolvedComments: [], + }, + metadata: {}, + }, + + // DONE: killed + { + id: "frontend-1", + projectId: "my-app", + status: "killed", + activity: "exited", + branch: "feat/INT-1270-table", + issueId: "INT-1270", + summary: "Data table component (superseded by INT-1301)", + createdAt: ago(600), + lastActivityAt: ago(300), + pr: null, + metadata: {}, + }, +]; + +export function getMockStats(): DashboardStats { + const sessions = mockSessions; + return { + totalSessions: sessions.length, + workingSessions: sessions.filter((s) => s.activity === "active").length, + openPRs: sessions.filter((s) => s.pr?.state === "open").length, + needsReview: sessions.filter( + (s) => + s.pr && + !s.pr.isDraft && + (s.pr.reviewDecision === "pending" || s.pr.reviewDecision === "none"), + ).length, + }; +} + +export function getMockSession(id: string): DashboardSession | null { + return mockSessions.find((s) => s.id === id) ?? null; +} diff --git a/packages/web/src/lib/types.ts b/packages/web/src/lib/types.ts new file mode 100644 index 000000000..3fb90127a --- /dev/null +++ b/packages/web/src/lib/types.ts @@ -0,0 +1,201 @@ +/** + * Dashboard-specific types for the web UI. + * + * Core types (SessionStatus, ActivityState, CIStatus, ReviewDecision, etc.) + * are re-exported from @agent-orchestrator/core. Dashboard-specific types + * extend/flatten the core types for client-side rendering (e.g. DashboardPR + * flattens core PRInfo + MergeReadiness + CICheck[] + ReviewComment[]). + */ + +// Re-export core types used directly by the dashboard +export type { + SessionStatus, + ActivityState, + CIStatus, + ReviewDecision, + MergeReadiness, + PRState, +} from "@agent-orchestrator/core"; + +import type { + CICheck as CoreCICheck, + MergeReadiness, + CIStatus, + SessionStatus, + ActivityState, + ReviewDecision, +} from "@agent-orchestrator/core"; + +/** + * Attention zone priority level, ordered by human action urgency: + * + * 1. merge — PR approved + CI green. One click to clear. Highest ROI. + * 2. respond — Agent waiting for human input. Quick unblock, agent resumes. + * 3. review — CI failed, changes requested, conflicts. Needs investigation. + * 4. pending — Waiting on external (reviewer, CI). Nothing to do right now. + * 5. working — Agents doing their thing. Don't interrupt. + * 6. done — Merged or terminated. Archive. + */ +export type AttentionLevel = "merge" | "respond" | "review" | "pending" | "working" | "done"; + +/** + * Flattened session for dashboard rendering. + * Maps to core Session but uses string dates (JSON-serializable for SSR/client boundary) + * and inlines PR state. + * + * TODO: When wiring to real data, add a serialization layer that converts + * core Session (Date objects) → DashboardSession (string dates). + */ +export interface DashboardSession { + id: string; + projectId: string; + status: SessionStatus; + activity: ActivityState; + branch: string | null; + issueId: string | null; + summary: string | null; + createdAt: string; + lastActivityAt: string; + pr: DashboardPR | null; + metadata: Record; +} + +/** + * Flattened PR for dashboard rendering. + * Aggregates core PRInfo + PRState + CICheck[] + MergeReadiness + ReviewComment[]. + */ +export interface DashboardPR { + number: number; + url: string; + title: string; + owner: string; + repo: string; + branch: string; + baseBranch: string; + isDraft: boolean; + state: "open" | "merged" | "closed"; + additions: number; + deletions: number; + ciStatus: CIStatus; + ciChecks: DashboardCICheck[]; + reviewDecision: ReviewDecision; + mergeability: DashboardMergeability; + unresolvedThreads: number; + unresolvedComments: DashboardUnresolvedComment[]; +} + +/** + * Mirrors core CICheck but omits Date fields (not JSON-serializable). + * Core CICheck also has conclusion, startedAt, completedAt. + */ +export interface DashboardCICheck { + name: string; + status: CoreCICheck["status"]; + url?: string; +} + +/** + * Same shape as core MergeReadiness — re-exported for convenience. + */ +export type DashboardMergeability = MergeReadiness; + +export interface DashboardUnresolvedComment { + url: string; + path: string; + author: string; + body: string; +} + +export interface DashboardStats { + totalSessions: number; + workingSessions: number; + openPRs: number; + needsReview: number; +} + +/** SSE snapshot event from /api/events */ +export interface SSESnapshotEvent { + type: "snapshot"; + sessions: Array<{ + id: string; + status: SessionStatus; + activity: ActivityState; + attentionLevel: AttentionLevel; + lastActivityAt: string; + }>; +} + +/** SSE activity update event from /api/events */ +export interface SSEActivityEvent { + type: "session.activity"; + sessionId: string; + activity: ActivityState; + status: SessionStatus; + attentionLevel: AttentionLevel; + timestamp: string; +} + +/** Determines which attention zone a session belongs to */ +export function getAttentionLevel(session: DashboardSession): AttentionLevel { + // ── Done: terminal states ───────────────────────────────────────── + if (session.status === "merged" || session.status === "killed" || session.status === "cleanup") { + return "done"; + } + if (session.pr) { + if (session.pr.state === "merged" || session.pr.state === "closed") { + return "done"; + } + } + + // ── Merge: PR is ready — one click to clear ─────────────────────── + // Check this early: if the PR is mergeable, that's the most valuable + // action for the human regardless of agent activity. + if (session.status === "mergeable" || session.status === "approved") { + return "merge"; + } + if (session.pr?.mergeability.mergeable) { + return "merge"; + } + + // ── Respond: agent is waiting for human input ───────────────────── + if (session.activity === "waiting_input" || session.activity === "blocked") { + return "respond"; + } + if ( + session.status === "needs_input" || + session.status === "stuck" || + session.status === "errored" + ) { + return "respond"; + } + // Exited agent with non-terminal status = crashed, needs human attention + if (session.activity === "exited") { + return "respond"; + } + + // ── Review: problems that need investigation ────────────────────── + if (session.status === "ci_failed" || session.status === "changes_requested") { + return "review"; + } + if (session.pr) { + const pr = session.pr; + if (pr.ciStatus === "failing") return "review"; + if (pr.reviewDecision === "changes_requested") return "review"; + if (!pr.mergeability.noConflicts) return "review"; + } + + // ── Pending: waiting on external (reviewer, CI) ─────────────────── + if (session.status === "review_pending") { + return "pending"; + } + if (session.pr) { + const pr = session.pr; + if (!pr.isDraft && pr.unresolvedThreads > 0) return "pending"; + if (!pr.isDraft && (pr.reviewDecision === "pending" || pr.reviewDecision === "none")) { + return "pending"; + } + } + + // ── Working: agents doing their thing ───────────────────────────── + return "working"; +} diff --git a/packages/web/src/lib/validation.ts b/packages/web/src/lib/validation.ts new file mode 100644 index 000000000..6de67c598 --- /dev/null +++ b/packages/web/src/lib/validation.ts @@ -0,0 +1,44 @@ +/** Validate that a value is a non-empty string within a max length. Returns error message or null. */ +export function validateString( + value: unknown, + fieldName: string, + maxLength: number, +): string | null { + if (value === undefined || value === null) { + return `${fieldName} is required`; + } + if (typeof value !== "string") { + return `${fieldName} must be a string`; + } + if (value.trim().length === 0) { + return `${fieldName} must not be empty`; + } + if (value.length > maxLength) { + return `${fieldName} must be at most ${maxLength} characters`; + } + return null; +} + +/** Validate that a value matches a safe identifier pattern (alphanumeric, hyphens, underscores). */ +export function validateIdentifier( + value: unknown, + fieldName: string, + maxLength = 128, +): string | null { + const strErr = validateString(value, fieldName, maxLength); + if (strErr) return strErr; + if (!/^[a-zA-Z0-9_-]+$/.test(value as string)) { + return `${fieldName} must match [a-zA-Z0-9_-]+`; + } + return null; +} + +/** + * Strip control characters (U+0000–U+001F, U+007F–U+009F) from a string. + * Critical for messages that may be passed to shell-based runtimes (tmux send-keys, etc.) + * to prevent command injection via control sequences. + */ +export function stripControlChars(value: string): string { + // eslint-disable-next-line no-control-regex + return value.replace(/[\x00-\x1f\x7f-\x9f]/g, ""); +} diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json index 8d8f525eb..c148c2ee5 100644 --- a/packages/web/tsconfig.json +++ b/packages/web/tsconfig.json @@ -1,16 +1,27 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "lib": ["ES2022", "DOM", "DOM.Iterable"], "outDir": "dist", "rootDir": ".", "jsx": "preserve", "module": "ESNext", "moduleResolution": "Bundler", - "plugins": [{ "name": "next" }], + "plugins": [ + { + "name": "next" + } + ], "paths": { "@/*": ["./src/*"] - } + }, + "allowJs": true, + "noEmit": true, + "incremental": true, + "declaration": false, + "declarationMap": false, + "verbatimModuleSyntax": false }, - "include": ["src", "next-env.d.ts", ".next/types/**/*.ts"], + "include": ["next-env.d.ts", "src", ".next/types/**/*.ts"], "exclude": ["node_modules"] } diff --git a/packages/web/vitest.config.ts b/packages/web/vitest.config.ts new file mode 100644 index 000000000..03c46261e --- /dev/null +++ b/packages/web/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; +import { resolve } from "node:path"; + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./src/__tests__/setup.ts"], + include: ["src/**/*.test.{ts,tsx}"], + }, + resolve: { + alias: { + "@": resolve(__dirname, "./src"), + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84a716597..4956f6338 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,22 +10,22 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.0.0) + version: 10.0.1(eslint@10.0.0(jiti@2.6.1)) '@types/node': specifier: ^25.2.3 version: 25.2.3 eslint: specifier: ^10.0.0 - version: 10.0.0 + version: 10.0.0(jiti@2.6.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.0.0) + version: 10.1.8(eslint@10.0.0(jiti@2.6.1)) prettier: specifier: ^3.8.1 version: 3.8.1 typescript-eslint: specifier: ^8.55.0 - version: 8.55.0(eslint@10.0.0)(typescript@5.9.3) + version: 8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3) packages/cli: dependencies: @@ -69,7 +69,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.2.3)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/integration-tests: dependencies: @@ -97,7 +97,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.4(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/node@25.2.3)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/plugins/agent-aider: dependencies: @@ -113,7 +113,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.4(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/node@25.2.3)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/plugins/agent-claude-code: dependencies: @@ -129,7 +129,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.4(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/node@25.2.3)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/plugins/agent-codex: dependencies: @@ -145,7 +145,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.4(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/node@25.2.3)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/plugins/agent-opencode: dependencies: @@ -161,7 +161,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.0 - version: 3.2.4(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/node@25.2.3)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) packages/plugins/notifier-desktop: dependencies: @@ -326,7 +326,7 @@ importers: version: link:../core next: specifier: ^15.1.0 - version: 15.5.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 15.5.12(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: ^19.0.0 version: 19.2.4 @@ -334,120 +334,371 @@ importers: specifier: ^19.0.0 version: 19.2.4(react@19.2.4) devDependencies: + '@tailwindcss/postcss': + specifier: ^4.0.0 + version: 4.1.18 + '@testing-library/jest-dom': + specifier: ^6.6.0 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.1.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@types/react': specifier: ^19.0.0 version: 19.2.14 '@types/react-dom': specifier: ^19.0.0 version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^4.3.0 + version: 4.7.0(vite@5.4.21(@types/node@25.2.3)(lightningcss@1.30.2)) + jsdom: + specifier: ^25.0.0 + version: 25.0.1 + playwright: + specifier: ^1.49.0 + version: 1.58.2 tailwindcss: specifier: ^4.0.0 version: 4.1.18 + tsx: + specifier: ^4.19.0 + version: 4.21.0 typescript: specifier: ^5.7.0 version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@25.2.3)(jsdom@25.0.1)(lightningcss@1.30.2) packages: + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.27.3': resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.27.3': resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.27.3': resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.27.3': resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.27.3': resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.27.3': resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.27.3': resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.27.3': resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.27.3': resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.27.3': resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.27.3': resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.27.3': resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.27.3': resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.27.3': resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.27.3': resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} @@ -460,6 +711,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} @@ -472,6 +729,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} @@ -484,24 +747,48 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.27.3': resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.27.3': resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.27.3': resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.27.3': resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} @@ -704,9 +991,22 @@ packages: resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} engines: {node: '>=18'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@next/env@15.5.12': resolution: {integrity: sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==} @@ -758,6 +1058,9 @@ packages: cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/rollup-android-arm-eabi@4.57.1': resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} cpu: [arm] @@ -889,6 +1192,132 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.18': + resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -974,12 +1403,32 @@ packages: resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} '@vitest/expect@4.0.18': resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@3.2.4': resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} peerDependencies: @@ -1002,30 +1451,45 @@ packages: vite: optional: true + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} '@vitest/pretty-format@4.0.18': resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@3.2.4': resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} '@vitest/runner@4.0.18': resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@3.2.4': resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} '@vitest/snapshot@4.0.18': resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} '@vitest/spy@4.0.18': resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} @@ -1042,17 +1506,39 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + ansi-regex@6.2.2: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1060,6 +1546,10 @@ packages: resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==} engines: {node: 20 || >=22} + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -1067,10 +1557,19 @@ packages: resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} engines: {node: 20 || >=22} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001769: resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} @@ -1101,17 +1600,35 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@13.1.0: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1121,6 +1638,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -1128,21 +1648,75 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.286: + resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + enhanced-resolve@5.19.0: + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} + engines: {node: '>=10.13.0'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1239,15 +1813,39 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-east-asian-width@1.4.0: resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} @@ -1255,6 +1853,41 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1267,6 +1900,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1279,6 +1916,9 @@ packages: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-unicode-supported@1.3.0: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} @@ -1294,9 +1934,30 @@ packages: resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} engines: {node: 20 || >=22} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + jsdom@25.0.1: + resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -1306,6 +1967,11 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1313,6 +1979,76 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1324,13 +2060,39 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@10.2.0: resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==} engines: {node: 20 || >=22} @@ -1371,6 +2133,12 @@ packages: sass: optional: true + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -1394,6 +2162,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1402,6 +2173,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1416,6 +2190,16 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -1433,6 +2217,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1442,10 +2230,21 @@ packages: peerDependencies: react: ^19.2.4 + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -1458,9 +2257,26 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -1507,6 +2323,10 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -1523,9 +2343,16 @@ packages: babel-plugin-macros: optional: true + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tailwindcss@4.1.18: resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1544,6 +2371,10 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -1552,10 +2383,29 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + tinyspy@4.0.4: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -1589,14 +2439,56 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1637,6 +2529,31 @@ packages: yaml: optional: true + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@3.2.4: resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1699,6 +2616,27 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1713,6 +2651,28 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.2: resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} @@ -1727,92 +2687,307 @@ packages: snapshots: + '@adobe/css-tools@4.4.4': {} + + '@alloc/quick-lru@5.2.0': {} + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/runtime@7.28.6': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.27.3': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.27.3': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.27.3': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.27.3': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.27.3': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.27.3': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.27.3': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.27.3': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.27.3': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.27.3': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.27.3': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.27.3': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.27.3': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.27.3': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.27.3': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.27.3': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.27.3': optional: true '@esbuild/netbsd-arm64@0.27.3': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.27.3': optional: true '@esbuild/openbsd-arm64@0.27.3': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.27.3': optional: true '@esbuild/openharmony-arm64@0.27.3': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.27.3': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.27.3': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.27.3': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.0.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.0.0(jiti@2.6.1))': dependencies: - eslint: 10.0.0 + eslint: 10.0.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -1833,9 +3008,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.0.0)': + '@eslint/js@10.0.1(eslint@10.0.0(jiti@2.6.1))': optionalDependencies: - eslint: 10.0.0 + eslint: 10.0.0(jiti@2.6.1) '@eslint/object-schema@3.0.1': {} @@ -1954,8 +3129,25 @@ snapshots: '@isaacs/cliui@9.0.0': {} + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@next/env@15.5.12': {} '@next/swc-darwin-arm64@15.5.12': @@ -1982,6 +3174,8 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.12': optional: true + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/rollup-android-arm-eabi@4.57.1': optional: true @@ -2063,6 +3257,128 @@ snapshots: dependencies: tslib: 2.8.1 + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.19.0 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/postcss@4.1.18': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + postcss: 8.5.6 + tailwindcss: 4.1.18 + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.28.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@testing-library/dom': 10.4.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -2088,15 +3404,15 @@ snapshots: dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.9.3))(eslint@10.0.0)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.55.0(eslint@10.0.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/type-utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.55.0 - eslint: 10.0.0 + eslint: 10.0.0(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -2104,14 +3420,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.9.3)': + '@typescript-eslint/parser@8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.55.0 '@typescript-eslint/types': 8.55.0 '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.55.0 debug: 4.4.3 - eslint: 10.0.0 + eslint: 10.0.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -2134,13 +3450,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.55.0(eslint@10.0.0)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.55.0 '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 10.0.0 + eslint: 10.0.0(jiti@2.6.1) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -2163,13 +3479,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.55.0(eslint@10.0.0)(typescript@5.9.3)': + '@typescript-eslint/utils@8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.55.0 '@typescript-eslint/types': 8.55.0 '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - eslint: 10.0.0 + eslint: 10.0.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -2179,6 +3495,25 @@ snapshots: '@typescript-eslint/types': 8.55.0 eslint-visitor-keys: 4.2.1 + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@25.2.3)(lightningcss@1.30.2))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21(@types/node@25.2.3)(lightningcss@1.30.2) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.3 @@ -2196,21 +3531,33 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@25.2.3)(lightningcss@1.30.2))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@25.2.3)(lightningcss@1.30.2) + + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 '@vitest/pretty-format@3.2.4': dependencies: @@ -2220,6 +3567,11 @@ snapshots: dependencies: tinyrainbow: 3.0.3 + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + '@vitest/runner@3.2.4': dependencies: '@vitest/utils': 3.2.4 @@ -2231,6 +3583,12 @@ snapshots: '@vitest/utils': 4.0.18 pathe: 2.0.3 + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 @@ -2243,12 +3601,22 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + '@vitest/spy@3.2.4': dependencies: tinyspy: 4.0.4 '@vitest/spy@4.0.18': {} + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 @@ -2266,6 +3634,8 @@ snapshots: acorn@8.15.0: {} + agent-base@7.1.4: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -2273,16 +3643,30 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@5.0.1: {} + ansi-regex@6.2.2: {} + ansi-styles@5.2.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + assertion-error@2.0.1: {} + asynckit@0.4.0: {} + balanced-match@1.0.2: {} balanced-match@4.0.2: dependencies: jackspeak: 4.2.3 + baseline-browser-mapping@2.9.19: {} + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -2291,8 +3675,21 @@ snapshots: dependencies: balanced-match: 4.0.2 + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001769 + electron-to-chromium: 1.5.286 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + caniuse-lite@1.0.30001769: {} chai@5.3.3: @@ -2317,31 +3714,114 @@ snapshots: client-only@0.0.1: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@13.1.0: {} + convert-source-map@2.0.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + css.escape@1.5.1: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + csstype@3.2.3: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + debug@4.4.3: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + deep-eql@5.0.2: {} deep-is@0.1.4: {} - detect-libc@2.1.2: - optional: true + delayed-stream@1.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.286: {} emoji-regex@10.6.0: {} + enhanced-resolve@5.19.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@6.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + esbuild@0.27.3: optionalDependencies: '@esbuild/aix-ppc64': 0.27.3 @@ -2371,11 +3851,13 @@ snapshots: '@esbuild/win32-ia32': 0.27.3 '@esbuild/win32-x64': 0.27.3 + escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.0.0): + eslint-config-prettier@10.1.8(eslint@10.0.0(jiti@2.6.1)): dependencies: - eslint: 10.0.0 + eslint: 10.0.0(jiti@2.6.1) eslint-scope@9.1.0: dependencies: @@ -2390,9 +3872,9 @@ snapshots: eslint-visitor-keys@5.0.0: {} - eslint@10.0.0: + eslint@10.0.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.1 '@eslint/config-helpers': 0.5.2 @@ -2422,6 +3904,8 @@ snapshots: minimatch: 10.2.0 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color @@ -2475,11 +3959,44 @@ snapshots: flatted@3.3.3: {} + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + get-east-asian-width@1.4.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-tsconfig@4.13.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -2488,12 +4005,50 @@ snapshots: dependencies: is-glob: 4.0.3 + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -2502,6 +4057,8 @@ snapshots: is-interactive@2.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-unicode-supported@1.3.0: {} is-unicode-supported@2.1.0: {} @@ -2512,14 +4069,50 @@ snapshots: dependencies: '@isaacs/cliui': 9.0.0 + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + jsdom@25.0.1: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + form-data: 4.0.5 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -2529,6 +4122,55 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -2540,12 +4182,30 @@ snapshots: loupe@3.2.1: {} + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mimic-function@5.0.1: {} + min-indent@1.0.1: {} + minimatch@10.2.0: dependencies: brace-expansion: 5.0.2 @@ -2560,7 +4220,7 @@ snapshots: natural-compare@1.4.0: {} - next@15.5.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@15.5.12(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 15.5.12 '@swc/helpers': 0.5.15 @@ -2568,7 +4228,7 @@ snapshots: postcss: 8.4.31 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - styled-jsx: 5.1.6(react@19.2.4) + styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) optionalDependencies: '@next/swc-darwin-arm64': 15.5.12 '@next/swc-darwin-x64': 15.5.12 @@ -2583,6 +4243,10 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-releases@2.0.27: {} + + nwsapi@2.2.23: {} + obug@2.1.1: {} onetime@7.0.0: @@ -2618,10 +4282,16 @@ snapshots: dependencies: p-limit: 3.1.0 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + path-exists@4.0.0: {} path-key@3.1.1: {} + pathe@1.1.2: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -2630,6 +4300,14 @@ snapshots: picomatch@4.0.3: {} + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + postcss@8.4.31: dependencies: nanoid: 3.3.11 @@ -2646,6 +4324,12 @@ snapshots: prettier@3.8.1: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + punycode@2.3.1: {} react-dom@19.2.4(react@19.2.4): @@ -2653,8 +4337,17 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 + react-is@17.0.2: {} + + react-refresh@0.17.0: {} + react@19.2.4: {} + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + resolve-pkg-maps@1.0.0: {} restore-cursor@5.1.0: @@ -2693,8 +4386,20 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 + rrweb-cssom@0.7.1: {} + + rrweb-cssom@0.8.0: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} + semver@6.3.1: {} + semver@7.7.4: {} sharp@0.34.5: @@ -2757,17 +4462,27 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 - styled-jsx@5.1.6(react@19.2.4): + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4): dependencies: client-only: 0.0.1 react: 19.2.4 + optionalDependencies: + '@babel/core': 7.29.0 + + symbol-tree@3.2.4: {} tailwindcss@4.1.18: {} + tapable@2.3.0: {} + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -2781,12 +4496,30 @@ snapshots: tinypool@1.1.1: {} + tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} tinyrainbow@3.0.3: {} + tinyspy@3.0.2: {} + tinyspy@4.0.4: {} + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -2804,13 +4537,13 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.55.0(eslint@10.0.0)(typescript@5.9.3): + typescript-eslint@8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0)(typescript@5.9.3))(eslint@10.0.0)(typescript@5.9.3) - '@typescript-eslint/parser': 8.55.0(eslint@10.0.0)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.55.0(eslint@10.0.0)(typescript@5.9.3) - eslint: 10.0.0 + '@typescript-eslint/utils': 8.55.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.0.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -2819,17 +4552,41 @@ snapshots: undici-types@7.16.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 - vite-node@3.2.4(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2): + vite-node@2.1.9(@types/node@25.2.3)(lightningcss@1.30.2): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@25.2.3)(lightningcss@1.30.2) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-node@3.2.4(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -2844,7 +4601,17 @@ snapshots: - tsx - yaml - vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2): + vite@5.4.21(@types/node@25.2.3)(lightningcss@1.30.2): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.57.1 + optionalDependencies: + '@types/node': 25.2.3 + fsevents: 2.3.3 + lightningcss: 1.30.2 + + vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -2855,14 +4622,52 @@ snapshots: optionalDependencies: '@types/node': 25.2.3 fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 tsx: 4.21.0 yaml: 2.8.2 - vitest@3.2.4(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2): + vitest@2.1.9(@types/node@25.2.3)(jsdom@25.0.1)(lightningcss@1.30.2): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@25.2.3)(lightningcss@1.30.2)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@25.2.3)(lightningcss@1.30.2) + vite-node: 2.1.9(@types/node@25.2.3)(lightningcss@1.30.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.2.3 + jsdom: 25.0.1 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vitest@3.2.4(@types/node@25.2.3)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -2880,11 +4685,12 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.2.3 + jsdom: 25.0.1 transitivePeerDependencies: - jiti - less @@ -2899,10 +4705,10 @@ snapshots: - tsx - yaml - vitest@4.0.18(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@types/node@25.2.3)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -2919,10 +4725,11 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.2.3 + jsdom: 25.0.1 transitivePeerDependencies: - jiti - less @@ -2936,6 +4743,23 @@ snapshots: - tsx - yaml + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -2947,6 +4771,14 @@ snapshots: word-wrap@1.2.5: {} + ws@8.19.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} + yaml@2.8.2: {} yocto-queue@0.1.0: {}