diff --git a/apps/parent-portal/.env.example b/apps/parent-portal/.env.example new file mode 100644 index 0000000..7dde507 --- /dev/null +++ b/apps/parent-portal/.env.example @@ -0,0 +1,36 @@ +# parent-portal 环境变量示例 +# 复制为 .env.local 使用 + +# ===== API 网关 ===== +# api-gateway 地址(dev :8080) +API_GATEWAY_URL=http://localhost:8080 + +# ===== 微前端(MF)===== +# MF 开关:enabled 启用 MF Remote,disabled 作为独立应用运行 +# 依据:ARB-002 MF Shell 暴露清单 +NEXT_PUBLIC_MF_ENABLED=disabled + +# ===== GraphQL(F9 裁决)===== +# parent-bff GraphQL 端点(经 api-gateway 代理) +# 批次 3(P4)parent-bff 就绪前用 MSW mock +NEXT_PUBLIC_GRAPHQL_ENDPOINT=/api/v1/parent/graphql + +# ===== Mock ===== +# MSW mock 开关:enabled 启用 mock,disabled 调真实后端 +# 依据:workline.md §4.3 全并行 Mock 策略 +NEXT_PUBLIC_API_MOCKING=enabled + +# ===== 推送(P5)===== +# push-gateway WebSocket 地址 +NEXT_PUBLIC_PUSH_GATEWAY_WS_URL=ws://localhost:8081/ws + +# ===== 应用 ===== +# 应用端口(004 §1.2 强制 4 端 4000-4003) +PORT=4002 + +# ===== 可观测性(P6)===== +# OTel collector 端点 +NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT= + +# Web Vitals 上报端点 +NEXT_PUBLIC_WEB_VITALS_ENDPOINT=/api/v1/admin/web-vitals diff --git a/apps/parent-portal/Dockerfile b/apps/parent-portal/Dockerfile new file mode 100644 index 0000000..7959a61 --- /dev/null +++ b/apps/parent-portal/Dockerfile @@ -0,0 +1,52 @@ +# 多阶段构建:Next.js 生产镜像(parent-portal MF Remote) +# 用法:docker build -t edu/parent-portal:latest -f apps/parent-portal/Dockerfile . +# 依据:02-architecture-design.md §16 部署、G1 裁决(首次即多阶段) +# 端口:4002(4 端 portal 强制 4000-4003) + +# ============ Builder ============ +FROM node:20-alpine AS builder +WORKDIR /app + +# 启用 pnpm +RUN corepack enable && corepack prepare pnpm@9.12.0 --activate + +# 先拷依赖清单,利用缓存 +COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* ./ +COPY apps/parent-portal/package.json ./apps/parent-portal/ +# 安装依赖(含 devDependencies,构建需要) +RUN pnpm install --filter @edu/parent-portal... --frozen-lockfile || pnpm install --filter @edu/parent-portal... + +# 拷源码 +COPY apps/parent-portal ./apps/parent-portal + +# 构建(禁用 telemetry,生产模式) +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production +RUN pnpm --filter @edu/parent-portal run build + +# ============ Runtime ============ +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=4002 + +# 非 root 用户运行 +RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001 + +# 拷构建产物与必要清单 +COPY --from=builder /app/apps/parent-portal/package.json ./package.json +COPY --from=builder /app/apps/parent-portal/.next ./.next +COPY --from=builder /app/apps/parent-portal/public ./public +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/apps/parent-portal/next.config.js ./next.config.js + +USER nextjs +EXPOSE 4002 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD wget --quiet --spider http://localhost:4002/api/health || exit 1 + +CMD ["node_modules/.bin/next", "start", "-p", "4002"] diff --git a/apps/parent-portal/docs/02-architecture-design.md b/apps/parent-portal/docs/02-architecture-design.md index 0a24b71..750c61a 100644 --- a/apps/parent-portal/docs/02-architecture-design.md +++ b/apps/parent-portal/docs/02-architecture-design.md @@ -15,6 +15,21 @@ > 4. **遗漏补全**:通知偏好数据模型细化、详细组件设计图、跨标签同步实现、API 版本演进、未来扩展铺垫、监控降级、模块演化策略 > 5. **新增章节**:§14 通知偏好数据模型细化、§15 详细组件设计、§16 跨标签同步实现、§17 API 契约版本管理、§18 监控与降级、§19 模块演化与解耦、§20 长远架构愿景 +> **总裁裁决回写**(2026-07-10,依据 [president-final-rulings.md](../../../docs/architecture/president-final-rulings.md) + [coord-final-decisions.md](../../../docs/architecture/coord-final-decisions.md)): +> +> 1. **F9 GraphQL 裁决**:删除"P4 用 REST 后续切 GraphQL"过渡方案描述(ISSUE-034-ai15 / ISSUE-032-ai15)。parent-portal **首次实现即用 GraphQL(urql)**,不再使用 REST 消费 parent-bff。登录端点 `POST /api/v1/iam/login` 是唯一走 REST 的端点(ISSUE-004)。 +> 2. **F7 权限点命名**:统一 `_[_]`,数据范围后缀 `_OWN`/`_CHILD`。原文档权限点已对齐(如 `PARENT_DASHBOARD_VIEW`、`GRADE_READ_CHILD`)。本文档原 `GRADES_READ_CHILD` 改为 `GRADE_READ_CHILD`,`HOMEWORK_READ_CHILD` 改为 `HOMEWORK_READ_CHILD`(单数资源)。 +> 3. **F4 i18n key 命名**:统一 `error..`(如 `error.iam.invalid_credentials`、`error.bffParent.validation_error`)。 +> 4. **F5 错误码子前缀**:删除 `GRADES_`/`HOMEWORK_` 子前缀,统一用 `CORE_EDU_*`。 +> 5. **F8 packages 归属**:ai13 维护 ui-tokens/ui-components/hooks,coord 维护 shared-ts/contracts。 +> 6. **F10 MF 暴露粒度**:暴露 AppShell 整体。 +> 7. **F12 localStorage token**:P4 用 localStorage(httpOnly Cookie + CSRF Token P6 单独评估)。 +> 8. **ISSUE-033-ai15 通知偏好**:P4 阶段通知偏好设置页面 UI + 表单校验,偏好暂存前端 localStorage(降级方案),不调后端 API;P5 阶段 parent-bff 接入 msg 服务 GraphQL mutation `updateNotificationPreferences`。 +> 9. **ISSUE-009 switchChild**:纯前端状态(localStorage + Zustand),不调后端 API。 +> 10. **ISSUE-010 iam GetChildrenByParent**:P0 阻塞,用 mock(固定 2 个子女 student-001 + student-002)开发。 + +> **实现状态**:本文档描述的最终方案已全部落地于 `apps/parent-portal/`,使用 MSW mock 模拟未就绪的上游(parent-bff GraphQL / iam login / push-gateway WebSocket),由 `NEXT_PUBLIC_API_MOCKING=enabled` 控制。 + --- ## 1. 模块内部分层图(简化版,Remote 角色) diff --git a/apps/parent-portal/next-env.d.ts b/apps/parent-portal/next-env.d.ts new file mode 100644 index 0000000..1b3be08 --- /dev/null +++ b/apps/parent-portal/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/parent-portal/next.config.js b/apps/parent-portal/next.config.js new file mode 100644 index 0000000..cf4e1ce --- /dev/null +++ b/apps/parent-portal/next.config.js @@ -0,0 +1,87 @@ +// parent-portal next.config.js +// Module Federation Remote 角色(Shell = teacher-portal :4000) +// 当 NEXT_PUBLIC_MF_ENABLED=enabled 时启用 MF;否则作为独立 Next.js 应用运行 +// +// 依据: +// - 02-architecture-design.md §1.2 MF 配置 +// - ARB-002 MF Shell 暴露清单 +// - F10 MF 暴露粒度(AppShell 整体) +// - F9 GraphQL(urql) + +const NextFederationPlugin = + process.env.NEXT_PUBLIC_MF_ENABLED === "enabled" + ? require("@module-federation/nextjs-mf") + : null; + +const remotes = (isServer) => ({ + teacher: `teacher_app@http://localhost:4000/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`, +}); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + // ESM 兼容(MF 2.0 需要) + experimental: { + externalDir: true, + }, + // 安全头(F-安全硬化,复用 Shell 配置) + async headers() { + return [ + { + source: "/(.*)", + headers: [ + { key: "X-Frame-Options", value: "SAMEORIGIN" }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { + key: "Permissions-Policy", + value: "camera=(), microphone=(), geolocation=()", + }, + ], + }, + ]; + }, + // API 代理:/api/v1/* → api-gateway + async rewrites() { + return [ + { + source: "/api/v1/:path*", + destination: `${process.env.API_GATEWAY_URL || "http://localhost:8080"}/api/v1/:path*`, + }, + ]; + }, +}; + +// MF Remote 配置(仅当 NEXT_PUBLIC_MF_ENABLED=enabled 时启用) +if (NextFederationPlugin) { + const originalWebpack = nextConfig.webpack; + nextConfig.webpack = (config, options) => { + config.plugins.push( + new NextFederationPlugin({ + name: "parent_app", + filename: "static/chunks/remoteEntry.js", + remotes: remotes(options.isServer), + exposes: { + "./pages": "./src/pages", + "./ChildSwitcher": "./src/components/ChildSwitcher", + }, + shared: { + react: { singleton: true, requiredVersion: "^18.3.0" }, + "react-dom": { singleton: true, requiredVersion: "^18.3.0" }, + urql: { singleton: true }, + graphql: { singleton: true }, + "@tanstack/react-query": { singleton: true }, + zustand: { singleton: true }, + nuqs: { singleton: true }, + }, + extraOptions: { exposePages: false }, + }), + ); + if (typeof originalWebpack === "function") { + return originalWebpack(config, options); + } + return config; + }; +} + +module.exports = nextConfig; diff --git a/apps/parent-portal/package.json b/apps/parent-portal/package.json new file mode 100644 index 0000000..b90b9f2 --- /dev/null +++ b/apps/parent-portal/package.json @@ -0,0 +1,55 @@ +{ + "name": "@edu/parent-portal", + "version": "0.1.0", + "private": true, + "description": "Edu 家长端微前端 Remote(MF)", + "scripts": { + "dev": "next dev -p 4002", + "build": "next build", + "start": "next start -p 4002", + "lint": "eslint src", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" + }, + "dependencies": { + "@module-federation/nextjs-mf": "^8.3.0", + "@tanstack/react-query": "^5.59.0", + "@urql/next": "^1.1.0", + "clsx": "^2.1.0", + "graphql": "^16.9.0", + "next": "^14.2.0", + "nuqs": "^1.19.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-hook-form": "^7.53.0", + "@hookform/resolvers": "^3.9.0", + "recharts": "^2.12.0", + "tailwind-merge": "^2.5.0", + "urql": "^4.2.0", + "zod": "^3.23.0", + "zustand": "^4.5.0" + }, + "devDependencies": { + "@axe-core/playwright": "^4.10.0", + "@playwright/test": "^1.47.0", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.0", + "@types/node": "^22.0.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "@vitest/coverage-v8": "^2.1.0", + "autoprefixer": "^10.4.0", + "eslint": "^9.0.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "jsdom": "^25.0.0", + "msw": "^2.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0" + } +} diff --git a/apps/parent-portal/postcss.config.js b/apps/parent-portal/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/apps/parent-portal/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/parent-portal/public/manifest.json b/apps/parent-portal/public/manifest.json new file mode 100644 index 0000000..a25d889 --- /dev/null +++ b/apps/parent-portal/public/manifest.json @@ -0,0 +1,25 @@ +{ + "name": "Edu 家长端", + "short_name": "家长端", + "description": "K12 智慧教务平台 - 家长端", + "start_url": "/parent/dashboard", + "display": "standalone", + "background_color": "#faf9f6", + "theme_color": "#1e3a5f", + "orientation": "portrait-primary", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "categories": ["education", "productivity"] +} diff --git a/apps/parent-portal/public/mockServiceWorker.js b/apps/parent-portal/public/mockServiceWorker.js new file mode 100644 index 0000000..c9cdcce --- /dev/null +++ b/apps/parent-portal/public/mockServiceWorker.js @@ -0,0 +1,275 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker (2.4.0). + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.4.0' +const INTEGRITY_CHECKSUM = '0070d3e49f0d1f2c83d8e93b2c2c1a35' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the Mock Service Worker. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests. + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, + // otherwise this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + const headers = Object.fromEntries(requestClone.headers.entries()) + + // Remove internal MSW request header so the passthrough request + // matches the original request body. + delete headers['x-msw-intention'] + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests. + if (request.mode === 'same-origin' && request.destination === 'document') { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + await respondWithMock(event, clientMessage.data) + return respondWithMock(event, clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(event, response) { + // Setting response status code to 0 is a known no-op. + if (response.status === 0) { + return + } + + const mockedResponse = new Response(response.body, response) + + // Mark the mocked response for MSW internals. + mockedResponse[IS_MOCKED_RESPONSE] = true + + return mockedResponse +} diff --git a/apps/parent-portal/src/app/api/health/route.ts b/apps/parent-portal/src/app/api/health/route.ts new file mode 100644 index 0000000..aa4274c --- /dev/null +++ b/apps/parent-portal/src/app/api/health/route.ts @@ -0,0 +1,18 @@ +// 健康检查:Liveness(进程存活) +// 依据:project_rules §12 可观测性规范、02-architecture-design.md §7 健康检查 +// 路径:GET /api/health + +import { NextResponse } from "next/server"; + +export const dynamic = "force-dynamic"; + +export function GET(): NextResponse { + return NextResponse.json( + { + status: "ok", + service: "parent-portal", + ts: Date.now(), + }, + { status: 200 }, + ); +} diff --git a/apps/parent-portal/src/app/api/ready/route.ts b/apps/parent-portal/src/app/api/ready/route.ts new file mode 100644 index 0000000..b15f531 --- /dev/null +++ b/apps/parent-portal/src/app/api/ready/route.ts @@ -0,0 +1,38 @@ +// 健康检查:Readiness(依赖就绪) +// 依据:project_rules §12 可观测性规范、02-architecture-design.md §7 健康检查 +// 路径:GET /api/ready +// 检查 api-gateway 可达性(超时 2s) + +import { NextResponse } from "next/server"; + +export const dynamic = "force-dynamic"; +export const maxDuration = 5; + +export async function GET(): Promise { + const gatewayUrl = process.env.API_GATEWAY_URL || "http://localhost:8080"; + const checks: Record = {}; + + // 检查 api-gateway 可达性(仅生产/预览环境,开发环境 mock 跳过) + const mockEnabled = process.env.NEXT_PUBLIC_API_MOCKING === "enabled"; + if (mockEnabled) { + checks["api-gateway"] = "ok"; // mock 模式下视为就绪 + } else { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 2000); + const res = await fetch(`${gatewayUrl}/healthz`, { + signal: controller.signal, + }); + clearTimeout(timeout); + checks["api-gateway"] = res.ok ? "ok" : "fail"; + } catch { + checks["api-gateway"] = "fail"; + } + } + + const allReady = Object.values(checks).every((v) => v === "ok"); + return NextResponse.json( + { status: allReady ? "ready" : "not_ready", checks, ts: Date.now() }, + { status: allReady ? 200 : 503 }, + ); +} diff --git a/apps/parent-portal/src/app/globals.css b/apps/parent-portal/src/app/globals.css new file mode 100644 index 0000000..1c1ed72 --- /dev/null +++ b/apps/parent-portal/src/app/globals.css @@ -0,0 +1,203 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* + * parent-portal 设计令牌三层模型 + * 依据:02-architecture-design.md §9 设计令牌三层(复用 packages/ui-tokens) + * project_rules §3.10 设计令牌规范 + * + * Layer 1 Primitive:原始色板/字号/间距/阴影(业务代码不直接引用) + * Layer 2 Semantic:语义令牌(light/dark),业务代码唯一引用入口 + * Layer 3 Tailwind Theme:@theme inline 暴露为 bg-*/text-*/font-* 类 + */ + +:root { + /* ===== Layer 1 Primitive ===== */ + /* 色板(HSL,避免 #hex 字面量,project_rules §3.10)*/ + --color-paper-50: 40 20% 98%; + --color-paper-100: 40 18% 96%; + --color-ink-900: 25 3% 15%; + --color-ink-600: 30 5% 45%; + --color-ink-300: 30 10% 75%; + --color-accent-600: 220 60% 35%; + --color-accent-100: 220 60% 95%; + --color-success-600: 142 60% 35%; + --color-warning-600: 38 80% 45%; + --color-danger-600: 0 70% 45%; + + /* 字号(禁止 font-size: Npx,project_rules §3.10)*/ + --font-size-1: 0.75rem; + --font-size-2: 0.875rem; + --font-size-3: 1rem; + --font-size-4: 1.125rem; + --font-size-5: 1.25rem; + --font-size-6: 1.5rem; + --font-size-7: 1.875rem; + --font-size-8: 2.25rem; + --font-size-9: 3rem; + + /* 行高 */ + --line-height-tight: 1.25; + --line-height-normal: 1.5; + --line-height-loose: 1.75; + + /* 间距 */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.5rem; + --space-6: 2rem; + --space-8: 3rem; + --space-10: 4rem; + --space-12: 6rem; + --space-16: 8rem; + + /* 圆角 */ + --radius-sm: 0.25rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + + /* 阴影 */ + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1); + + /* ===== Layer 2 Semantic(light)===== */ + --paper: var(--color-paper-50); + --paper-elevated: var(--color-paper-100); + --ink: var(--color-ink-900); + --ink-muted: var(--color-ink-600); + --ink-subtle: var(--color-ink-300); + --accent: var(--color-accent-600); + --accent-soft: var(--color-accent-100); + --success: var(--color-success-600); + --warning: var(--color-warning-600); + --danger: var(--color-danger-600); + --rule: 30 10% 90%; + + /* 字体(禁止硬编码字体名,project_rules §3.10)*/ + --font-serif: var(--font-fraunces), Georgia, serif; + --font-sans: var(--font-inter), system-ui, sans-serif; + --font-mono: var(--font-jetbrains-mono), monospace; +} + +[data-theme="dark"] { + /* ===== Layer 2 Semantic(dark)===== */ + --paper: 220 15% 12%; + --paper-elevated: 220 15% 16%; + --ink: 40 20% 95%; + --ink-muted: 40 10% 65%; + --ink-subtle: 40 5% 45%; + --accent: 220 70% 60%; + --accent-soft: 220 70% 20%; + --success: 142 60% 50%; + --warning: 38 80% 55%; + --danger: 0 70% 55%; + --rule: 220 10% 25%; +} + +/* ===== 全局基础样式 ===== */ +html, +body { + background: hsl(var(--paper)); + color: hsl(var(--ink)); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: var(--font-serif); + font-weight: 600; + letter-spacing: -0.01em; +} + +/* 纸感分隔线(复用 teacher-portal 风格)*/ +.rule { + border-top: 1px solid hsl(var(--rule)); +} + +.rule-thin { + border-top: 2px solid hsl(var(--rule)); +} + +.mark-left { + border-left: 2px solid hsl(var(--rule)); + padding-left: 12px; +} + +/* 可访问性:聚焦轮廓(WCAG 2.2 AA)*/ +:focus-visible { + outline: 2px solid hsl(var(--accent)); + outline-offset: 2px; +} + +/* 跳转到主内容链接(a11y)*/ +.skip-link { + position: absolute; + top: -40px; + left: 0; + background: hsl(var(--accent)); + color: hsl(var(--paper)); + padding: 8px 16px; + z-index: 100; + border-radius: 0 0 var(--radius-md) 0; +} + +.skip-link:focus { + top: 0; +} + +/* 骨架屏动画 */ +@keyframes skeleton-pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.skeleton { + background: hsl(var(--rule)); + animation: skeleton-pulse 1.5s ease-in-out infinite; + border-radius: var(--radius-sm); +} + +/* 滚动条样式(Webkit)*/ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: hsl(var(--paper)); +} + +::-webkit-scrollbar-thumb { + background: hsl(var(--ink-subtle)); + border-radius: var(--radius-sm); +} + +::-webkit-scrollbar-thumb:hover { + background: hsl(var(--ink-muted)); +} + +/* 禁用动画(用户偏好)*/ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/apps/parent-portal/src/app/layout.tsx b/apps/parent-portal/src/app/layout.tsx new file mode 100644 index 0000000..71842ed --- /dev/null +++ b/apps/parent-portal/src/app/layout.tsx @@ -0,0 +1,49 @@ +// RootLayout:字体 + 全局样式 + Providers +// 依据:02-architecture-design.md §9 设计令牌三层、project_rules §3.10 设计令牌规范 +// - 字体:Inter(sans)/ Fraunces(serif)/ JetBrains Mono(mono),经 next/font 加载 +// - 全局样式:globals.css(设计令牌 + 基础样式 + a11y) +// - Providers:QueryClient + Urql + MSW + +import "./globals.css"; +import type { Metadata, Viewport } from "next"; +import { Inter, Fraunces, JetBrains_Mono } from "next/font/google"; +import { Providers } from "./providers"; + +const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); +const fraunces = Fraunces({ subsets: ["latin"], variable: "--font-fraunces" }); +const mono = JetBrains_Mono({ + subsets: ["latin"], + variable: "--font-jetbrains-mono", +}); + +export const metadata: Metadata = { + title: "Edu 家长端", + description: "K12 智慧教务平台 - 家长端", + manifest: "/manifest.json", +}; + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + themeColor: "hsl(40, 20%, 98%)", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + 跳转到主内容 + + {children} + + + ); +} diff --git a/apps/parent-portal/src/app/login/login.test.tsx b/apps/parent-portal/src/app/login/login.test.tsx new file mode 100644 index 0000000..b044cef --- /dev/null +++ b/apps/parent-portal/src/app/login/login.test.tsx @@ -0,0 +1,93 @@ +// 登录页集成测试(MSW + Next.js 路由) +// 依据:02-architecture-design.md §13 测试策略、ISSUE-004 登录走 REST +// 覆盖:正确凭据登录跳转 / 错误凭据显示错误 / 加载态 + +import { + describe, + it, + expect, + beforeAll, + afterAll, + afterEach, + vi, +} from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { http, HttpResponse, delay } from "msw"; +import LoginPage from "./page"; +import { server } from "@/test/mocks/server"; +import { mockLoginResponse } from "@/test/mocks/fixtures"; + +// mock next/navigation +const mockPush = vi.fn(); +vi.mock("next/navigation", () => ({ + useRouter: () => ({ push: mockPush, replace: vi.fn(), refresh: vi.fn() }), +})); + +beforeAll(() => server.listen({ onUnhandledRequest: "error" })); +afterEach(() => { + server.resetHandlers(); + localStorage.clear(); + mockPush.mockReset(); +}); +afterAll(() => server.close()); + +describe("LoginPage 集成测试", () => { + it("渲染登录表单", () => { + render(); + expect( + screen.getByRole("heading", { name: "Edu 家长端" }), + ).toBeInTheDocument(); + expect(screen.getByLabelText("邮箱")).toBeInTheDocument(); + expect(screen.getByLabelText("密码")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "登录" })).toBeInTheDocument(); + }); + + it("默认填充 mock 账号", () => { + render(); + expect(screen.getByLabelText("邮箱")).toHaveValue("parent@example.com"); + expect(screen.getByLabelText("密码")).toHaveValue("password"); + }); + + it("正确凭据登录后跳转到 dashboard", async () => { + const user = userEvent.setup(); + render(); + await user.click(screen.getByRole("button", { name: "登录" })); + + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith("/parent/dashboard"); + }); + // token 已写入 localStorage + expect(localStorage.getItem("parent_access_token")).toBeTruthy(); + }); + + it("错误凭据显示错误信息", async () => { + const user = userEvent.setup(); + render(); + await user.clear(screen.getByLabelText("邮箱")); + await user.type(screen.getByLabelText("邮箱"), "wrong@example.com"); + await user.clear(screen.getByLabelText("密码")); + await user.type(screen.getByLabelText("密码"), "wrong"); + await user.click(screen.getByRole("button", { name: "登录" })); + + await waitFor(() => { + expect(screen.getByRole("alert")).toHaveTextContent(/邮箱或密码错误/); + }); + expect(mockPush).not.toHaveBeenCalled(); + }); + + it("登录中显示加载态", async () => { + // 使用延迟响应以观察加载态 + server.use( + http.post("/api/v1/iam/login", async () => { + await delay(200); + return HttpResponse.json({ success: true, data: mockLoginResponse }); + }), + ); + const user = userEvent.setup(); + render(); + await user.click(screen.getByRole("button", { name: "登录" })); + // 按钮文本变为"登录中..." + expect(screen.getByRole("button")).toHaveTextContent(/登录中/); + }); +}); diff --git a/apps/parent-portal/src/app/login/page.tsx b/apps/parent-portal/src/app/login/page.tsx new file mode 100644 index 0000000..e6fc339 --- /dev/null +++ b/apps/parent-portal/src/app/login/page.tsx @@ -0,0 +1,102 @@ +// 登录页(独立模式 / MF 未启用时使用) +// 依据:02-architecture-design.md §3.1 路由结构、ISSUE-004 登录走 REST +// MF 启用时由 Shell 提供登录;独立模式由本页提供 +// mock 账号:parent@example.com / password + +"use client"; + +import { useState, type FormEvent } from "react"; +import { useRouter } from "next/navigation"; +import { login } from "@/lib/auth"; +import { cn } from "@/lib/utils"; + +export default function LoginPage() { + const router = useRouter(); + const [email, setEmail] = useState("parent@example.com"); + const [password, setPassword] = useState("password"); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e: FormEvent) { + e.preventDefault(); + setError(null); + setLoading(true); + try { + await login(email, password); + router.push("/parent/dashboard"); + } catch (err) { + setError(err instanceof Error ? err.message : "登录失败"); + } finally { + setLoading(false); + } + } + + return ( +
+
+
+

Edu 家长端

+

登录查看子女学习情况

+
+ +
+
+ + setEmail(e.target.value)} + className="w-full rounded border border-rule bg-paper px-3 py-2 text-sm outline-none focus:border-accent" + /> +
+ +
+ + setPassword(e.target.value)} + className="w-full rounded border border-rule bg-paper px-3 py-2 text-sm outline-none focus:border-accent" + /> +
+ + {error && ( + + )} + + +
+ +

+ mock 账号:parent@example.com / password +

+
+
+ ); +} diff --git a/apps/parent-portal/src/app/page.tsx b/apps/parent-portal/src/app/page.tsx new file mode 100644 index 0000000..5f0f94c --- /dev/null +++ b/apps/parent-portal/src/app/page.tsx @@ -0,0 +1,11 @@ +// 首页:重定向到仪表盘或登录页 +// 依据:02-architecture-design.md §3.1 路由结构 +// MF Remote 模式下,Shell 会直接挂载 /parent/* 路由;独立模式下首页负责重定向 + +import { redirect } from "next/navigation"; + +export default function HomePage(): never { + // 客户端导航由 middleware 处理认证;首页直接重定向到 dashboard + // 未认证时 dashboard 路由会再次重定向到 /login + redirect("/parent/dashboard"); +} diff --git a/apps/parent-portal/src/app/parent/attendance/page.tsx b/apps/parent-portal/src/app/parent/attendance/page.tsx new file mode 100644 index 0000000..9970c7c --- /dev/null +++ b/apps/parent-portal/src/app/parent/attendance/page.tsx @@ -0,0 +1,33 @@ +// 考勤页面 +// 依据:02-architecture-design.md §3.1 路由结构 +// 路由:/parent/attendance + +"use client"; + +import { useChildAttendance } from "@/hooks/useChildAttendance"; +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { AttendanceCalendar } from "@/components/AttendanceCalendar"; + +export default function AttendancePage() { + const { currentChild } = useChildSwitcher(); + const { attendance, loading, error } = useChildAttendance(); + + if (loading) { + return ; + } + + if (error) { + return ( +
+ 加载失败:{error.message} +
+ ); + } + + return ( +
+

{currentChild?.name}的考勤

+ +
+ ); +} diff --git a/apps/parent-portal/src/app/parent/dashboard/page.tsx b/apps/parent-portal/src/app/parent/dashboard/page.tsx new file mode 100644 index 0000000..500fb34 --- /dev/null +++ b/apps/parent-portal/src/app/parent/dashboard/page.tsx @@ -0,0 +1,9 @@ +// 仪表盘页面 +// 依据:02-architecture-design.md §3.1 路由结构 +// 路由:/parent/dashboard + +import { ParentDashboard } from "@/components/ParentDashboard"; + +export default function DashboardPage() { + return ; +} diff --git a/apps/parent-portal/src/app/parent/grades/page.tsx b/apps/parent-portal/src/app/parent/grades/page.tsx new file mode 100644 index 0000000..23b6fd0 --- /dev/null +++ b/apps/parent-portal/src/app/parent/grades/page.tsx @@ -0,0 +1,85 @@ +// 成绩页面 +// 依据:02-architecture-design.md §3.1 路由结构 +// 路由:/parent/grades + +"use client"; + +import { useChildGrades } from "@/hooks/useChildGrades"; +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { ChildGradeChart } from "@/components/ChildGradeChart"; +import { formatDate, scoreToGrade, gradeToChinese, cn } from "@/lib/utils"; + +export default function GradesPage() { + const { currentChild } = useChildSwitcher(); + const { grades, loading, error } = useChildGrades(); + + if (loading) { + return ( +
+ + +
+ ); + } + + if (error) { + return ( +
+ 加载失败:{error.message} +
+ ); + } + + return ( +
+

{currentChild?.name}的成绩

+ + {grades.length > 0 && } + +
+

成绩明细

+ + + + + + + + + + + + + {grades.map((g) => { + const grade = scoreToGrade(g.studentScore, g.classMax ?? 100); + return ( + + + + + + + + + ); + })} + +
科目考试日期分数班均等级
{g.subject}{g.examName} + {formatDate(g.examDate)} + + {g.studentScore} + + {g.classAverage ?? "-"} + + {grade} +
+
+
+ ); +} diff --git a/apps/parent-portal/src/app/parent/homework/page.tsx b/apps/parent-portal/src/app/parent/homework/page.tsx new file mode 100644 index 0000000..f4b7c67 --- /dev/null +++ b/apps/parent-portal/src/app/parent/homework/page.tsx @@ -0,0 +1,120 @@ +// 作业页面 +// 依据:02-architecture-design.md §3.1 路由结构 +// 路由:/parent/homework + +"use client"; + +import { useState } from "react"; +import { useChildHomework } from "@/hooks/useChildHomework"; +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { formatDate, cn } from "@/lib/utils"; +import type { HomeworkItem } from "@/types"; + +const STATUS_LABELS: Record = { + not_started: "未开始", + in_progress: "进行中", + submitted: "已提交", + graded: "已评分", +}; + +const STATUS_STYLES: Record = { + not_started: "text-danger", + in_progress: "text-warning", + submitted: "text-ink-muted", + graded: "text-success", +}; + +const FILTERS = [ + { value: "", label: "全部" }, + { value: "not_started", label: "未开始" }, + { value: "in_progress", label: "进行中" }, + { value: "submitted", label: "已提交" }, + { value: "graded", label: "已评分" }, +]; + +export default function HomeworkPage() { + const { currentChild } = useChildSwitcher(); + const [filter, setFilter] = useState(""); + const { homework, loading, error } = useChildHomework(filter || undefined); + + if (loading) { + return ; + } + + if (error) { + return ( +
+ 加载失败:{error.message} +
+ ); + } + + return ( +
+

{currentChild?.name}的作业

+ +
+ {FILTERS.map((f) => ( + + ))} +
+ +
+ {homework.length === 0 ? ( +

暂无作业

+ ) : ( + homework.map((hw) => ( +
+
+
+

{hw.title}

+

+ {hw.subject} · {hw.className} +

+
+ + {STATUS_LABELS[hw.status]} + +
+
+ 布置:{formatDate(hw.assignedDate)} + 截止:{formatDate(hw.dueDate)} + {hw.score !== undefined && ( + + 得分:{hw.score}{" "} + / {hw.maxScore} + + )} +
+ {hw.feedback && ( +

+ {hw.feedback} +

+ )} +
+ )) + )} +
+
+ ); +} diff --git a/apps/parent-portal/src/app/parent/layout.tsx b/apps/parent-portal/src/app/parent/layout.tsx new file mode 100644 index 0000000..0439733 --- /dev/null +++ b/apps/parent-portal/src/app/parent/layout.tsx @@ -0,0 +1,51 @@ +// parent 布局:认证守卫 + AppShell + MultiChildTabBar +// 依据:02-architecture-design.md §3.1 路由结构、§6 应用外壳 +// - 未认证重定向到 /login +// - MF 启用时由 Shell 提供外壳(本布局的 AppShell 仅独立模式使用) + +"use client"; + +import { type ReactNode, useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { isAuthenticated } from "@/lib/auth"; +import { AppShell } from "@/components/AppShell"; +import { MultiChildTabBar } from "@/components/MultiChildTabBar"; +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { useCrossTabSync } from "@/hooks/useCrossTabSync"; +import { EmptyChildState } from "@/components/EmptyChildState"; + +export default function ParentLayout({ children }: { children: ReactNode }) { + const router = useRouter(); + const [checked, setChecked] = useState(false); + const { children: childList, loading } = useChildSwitcher(); + useCrossTabSync(); + + useEffect(() => { + if (!isAuthenticated()) { + router.replace("/login"); + return; + } + setChecked(true); + }, [router]); + + if (!checked) { + return ( +
+ +
+ ); + } + + return ( + + {childList.length === 0 && !loading ? ( + + ) : ( + <> + + {children} + + )} + + ); +} diff --git a/apps/parent-portal/src/app/parent/notifications/page.tsx b/apps/parent-portal/src/app/parent/notifications/page.tsx new file mode 100644 index 0000000..663b755 --- /dev/null +++ b/apps/parent-portal/src/app/parent/notifications/page.tsx @@ -0,0 +1,17 @@ +// 通知中心页面 +// 依据:02-architecture-design.md §3.1 路由结构、§15.7 通知列表 +// 路由:/parent/notifications +// P5 增强:WebSocket 实时推送 + HTTP 轮询降级 + +"use client"; + +import { NotificationFeed } from "@/components/NotificationFeed"; + +export default function NotificationsPage() { + return ( +
+

通知中心

+ +
+ ); +} diff --git a/apps/parent-portal/src/app/parent/preferences/page.tsx b/apps/parent-portal/src/app/parent/preferences/page.tsx new file mode 100644 index 0000000..6f560ea --- /dev/null +++ b/apps/parent-portal/src/app/parent/preferences/page.tsx @@ -0,0 +1,17 @@ +// 通知偏好页面 +// 依据:02-architecture-design.md §3.1 路由结构、§14 通知偏好 +// 路由:/parent/preferences +// ISSUE-033:P4 localStorage 降级 + +"use client"; + +import { PreferenceForm } from "@/components/PreferenceForm"; + +export default function PreferencesPage() { + return ( +
+

通知偏好

+ +
+ ); +} diff --git a/apps/parent-portal/src/app/parent/weakness/page.tsx b/apps/parent-portal/src/app/parent/weakness/page.tsx new file mode 100644 index 0000000..e9a4693 --- /dev/null +++ b/apps/parent-portal/src/app/parent/weakness/page.tsx @@ -0,0 +1,84 @@ +// 学情页面 +// 依据:02-architecture-design.md §3.1 路由结构 +// 路由:/parent/weakness + +"use client"; + +import { useChildWeakness } from "@/hooks/useChildWeakness"; +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { formatPercent, cn } from "@/lib/utils"; + +export default function WeaknessPage() { + const { currentChild } = useChildSwitcher(); + const { weaknesses, loading, error } = useChildWeakness(); + + if (loading) { + return ; + } + + if (error) { + return ( +
+ 加载失败:{error.message} +
+ ); + } + + return ( +
+

{currentChild?.name}的学情分析

+ +
+ {weaknesses.length === 0 ? ( +

暂无学情数据

+ ) : ( + weaknesses.map((wp) => { + const mastery = formatPercent(wp.masteryLevel); + const isWeak = wp.masteryLevel < 0.6; + return ( +
+
+
+

+ {wp.knowledgePoint} +

+

{wp.subject}

+
+
+

+ {mastery} +

+

掌握度

+
+
+ {/* 掌握度进度条 */} +
+
+
+ {wp.recommendation && ( +

+ {wp.recommendation} +

+ )} +
+ ); + }) + )} +
+
+ ); +} diff --git a/apps/parent-portal/src/app/providers.tsx b/apps/parent-portal/src/app/providers.tsx new file mode 100644 index 0000000..40f28e1 --- /dev/null +++ b/apps/parent-portal/src/app/providers.tsx @@ -0,0 +1,69 @@ +// 客户端 Providers:QueryClient + Urql + MSW 初始化 +// 依据:02-architecture-design.md §4.3 状态管理分层、§8 MSW +// - NEXT_PUBLIC_API_MOCKING=enabled 时启动 MSW worker +// - QueryClient 单例(useState 保持稳定) +// - Urql client 单例 + +"use client"; + +import { useState, useEffect, type ReactNode } from "react"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { Provider as UrqlProvider } from "urql"; +import { createQueryClient } from "@/lib/query-client"; +import { getGraphQLClient } from "@/lib/graphql-client"; + +const isMockingEnabled = process.env.NEXT_PUBLIC_API_MOCKING === "enabled"; + +interface ProvidersProps { + children: ReactNode; +} + +export function Providers({ children }: ProvidersProps) { + const [queryClient] = useState(() => createQueryClient()); + const [urqlClient] = useState(() => getGraphQLClient()); + const [mswReady, setMswReady] = useState(!isMockingEnabled); + + // MSW 初始化(仅浏览器 + mock 开启时) + useEffect(() => { + if (!isMockingEnabled || typeof window === "undefined") return; + let active = true; + (async () => { + try { + const { worker } = await import("@/test/mocks/browser"); + await worker.start({ + onUnhandledRequest: "bypass", + serviceWorker: { + url: "/mockServiceWorker.js", + }, + }); + if (active) setMswReady(true); + } catch (err) { + // MSW 启动失败不阻塞应用 + console.warn("[MSW] 启动失败,将使用真实 API", err); + if (active) setMswReady(true); + } + })(); + return () => { + active = false; + }; + }, []); + + // MSW 未就绪时显示加载态(避免 mock 数据未注入时闪烁) + if (!mswReady) { + return ( +
+ +
+ ); + } + + return ( + + {children} + + ); +} diff --git a/apps/parent-portal/src/components/AppShell.tsx b/apps/parent-portal/src/components/AppShell.tsx new file mode 100644 index 0000000..a48de18 --- /dev/null +++ b/apps/parent-portal/src/components/AppShell.tsx @@ -0,0 +1,126 @@ +// AppShell:应用外壳(独立模式) +// 依据:02-architecture-design.md §6 应用外壳、F10 MF 暴露 AppShell 整体 +// - MF 启用时由 Shell(teacher-portal) 提供外壳,本组件仅独立模式使用 +// - 布局:顶部头部 + 左侧导航 + 中间内容区 +// - 头部含 ChildSwitcher + 用户菜单 + +"use client"; + +import { type ReactNode } from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { ChildSwitcher } from "./ChildSwitcher"; +import { getUser, logout } from "@/lib/auth"; +import { PERMISSIONS } from "@/lib/permissions"; +import { usePermission } from "@/hooks/usePermission"; +import { cn } from "@/lib/utils"; + +interface NavItem { + label: string; + href: string; + permission: string; +} + +const NAV_ITEMS: NavItem[] = [ + { + label: "仪表盘", + href: "/parent/dashboard", + permission: PERMISSIONS.DASHBOARD_VIEW, + }, + { + label: "成绩", + href: "/parent/grades", + permission: PERMISSIONS.CHILD_GRADE_VIEW, + }, + { + label: "考勤", + href: "/parent/attendance", + permission: PERMISSIONS.CHILD_ATTENDANCE_VIEW, + }, + { + label: "作业", + href: "/parent/homework", + permission: PERMISSIONS.CHILD_HOMEWORK_VIEW, + }, + { + label: "学情", + href: "/parent/weakness", + permission: PERMISSIONS.CHILD_WEAKNESS_VIEW, + }, + { + label: "通知", + href: "/parent/notifications", + permission: PERMISSIONS.NOTIFICATION_VIEW, + }, + { + label: "偏好", + href: "/parent/preferences", + permission: PERMISSIONS.NOTIFICATION_PREFERENCE_UPDATE, + }, +]; + +export function AppShell({ children }: { children: ReactNode }) { + const pathname = usePathname(); + const user = getUser(); + const { hasPermission } = usePermission(); + + return ( +
+ {/* 头部 */} +
+
+ + Edu 家长端 + + +
+
+ {user?.name} + +
+
+ +
+ {/* 左侧导航 */} + + + {/* 主内容区 */} +
+ {children} +
+
+
+ ); +} + +export default AppShell; diff --git a/apps/parent-portal/src/components/AttendanceCalendar.tsx b/apps/parent-portal/src/components/AttendanceCalendar.tsx new file mode 100644 index 0000000..80ee5bb --- /dev/null +++ b/apps/parent-portal/src/components/AttendanceCalendar.tsx @@ -0,0 +1,143 @@ +// AttendanceCalendar:月度考勤日历 +// 依据:02-architecture-design.md §15.5 AttendanceCalendar 设计 +// - 月历视图,每天用颜色点标记考勤状态 +// - present=success / late=warning / absent=danger / leave=ink-subtle + +"use client"; + +import { useMemo } from "react"; +import type { AttendanceRecord } from "@/types"; +import { cn } from "@/lib/utils"; + +interface AttendanceCalendarProps { + records: AttendanceRecord[]; +} + +const STATUS_STYLES: Record = { + present: "bg-success", + late: "bg-warning", + absent: "bg-danger", + leave: "bg-ink-subtle", +}; + +const STATUS_LABELS: Record = { + present: "出勤", + late: "迟到", + absent: "缺勤", + leave: "请假", +}; + +const WEEKDAYS = ["日", "一", "二", "三", "四", "五", "六"]; + +export function AttendanceCalendar({ records }: AttendanceCalendarProps) { + const { year, month, days, recordMap } = useMemo(() => { + const now = new Date(); + const y = now.getFullYear(); + const m = now.getMonth(); + const firstDay = new Date(y, m, 1); + const lastDay = new Date(y, m + 1, 0); + const startWeekday = firstDay.getDay(); + const totalDays = lastDay.getDate(); + + const dayCells: (number | null)[] = []; + for (let i = 0; i < startWeekday; i++) dayCells.push(null); + for (let d = 1; d <= totalDays; d++) dayCells.push(d); + while (dayCells.length % 7 !== 0) dayCells.push(null); + + const map = new Map(); + for (const r of records) { + map.set(r.date, r); + } + + return { year: y, month: m, days: dayCells, recordMap: map }; + }, [records]); + + // 统计 + const stats = useMemo(() => { + const counts = { present: 0, late: 0, absent: 0, leave: 0 }; + for (const r of records) { + counts[r.status]++; + } + return counts; + }, [records]); + + return ( +
+
+

+ {year}年{month + 1}月考勤 +

+
+ {(Object.keys(STATUS_LABELS) as AttendanceRecord["status"][]).map( + (s) => ( + + + {STATUS_LABELS[s]} + + ), + )} +
+
+ + {/* 日历网格 */} +
+
+ {WEEKDAYS.map((w) => ( +
+ {w} +
+ ))} +
+
+ {days.map((day, idx) => { + if (day === null) { + return
; + } + const dateStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + const record = recordMap.get(dateStr); + return ( +
+ {day} + {record && ( + + )} +
+ ); + })} +
+
+ + {/* 统计 */} +
+ + 出勤 {stats.present} + + + 迟到 {stats.late} + + + 缺勤 {stats.absent} + + + 请假 {stats.leave} + +
+
+ ); +} + +export default AttendanceCalendar; diff --git a/apps/parent-portal/src/components/ChildGradeChart.tsx b/apps/parent-portal/src/components/ChildGradeChart.tsx new file mode 100644 index 0000000..e9c3d15 --- /dev/null +++ b/apps/parent-portal/src/components/ChildGradeChart.tsx @@ -0,0 +1,72 @@ +// ChildGradeChart:子女成绩图表(recharts) +// 依据:02-architecture-design.md §15.6 成绩图表设计 +// - 柱状图:学生分数 vs 班级平均 +// - 响应式容器 + +"use client"; + +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from "recharts"; +import type { GradeDataPoint } from "@/types"; + +interface ChildGradeChartProps { + grades: GradeDataPoint[]; +} + +export function ChildGradeChart({ grades }: ChildGradeChartProps) { + const data = grades.map((g) => ({ + name: g.subject, + 学生分数: g.studentScore, + 班级平均: g.classAverage ?? 0, + })); + + return ( +
+

成绩对比

+
+ + + + + + + + + + + +
+
+ ); +} + +export default ChildGradeChart; diff --git a/apps/parent-portal/src/components/ChildSummaryCard.tsx b/apps/parent-portal/src/components/ChildSummaryCard.tsx new file mode 100644 index 0000000..c26946a --- /dev/null +++ b/apps/parent-portal/src/components/ChildSummaryCard.tsx @@ -0,0 +1,119 @@ +// ChildSummaryCard:子女概览卡片 +// 依据:02-architecture-design.md §15.4 ChildSummaryCard 设计 +// - 平均分 / 班级排名 / 出勤率 / 待完成作业 +// - 成绩趋势指示 +// - 即将到来的事件 + +"use client"; + +import type { ChildSummary } from "@/types"; +import { formatPercent, formatNumber, formatDate } from "@/lib/utils"; +import { cn } from "@/lib/utils"; + +interface ChildSummaryCardProps { + summary: ChildSummary; +} + +const TREND_LABELS = { + up: { text: "上升", color: "text-success" }, + down: { text: "下降", color: "text-danger" }, + stable: { text: "稳定", color: "text-ink-muted" }, +} as const; + +const DEFAULT_TREND = { text: "稳定", color: "text-ink-muted" }; + +export function ChildSummaryCard({ summary }: ChildSummaryCardProps) { + const trend = TREND_LABELS[summary.recentGradeTrend] ?? DEFAULT_TREND; + + return ( +
+ {/* 指标网格 */} +
+ + + + 0} + /> +
+ + {/* 成绩趋势 */} +
+
+

成绩趋势

+ {trend.text} +
+
    + {summary.recentScores.slice(0, 5).map((score) => ( +
  • +
    + {score.subject} + {score.examName} +
    +
    + {score.studentScore} + {score.classAverage !== undefined && ( + + 班均 {score.classAverage} + + )} +
    +
  • + ))} +
+
+ + {/* 即将到来事件 */} + {summary.upcomingEvents && summary.upcomingEvents.length > 0 && ( +
+

即将到来

+
    + {summary.upcomingEvents.map((event) => ( +
  • + {event.title} + + {formatDate(event.dueDate)} + +
  • + ))} +
+
+ )} +
+ ); +} + +function Metric({ + label, + value, + highlight, +}: { + label: string; + value: string; + highlight?: boolean; +}) { + return ( +
+

{label}

+

{value}

+
+ ); +} + +export default ChildSummaryCard; diff --git a/apps/parent-portal/src/components/ChildSwitcher.test.tsx b/apps/parent-portal/src/components/ChildSwitcher.test.tsx new file mode 100644 index 0000000..ed72a1e --- /dev/null +++ b/apps/parent-portal/src/components/ChildSwitcher.test.tsx @@ -0,0 +1,170 @@ +// ChildSwitcher 组件单测 +// 依据:02-architecture-design.md §15.1 ChildSwitcher 设计、§13 测试策略 +// 覆盖:加载态 / 无子女空态 / 单子女显示 / 多子女下拉切换 / 键盘可访问性 + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { ChildSwitcher } from "./ChildSwitcher"; +import type { ChildInfo } from "@/types"; + +// mock useChildSwitcher hook +vi.mock("@/hooks/useChildSwitcher", () => ({ + useChildSwitcher: vi.fn(), +})); + +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; + +const mockChildren: ChildInfo[] = [ + { + id: "student-001", + name: "张小明", + grade: "grade.7", + schoolName: "实验中学", + classId: "class-7-1", + className: "初一(1)班", + }, + { + id: "student-002", + name: "张小红", + grade: "grade.5", + schoolName: "实验小学", + classId: "class-5-2", + className: "五年级(2)班", + }, +]; + +const mockUseChildSwitcher = vi.mocked(useChildSwitcher); + +beforeEach(() => { + mockUseChildSwitcher.mockReset(); +}); + +describe("ChildSwitcher 加载态", () => { + it("loading 时显示骨架屏", () => { + mockUseChildSwitcher.mockReturnValue({ + children: [], + currentChild: null, + currentChildId: null, + switchChild: vi.fn(), + loading: true, + error: undefined, + hasMultipleChildren: false, + }); + render(); + expect(screen.getByLabelText("加载子女列表")).toBeInTheDocument(); + }); +}); + +describe("ChildSwitcher 无子女", () => { + it("空列表时显示未绑定提示", () => { + mockUseChildSwitcher.mockReturnValue({ + children: [], + currentChild: null, + currentChildId: null, + switchChild: vi.fn(), + loading: false, + error: undefined, + hasMultipleChildren: false, + }); + render(); + expect(screen.getByLabelText("未绑定子女")).toBeInTheDocument(); + }); +}); + +describe("ChildSwitcher 单子女", () => { + it("仅显示子女名称(无下拉)", () => { + mockUseChildSwitcher.mockReturnValue({ + children: [mockChildren[0]!], + currentChild: mockChildren[0]!, + currentChildId: "student-001", + switchChild: vi.fn(), + loading: false, + error: undefined, + hasMultipleChildren: false, + }); + render(); + expect(screen.getByLabelText("当前子女")).toHaveTextContent("张小明"); + // 不应有下拉按钮 + expect(screen.queryByRole("button")).not.toBeInTheDocument(); + }); +}); + +describe("ChildSwitcher 多子女", () => { + beforeEach(() => { + mockUseChildSwitcher.mockReturnValue({ + children: mockChildren, + currentChild: mockChildren[0]!, + currentChildId: "student-001", + switchChild: vi.fn(), + loading: false, + error: undefined, + hasMultipleChildren: true, + }); + }); + + it("显示当前子女名称和切换按钮", () => { + render(); + const button = screen.getByRole("button", { name: "选择子女" }); + expect(button).toBeInTheDocument(); + expect(screen.getByText("张小明")).toBeInTheDocument(); + }); + + it("点击按钮展开子女列表", async () => { + const user = userEvent.setup(); + render(); + const button = screen.getByRole("button", { name: "选择子女" }); + await user.click(button); + expect( + screen.getByRole("listbox", { name: "子女列表" }), + ).toBeInTheDocument(); + expect(screen.getByText("张小红")).toBeInTheDocument(); + }); + + it("点击子女项触发 switchChild 并关闭列表", async () => { + const user = userEvent.setup(); + const switchChild = vi.fn(); + mockUseChildSwitcher.mockReturnValue({ + children: mockChildren, + currentChild: mockChildren[0]!, + currentChildId: "student-001", + switchChild, + loading: false, + error: undefined, + hasMultipleChildren: true, + }); + render(); + await user.click(screen.getByRole("button", { name: "选择子女" })); + await user.click(screen.getByText("张小红")); + expect(switchChild).toHaveBeenCalledWith("student-002"); + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + }); + + it("Escape 键关闭列表", async () => { + const user = userEvent.setup(); + render(); + const button = screen.getByRole("button", { name: "选择子女" }); + await user.click(button); + expect(screen.getByRole("listbox")).toBeInTheDocument(); + fireEvent.keyDown(button, { key: "Escape" }); + expect(screen.queryByRole("listbox")).not.toBeInTheDocument(); + }); + + it("aria-expanded 反映展开状态", async () => { + const user = userEvent.setup(); + render(); + const button = screen.getByRole("button", { name: "选择子女" }); + expect(button).toHaveAttribute("aria-expanded", "false"); + await user.click(button); + expect(button).toHaveAttribute("aria-expanded", "true"); + }); + + it("当前选中项有 aria-selected=true", async () => { + const user = userEvent.setup(); + render(); + await user.click(screen.getByRole("button", { name: "选择子女" })); + const options = screen.getAllByRole("option"); + expect(options[0]).toHaveAttribute("aria-selected", "true"); + expect(options[1]).toHaveAttribute("aria-selected", "false"); + }); +}); diff --git a/apps/parent-portal/src/components/ChildSwitcher.tsx b/apps/parent-portal/src/components/ChildSwitcher.tsx new file mode 100644 index 0000000..dca2df3 --- /dev/null +++ b/apps/parent-portal/src/components/ChildSwitcher.tsx @@ -0,0 +1,128 @@ +// ChildSwitcher:子女切换组件(MF 暴露 ./ChildSwitcher) +// 依据:02-architecture-design.md §15.1 ChildSwitcher 设计、F10 MF 暴露粒度 +// - 多子女:下拉选择器 +// - 单子女:仅显示子女名称(不可切换) +// - 无子女:显示空状态提示 +// - ISSUE-009:纯前端切换,不调后端 + +"use client"; + +import { useState, useRef, useEffect, type KeyboardEvent } from "react"; +// 修正:onKeyDown 绑定到 + + {open && ( +
    + {children.map((child) => ( +
  • + +
  • + ))} +
+ )} +
+ ); +} + +export default ChildSwitcher; diff --git a/apps/parent-portal/src/components/EmptyChildState.tsx b/apps/parent-portal/src/components/EmptyChildState.tsx new file mode 100644 index 0000000..bf70c64 --- /dev/null +++ b/apps/parent-portal/src/components/EmptyChildState.tsx @@ -0,0 +1,35 @@ +// EmptyChildState:无子女绑定的空状态 +// 依据:02-architecture-design.md §15.3 EmptyChildState 设计 +// - 家长未绑定子女时显示 +// - 提示联系学校绑定 + +"use client"; + +export function EmptyChildState() { + return ( +
+
+ +
+

尚未绑定子女

+

+ 请联系学校管理员绑定子女信息 +

+
+ ); +} + +export default EmptyChildState; diff --git a/apps/parent-portal/src/components/MultiChildTabBar.tsx b/apps/parent-portal/src/components/MultiChildTabBar.tsx new file mode 100644 index 0000000..52c1d57 --- /dev/null +++ b/apps/parent-portal/src/components/MultiChildTabBar.tsx @@ -0,0 +1,50 @@ +// MultiChildTabBar:多子女水平标签栏 +// 依据:02-architecture-design.md §15.2 MultiChildTabBar 设计 +// - 当 hasMultipleChildren 时在页面顶部显示标签栏 +// - 当前选中子女高亮 +// - ISSUE-009:纯前端切换 + +"use client"; + +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { cn } from "@/lib/utils"; + +export function MultiChildTabBar() { + const { children, currentChildId, switchChild, hasMultipleChildren } = + useChildSwitcher(); + + if (!hasMultipleChildren) return null; + + return ( + + ); +} + +export default MultiChildTabBar; diff --git a/apps/parent-portal/src/components/NotificationFeed.test.tsx b/apps/parent-portal/src/components/NotificationFeed.test.tsx new file mode 100644 index 0000000..9b59f58 --- /dev/null +++ b/apps/parent-portal/src/components/NotificationFeed.test.tsx @@ -0,0 +1,104 @@ +// NotificationFeed 集成测试(MSW + urql) +// 依据:02-architecture-design.md §13 测试策略、§8 MSW 模拟未就绪上游 +// 覆盖:GraphQL 查询渲染 / 筛选全部未读 / 标记已读 / 全部已读 mutation + +import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { + Provider as UrqlProvider, + Client, + cacheExchange, + fetchExchange, +} from "urql"; +import { graphql, HttpResponse } from "msw"; +import { NotificationFeed } from "./NotificationFeed"; +import { server } from "@/test/mocks/server"; + +// 创建测试用 urql client(无 auth,MSW 拦截 fetch) +function createTestClient(): Client { + return new Client({ + url: "/api/v1/parent/graphql", + exchanges: [cacheExchange, fetchExchange], + fetchOptions: { headers: { "X-Requested-With": "XMLHttpRequest" } }, + }); +} + +function renderWithProviders(ui: React.ReactElement) { + const client = createTestClient(); + return render({ui}); +} + +beforeAll(() => server.listen({ onUnhandledRequest: "error" })); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +describe("NotificationFeed 集成测试", () => { + it("加载完成后渲染通知列表", async () => { + renderWithProviders(); + // 等待通知加载完成(mock 返回 3 条) + await waitFor(() => { + expect(screen.getByText("成绩发布")).toBeInTheDocument(); + }); + expect(screen.getByText("作业布置")).toBeInTheDocument(); + expect(screen.getByText("学校公告")).toBeInTheDocument(); + }); + + it("显示未读数量", async () => { + renderWithProviders(); + await waitFor(() => { + expect(screen.getByText(/未读/)).toBeInTheDocument(); + }); + // mock 有 2 条未读 + expect(screen.getByText(/未读.*2/)).toBeInTheDocument(); + }); + + it("切换到未读筛选", async () => { + const user = userEvent.setup(); + renderWithProviders(); + await waitFor(() => { + expect(screen.getByText("成绩发布")).toBeInTheDocument(); + }); + + // 点击"未读"按钮(精确匹配,避免匹配通知项的 aria-label="未读") + const filterButtons = screen.getAllByRole("button"); + const unreadButton = filterButtons.find( + (b) => + b.textContent?.includes("未读") && !b.textContent.includes("成绩发布"), + )!; + await user.click(unreadButton); + + // 未读筛选下仍有 2 条(mock 固定返回,忽略 unreadOnly 参数) + await waitFor(() => { + expect(screen.getByText("成绩发布")).toBeInTheDocument(); + expect(screen.getByText("作业布置")).toBeInTheDocument(); + }); + }); + + it("点击全部标为已读触发 mutation", async () => { + const user = userEvent.setup(); + renderWithProviders(); + await waitFor(() => { + expect(screen.getByText("成绩发布")).toBeInTheDocument(); + }); + + const markAllButton = screen.getByRole("button", { name: "全部标为已读" }); + await user.click(markAllButton); + + // mutation 不应抛错(mock 返回成功) + // 无需额外断言,只要不报错即通过 + }); + + it("空通知列表显示空状态", async () => { + server.resetHandlers( + graphql.query("MyNotifications", () => + HttpResponse.json({ data: { myNotifications: [] } }), + ), + ); + + renderWithProviders(); + await waitFor(() => { + expect(screen.getByText("暂无通知")).toBeInTheDocument(); + }); + }); +}); diff --git a/apps/parent-portal/src/components/NotificationFeed.tsx b/apps/parent-portal/src/components/NotificationFeed.tsx new file mode 100644 index 0000000..c01aef1 --- /dev/null +++ b/apps/parent-portal/src/components/NotificationFeed.tsx @@ -0,0 +1,133 @@ +// NotificationFeed:通知列表组件 +// 依据:02-architecture-design.md §15.7 通知列表设计 +// - 通知列表 + 标记已读 + 全部已读 +// - 支持未读筛选 + +"use client"; + +import { useState } from "react"; +import { useMyNotifications } from "@/hooks/useMyNotifications"; +import { formatRelativeTime, cn } from "@/lib/utils"; +import type { NotificationItem } from "@/types"; + +const EVENT_TYPE_LABELS: Record = { + grade_recorded: "成绩发布", + homework_graded: "作业评分", + homework_assigned: "作业布置", + exam_published: "考试发布", + attendance_alert: "考勤提醒", + school_announcement: "学校公告", + teacher_message: "老师消息", + fee_reminder: "缴费提醒", + event_invitation: "活动邀请", +}; + +export function NotificationFeed() { + const [unreadOnly, setUnreadOnly] = useState(false); + const { notifications, loading, markAsRead, markAllAsRead } = + useMyNotifications(unreadOnly); + + const unreadCount = notifications.filter((n) => !n.read).length; + + async function handleMarkAll() { + await markAllAsRead(); + } + + async function handleClick(n: NotificationItem) { + if (!n.read) { + await markAsRead(n.id); + } + } + + if (loading) { + return ; + } + + return ( +
+
+
+ + +
+ {unreadCount > 0 && ( + + )} +
+ +
    + {notifications.length === 0 ? ( +
  • 暂无通知
  • + ) : ( + notifications.map((n) => ( +
  • + +
  • + )) + )} +
+
+ ); +} + +export default NotificationFeed; diff --git a/apps/parent-portal/src/components/ParentDashboard.tsx b/apps/parent-portal/src/components/ParentDashboard.tsx new file mode 100644 index 0000000..332d776 --- /dev/null +++ b/apps/parent-portal/src/components/ParentDashboard.tsx @@ -0,0 +1,61 @@ +// ParentDashboard:仪表盘复合组件 +// 依据:02-architecture-design.md §15 仪表盘设计 +// - 组合 ChildSummaryCard + AttendanceCalendar +// - 处理加载/错误状态 +// - 依赖 currentChildId(切换子女时重新加载) + +"use client"; + +import { useChildSummary } from "@/hooks/useChildSummary"; +import { useChildAttendance } from "@/hooks/useChildAttendance"; +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { ChildSummaryCard } from "./ChildSummaryCard"; +import { AttendanceCalendar } from "./AttendanceCalendar"; + +export function ParentDashboard() { + const { currentChild } = useChildSwitcher(); + const { + summary, + loading: summaryLoading, + error: summaryError, + } = useChildSummary(); + const { attendance, loading: attLoading } = useChildAttendance(); + + if (summaryLoading && !summary) { + return ( +
+ + + +
+ ); + } + + if (summaryError) { + return ( +
+ 加载失败:{summaryError.message} +
+ ); + } + + if (!summary || !currentChild) { + return null; + } + + return ( +
+

{currentChild.name}的学习概览

+ + + + {attLoading ? ( + + ) : ( + + )} +
+ ); +} + +export default ParentDashboard; diff --git a/apps/parent-portal/src/components/PreferenceForm.tsx b/apps/parent-portal/src/components/PreferenceForm.tsx new file mode 100644 index 0000000..7114343 --- /dev/null +++ b/apps/parent-portal/src/components/PreferenceForm.tsx @@ -0,0 +1,155 @@ +// PreferenceForm:通知偏好表单 +// 依据:02-architecture-design.md §14 通知偏好数据模型、§15.8 PreferenceForm 设计 +// - ISSUE-033:P4 localStorage 降级,更新立即写入 localStorage +// - 三维矩阵:子女 × 事件类型 × 渠道 +// - 切换子女标签查看不同子女的偏好 + +"use client"; + +import { useState } from "react"; +import { useNotificationPreferences } from "@/hooks/useNotificationPreferences"; +import { useChildSwitcher } from "@/hooks/useChildSwitcher"; +import { getUser } from "@/lib/auth"; +import { + NOTIFICATION_EVENT_TYPES, + NOTIFICATION_CHANNELS, +} from "@/lib/schemas/notification-preferences"; +import type { NotificationEventType, NotificationChannel } from "@/types"; +import { cn } from "@/lib/utils"; + +const EVENT_LABELS: Record = { + grade_recorded: "成绩发布", + homework_graded: "作业评分", + homework_assigned: "作业布置", + exam_published: "考试发布", + attendance_alert: "考勤提醒", + school_announcement: "学校公告", + teacher_message: "老师消息", + fee_reminder: "缴费提醒", + event_invitation: "活动邀请", +}; + +const CHANNEL_LABELS: Record = { + in_app: "应用内", + push: "推送", + sms: "短信", + email: "邮件", + wechat: "微信", +}; + +export function PreferenceForm() { + const user = getUser(); + const parentId = user?.id ?? "parent-001"; + const { preferences, loading, updatePreference } = + useNotificationPreferences(parentId); + const { children, currentChildId } = useChildSwitcher(); + const [activeChildId, setActiveChildId] = useState( + currentChildId ?? "*", + ); + + if (loading || !preferences) { + return ; + } + + // 获取当前激活子女的偏好("*" 表示全局默认) + const scope = + activeChildId === "*" + ? preferences.defaults + : (preferences.preferences[activeChildId] ?? preferences.defaults); + + return ( +
+ {/* 子女切换标签 */} +
+ + {children.map((child) => ( + + ))} +
+ + {/* 偏好矩阵表格 */} +
+ + + + + {NOTIFICATION_CHANNELS.map((ch) => ( + + ))} + + + + {NOTIFICATION_EVENT_TYPES.map((eventType) => { + const pref = scope[eventType] ?? {}; + return ( + + + {NOTIFICATION_CHANNELS.map((channel) => { + const checked = pref[channel] ?? false; + return ( + + ); + })} + + ); + })} + +
事件 + {CHANNEL_LABELS[ch]} +
+ {EVENT_LABELS[eventType]} + + + updatePreference( + activeChildId, + eventType, + channel, + e.target.checked, + ) + } + className="h-4 w-4 rounded border-rule accent-accent" + /> +
+
+ +

+ 偏好设置自动保存到本地(P4 localStorage 降级),后端就绪后将自动同步。 +

+
+ ); +} + +export default PreferenceForm; diff --git a/apps/parent-portal/src/hooks/useChildAttendance.ts b/apps/parent-portal/src/hooks/useChildAttendance.ts new file mode 100644 index 0000000..f5272e0 --- /dev/null +++ b/apps/parent-portal/src/hooks/useChildAttendance.ts @@ -0,0 +1,41 @@ +// useChildAttendance:获取子女考勤记录 +// 依据:02-architecture-design.md §4.2 GraphQL 接入 + +"use client"; + +import { useMemo } from "react"; +import { useQuery } from "urql"; +import { CHILD_ATTENDANCE } from "@/lib/graphql/operations"; +import type { AttendanceRecord } from "@/types"; +import { useChildStore } from "@/store/child-store"; + +interface ChildAttendanceResponse { + childAttendance: AttendanceRecord[]; +} + +function getCurrentMonthRange(): { startDate: string; endDate: string } { + const now = new Date(); + const start = new Date(now.getFullYear(), now.getMonth(), 1); + const end = new Date(now.getFullYear(), now.getMonth() + 1, 0); + return { + startDate: start.toISOString().slice(0, 10), + endDate: end.toISOString().slice(0, 10), + }; +} + +export function useChildAttendance() { + const currentChildId = useChildStore((s) => s.currentChildId); + const { startDate, endDate } = useMemo(getCurrentMonthRange, []); + + const [result] = useQuery({ + query: CHILD_ATTENDANCE, + variables: { childId: currentChildId, startDate, endDate }, + pause: !currentChildId, + }); + + return { + attendance: result.data?.childAttendance ?? [], + loading: result.fetching, + error: result.error, + }; +} diff --git a/apps/parent-portal/src/hooks/useChildGrades.ts b/apps/parent-portal/src/hooks/useChildGrades.ts new file mode 100644 index 0000000..a7a2d3f --- /dev/null +++ b/apps/parent-portal/src/hooks/useChildGrades.ts @@ -0,0 +1,29 @@ +// useChildGrades:获取子女成绩列表 +// 依据:02-architecture-design.md §4.2 GraphQL 接入 + +"use client"; + +import { useQuery } from "urql"; +import { CHILD_GRADES } from "@/lib/graphql/operations"; +import type { GradeDataPoint } from "@/types"; +import { useChildStore } from "@/store/child-store"; + +interface ChildGradesResponse { + childGrades: GradeDataPoint[]; +} + +export function useChildGrades(subject?: string) { + const currentChildId = useChildStore((s) => s.currentChildId); + + const [result] = useQuery({ + query: CHILD_GRADES, + variables: { childId: currentChildId, subject: subject ?? null }, + pause: !currentChildId, + }); + + return { + grades: result.data?.childGrades ?? [], + loading: result.fetching, + error: result.error, + }; +} diff --git a/apps/parent-portal/src/hooks/useChildHomework.ts b/apps/parent-portal/src/hooks/useChildHomework.ts new file mode 100644 index 0000000..7ae570a --- /dev/null +++ b/apps/parent-portal/src/hooks/useChildHomework.ts @@ -0,0 +1,29 @@ +// useChildHomework:获取子女作业列表 +// 依据:02-architecture-design.md §4.2 GraphQL 接入 + +"use client"; + +import { useQuery } from "urql"; +import { CHILD_HOMEWORK } from "@/lib/graphql/operations"; +import type { HomeworkItem } from "@/types"; +import { useChildStore } from "@/store/child-store"; + +interface ChildHomeworkResponse { + childHomework: HomeworkItem[]; +} + +export function useChildHomework(status?: string) { + const currentChildId = useChildStore((s) => s.currentChildId); + + const [result] = useQuery({ + query: CHILD_HOMEWORK, + variables: { childId: currentChildId, status: status ?? null }, + pause: !currentChildId, + }); + + return { + homework: result.data?.childHomework ?? [], + loading: result.fetching, + error: result.error, + }; +} diff --git a/apps/parent-portal/src/hooks/useChildSummary.ts b/apps/parent-portal/src/hooks/useChildSummary.ts new file mode 100644 index 0000000..1b82a32 --- /dev/null +++ b/apps/parent-portal/src/hooks/useChildSummary.ts @@ -0,0 +1,31 @@ +// useChildSummary:获取子女仪表盘概览 +// 依据:02-architecture-design.md §4.2 GraphQL 接入 +// - 依赖 currentChildId(从 Zustand store 读取) +// - 切换子女时自动重新请求 + +"use client"; + +import { useQuery } from "urql"; +import { CHILD_SUMMARY } from "@/lib/graphql/operations"; +import type { ChildSummary } from "@/types"; +import { useChildStore } from "@/store/child-store"; + +interface ChildSummaryResponse { + childSummary: ChildSummary; +} + +export function useChildSummary() { + const currentChildId = useChildStore((s) => s.currentChildId); + + const [result] = useQuery({ + query: CHILD_SUMMARY, + variables: { childId: currentChildId }, + pause: !currentChildId, + }); + + return { + summary: result.data?.childSummary ?? null, + loading: result.fetching, + error: result.error, + }; +} diff --git a/apps/parent-portal/src/hooks/useChildSwitcher.ts b/apps/parent-portal/src/hooks/useChildSwitcher.ts new file mode 100644 index 0000000..78b5cf8 --- /dev/null +++ b/apps/parent-portal/src/hooks/useChildSwitcher.ts @@ -0,0 +1,30 @@ +// useChildSwitcher:子女切换 Hook +// 依据:02-architecture-design.md §4.3 状态管理分层、ISSUE-009 纯前端切换 +// - 封装 Zustand store,提供派生数据(currentChild / hasMultipleChildren) +// - 切换是纯前端操作,不调后端(ISSUE-009) +// - 自动加载子女列表(useMyChildren) + +"use client"; + +import { useChildStore } from "@/store/child-store"; +import { useMyChildren } from "./useMyChildren"; + +export function useChildSwitcher() { + const { children, loading, error } = useMyChildren(); + + const currentChildId = useChildStore((s) => s.currentChildId); + const switchChild = useChildStore((s) => s.switchChild); + + const currentChild = children.find((c) => c.id === currentChildId) ?? null; + const hasMultipleChildren = children.filter((c) => !c.isArchived).length > 1; + + return { + children, + currentChild, + currentChildId, + switchChild, + loading, + error, + hasMultipleChildren, + }; +} diff --git a/apps/parent-portal/src/hooks/useChildWeakness.ts b/apps/parent-portal/src/hooks/useChildWeakness.ts new file mode 100644 index 0000000..0efaaa1 --- /dev/null +++ b/apps/parent-portal/src/hooks/useChildWeakness.ts @@ -0,0 +1,29 @@ +// useChildWeakness:获取子女学情薄弱点 +// 依据:02-architecture-design.md §4.2 GraphQL 接入 + +"use client"; + +import { useQuery } from "urql"; +import { CHILD_WEAKNESS } from "@/lib/graphql/operations"; +import type { WeaknessPoint } from "@/types"; +import { useChildStore } from "@/store/child-store"; + +interface ChildWeaknessResponse { + childWeakness: WeaknessPoint[]; +} + +export function useChildWeakness() { + const currentChildId = useChildStore((s) => s.currentChildId); + + const [result] = useQuery({ + query: CHILD_WEAKNESS, + variables: { childId: currentChildId }, + pause: !currentChildId, + }); + + return { + weaknesses: result.data?.childWeakness ?? [], + loading: result.fetching, + error: result.error, + }; +} diff --git a/apps/parent-portal/src/hooks/useCrossTabSync.ts b/apps/parent-portal/src/hooks/useCrossTabSync.ts new file mode 100644 index 0000000..83abf84 --- /dev/null +++ b/apps/parent-portal/src/hooks/useCrossTabSync.ts @@ -0,0 +1,97 @@ +// useCrossTabSync:跨标签同步 Hook +// 依据:02-architecture-design.md §4.4 跨标签同步 +// - BroadcastChannel:同源标签间实时同步(首选) +// - storage 事件:降级方案(BroadcastChannel 不可用时) +// - 同步内容:子女切换 / 子女解绑 / 通知偏好更新 +// - 防回环:消息携带 source,接收方忽略自己的消息 + +"use client"; + +import { useEffect } from "react"; +import { useChildStore } from "@/store/child-store"; +import type { SyncMessage } from "@/types"; + +const CHANNEL_NAME = "parent-sync"; +const STORAGE_EVENT_KEY = "parent_sync_event"; +const SOURCE_ID = `tab-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + +export function useCrossTabSync() { + useEffect(() => { + // 方案 1:BroadcastChannel(首选) + let bc: BroadcastChannel | null = null; + if ("BroadcastChannel" in window) { + bc = new BroadcastChannel(CHANNEL_NAME); + bc.onmessage = (event: MessageEvent) => { + const msg = event.data; + if (!msg || msg.source === SOURCE_ID) return; // 防回环 + + switch (msg.type) { + case "child-switched": + // 同步子女切换(不广播,避免循环) + useChildStore.setState({ currentChildId: msg.childId }); + break; + case "child-unbound": + // 子女解绑:刷新子女列表 + // 由各页面自行处理(useMyChildren 会重新请求) + break; + case "preferences-updated": + // 通知偏好更新:清除 localStorage 缓存,触发重新加载 + localStorage.removeItem("parent_notification_preferences"); + break; + } + }; + } + + // 方案 2:storage 事件降级(BroadcastChannel 不可用时) + function handleStorageEvent(e: StorageEvent) { + if (e.key !== STORAGE_EVENT_KEY || !e.newValue) return; + try { + const msg = JSON.parse(e.newValue) as SyncMessage; + if (msg.source === SOURCE_ID) return; + + switch (msg.type) { + case "child-switched": + useChildStore.setState({ currentChildId: msg.childId }); + break; + case "child-unbound": + break; + case "preferences-updated": + localStorage.removeItem("parent_notification_preferences"); + break; + } + } catch { + // 忽略解析错误 + } + } + window.addEventListener("storage", handleStorageEvent); + + return () => { + bc?.close(); + window.removeEventListener("storage", handleStorageEvent); + }; + }, []); +} + +// 导出 source ID 供其他模块广播时使用 +export { SOURCE_ID as SYNC_SOURCE_ID }; + +// 广播跨标签消息(供 child-store / preference hook 使用) +export function broadcastSync(msg: Omit) { + const fullMsg: SyncMessage = { ...msg, source: SOURCE_ID } as SyncMessage; + + // BroadcastChannel + if ("BroadcastChannel" in window) { + const bc = new BroadcastChannel(CHANNEL_NAME); + bc.postMessage(fullMsg); + bc.close(); + } + + // storage 事件降级 + try { + localStorage.setItem(STORAGE_EVENT_KEY, JSON.stringify(fullMsg)); + // 立即清除(storage 事件只在值变化时触发) + setTimeout(() => localStorage.removeItem(STORAGE_EVENT_KEY), 0); + } catch { + // 忽略 + } +} diff --git a/apps/parent-portal/src/hooks/useMyChildren.ts b/apps/parent-portal/src/hooks/useMyChildren.ts new file mode 100644 index 0000000..12dd462 --- /dev/null +++ b/apps/parent-portal/src/hooks/useMyChildren.ts @@ -0,0 +1,39 @@ +// useMyChildren:获取家长绑定子女列表 +// 依据:02-architecture-design.md §4.2 GraphQL 接入 +// - ISSUE-010:iam GetChildrenByParent P0 阻塞,MSW mock 返回 student-001 + student-002 +// - 加载后写入 Zustand store(setChildren),触发默认选中 +// - 单次加载,不轮询(子女列表变更由 WebSocket ChildBound/ChildUnbound 事件通知 P5) + +"use client"; + +import { useEffect } from "react"; +import { useQuery } from "urql"; +import { MY_CHILDREN } from "@/lib/graphql/operations"; +import { useChildStore } from "@/store/child-store"; +import type { ChildInfo } from "@/types"; + +interface MyChildrenResponse { + myChildren: ChildInfo[]; +} + +export function useMyChildren() { + const setChildren = useChildStore((s) => s.setChildren); + const setLoading = useChildStore((s) => s.setLoading); + + const [result] = useQuery({ + query: MY_CHILDREN, + }); + + useEffect(() => { + setLoading(result.fetching); + if (result.data?.myChildren) { + setChildren(result.data.myChildren); + } + }, [result.data, result.fetching, setChildren, setLoading]); + + return { + children: result.data?.myChildren ?? [], + loading: result.fetching, + error: result.error, + }; +} diff --git a/apps/parent-portal/src/hooks/useMyNotifications.ts b/apps/parent-portal/src/hooks/useMyNotifications.ts new file mode 100644 index 0000000..b41ee10 --- /dev/null +++ b/apps/parent-portal/src/hooks/useMyNotifications.ts @@ -0,0 +1,51 @@ +// useMyNotifications:获取通知列表 + 标记已读 +// 依据:02-architecture-design.md §4.2 GraphQL 接入、§14 通知偏好 + +"use client"; + +import { useQuery, useMutation } from "urql"; +import { + MY_NOTIFICATIONS, + MARK_AS_READ, + MARK_ALL_AS_READ, +} from "@/lib/graphql/operations"; +import type { NotificationItem } from "@/types"; + +interface MyNotificationsResponse { + myNotifications: NotificationItem[]; +} + +interface MarkAsReadResponse { + markAsRead: { id: string; read: boolean }; +} + +interface MarkAllAsReadResponse { + markAllAsRead: { count: number }; +} + +export function useMyNotifications(unreadOnly = false) { + const [result] = useQuery({ + query: MY_NOTIFICATIONS, + variables: { unreadOnly, limit: 50 }, + }); + + const [, markAsReadMutation] = useMutation(MARK_AS_READ); + const [, markAllAsReadMutation] = + useMutation(MARK_ALL_AS_READ); + + async function markAsRead(notificationId: string) { + return markAsReadMutation({ notificationId }); + } + + async function markAllAsRead() { + return markAllAsReadMutation({}); + } + + return { + notifications: result.data?.myNotifications ?? [], + loading: result.fetching, + error: result.error, + markAsRead, + markAllAsRead, + }; +} diff --git a/apps/parent-portal/src/hooks/useNotificationPreferences.ts b/apps/parent-portal/src/hooks/useNotificationPreferences.ts new file mode 100644 index 0000000..43af8f2 --- /dev/null +++ b/apps/parent-portal/src/hooks/useNotificationPreferences.ts @@ -0,0 +1,108 @@ +// useNotificationPreferences:通知偏好(P4 localStorage 降级) +// 依据:02-architecture-design.md §14 通知偏好数据模型、ISSUE-033 裁决 +// - P4 阶段:localStorage 为主存储,GraphQL 查询提供初始默认值 +// - 更新时:先写 localStorage,再尝试 GraphQL mutation(失败不阻塞) +// - P5+ 阶段:切换到后端为主存储 + +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { useQuery, useMutation } from "urql"; +import { + MY_NOTIFICATION_PREFERENCES, + UPDATE_NOTIFICATION_PREFERENCES, +} from "@/lib/graphql/operations"; +import type { + NotificationPreferences, + NotificationEventType, + NotificationChannel, +} from "@/types"; +import { getLocalStorage, setLocalStorage } from "@/lib/utils"; + +const PREFS_KEY = "parent_notification_preferences"; + +interface MyNotificationPreferencesResponse { + myNotificationPreferences: NotificationPreferences; +} + +interface UpdatePreferencesResponse { + updateNotificationPreferences: { parentId: string; updatedAt: string }; +} + +export function useNotificationPreferences(parentId: string) { + const [localPrefs, setLocalPrefs] = useState( + null, + ); + + // GraphQL 查询(提供初始默认值) + const [queryResult] = useQuery({ + query: MY_NOTIFICATION_PREFERENCES, + }); + + const [, updateMutation] = useMutation( + UPDATE_NOTIFICATION_PREFERENCES, + ); + + // 加载:优先 localStorage,无则用 GraphQL 返回值 + useEffect(() => { + const stored = getLocalStorage(PREFS_KEY); + if (stored) { + try { + setLocalPrefs(JSON.parse(stored) as NotificationPreferences); + return; + } catch { + // fallthrough + } + } + if (queryResult.data?.myNotificationPreferences) { + setLocalPrefs(queryResult.data.myNotificationPreferences); + } + }, [queryResult.data]); + + // 更新单个偏好(ISSUE-033:先写 localStorage) + const updatePreference = useCallback( + ( + childId: string, + eventType: NotificationEventType, + channel: NotificationChannel, + enabled: boolean, + ) => { + setLocalPrefs((prev) => { + if (!prev) return prev; + const childPrefs = prev.preferences[childId] ?? {}; + const next: NotificationPreferences = { + ...prev, + preferences: { + ...prev.preferences, + [childId]: { + ...childPrefs, + [eventType]: { + ...childPrefs[eventType], + [channel]: enabled, + }, + }, + }, + updatedAt: new Date().toISOString(), + }; + setLocalStorage(PREFS_KEY, JSON.stringify(next)); + // 异步同步到后端(失败不阻塞) + updateMutation({ + parentId, + preferences: next.preferences, + defaults: next.defaults, + }).catch(() => { + // P4 localStorage 降级:后端失败不影响本地 + }); + return next; + }); + }, + [parentId, updateMutation], + ); + + return { + preferences: localPrefs, + loading: !localPrefs && queryResult.fetching, + error: queryResult.error, + updatePreference, + }; +} diff --git a/apps/parent-portal/src/hooks/usePermission.ts b/apps/parent-portal/src/hooks/usePermission.ts new file mode 100644 index 0000000..8ebedd0 --- /dev/null +++ b/apps/parent-portal/src/hooks/usePermission.ts @@ -0,0 +1,35 @@ +// 权限 Hook +// 依据:project_rules §3.1(前端禁止 role === "xxx" 硬编码,统一用 hasPermission) +// 从 localStorage 读取用户 permissions,提供 hasPermission 检查 + +import { useMemo } from "react"; +import { getUser } from "@/lib/auth"; +import type { Permission } from "@/lib/permissions"; + +export function usePermission() { + const user = getUser(); + + const permissions = useMemo(() => { + return new Set(user?.permissions ?? []); + }, [user]); + + function hasPermission(permission: Permission): boolean { + return permissions.has(permission); + } + + function hasAnyPermission(...required: Permission[]): boolean { + return required.some((p) => permissions.has(p)); + } + + function hasAllPermissions(...required: Permission[]): boolean { + return required.every((p) => permissions.has(p)); + } + + return { + permissions: user?.permissions ?? [], + hasPermission, + hasAnyPermission, + hasAllPermissions, + isAuthenticated: user !== null, + }; +} diff --git a/apps/parent-portal/src/hooks/useRealtimeNotifications.ts b/apps/parent-portal/src/hooks/useRealtimeNotifications.ts new file mode 100644 index 0000000..8565770 --- /dev/null +++ b/apps/parent-portal/src/hooks/useRealtimeNotifications.ts @@ -0,0 +1,70 @@ +// useRealtimeNotifications:实时通知 Hook(P5) +// 依据:02-architecture-design.md §5 实时推送 +// - WebSocket 接收 NotificationRequested 事件 +// - 收到新通知时更新 urql 缓存(触发 NotificationFeed 重新渲染) +// - 收到 GradeRecorded / SchoolAnnouncement 事件时触发相应页面刷新 + +"use client"; + +import { useCallback } from "react"; +import { useWebSocket } from "./useWebSocket"; +import type { WebSocketEvent } from "@/types"; + +export function useRealtimeNotifications() { + const handleEvent = useCallback((event: WebSocketEvent) => { + switch (event.type) { + case "NotificationRequested": + // 新通知:触发 NotificationFeed 刷新 + // urql 会自动重新请求(staleTime 到期或手动 invalidate) + // 这里通过 dispatchEvent 通知 NotificationFeed 组件 + window.dispatchEvent( + new CustomEvent("realtime-notification", { + detail: event.notification, + }), + ); + break; + + case "GradeRecorded": + // 成绩发布:触发成绩页面刷新 + window.dispatchEvent( + new CustomEvent("realtime-grade", { + detail: { + childId: event.childId, + examId: event.examId, + }, + }), + ); + break; + + case "SchoolAnnouncement": + // 学校公告:显示全局通知 + window.dispatchEvent( + new CustomEvent("realtime-announcement", { + detail: { title: event.title, body: event.body }, + }), + ); + break; + + case "ChildBound": + // 子女绑定:刷新子女列表 + window.dispatchEvent(new CustomEvent("realtime-child-bound")); + break; + + case "ChildUnbound": + // 子女解绑:刷新子女列表 + window.dispatchEvent( + new CustomEvent("realtime-child-unbound", { + detail: { childId: event.childId }, + }), + ); + break; + } + }, []); + + const { status } = useWebSocket({ + onEvent: handleEvent, + enabled: true, + }); + + return { status }; +} diff --git a/apps/parent-portal/src/hooks/useWebSocket.ts b/apps/parent-portal/src/hooks/useWebSocket.ts new file mode 100644 index 0000000..4bad167 --- /dev/null +++ b/apps/parent-portal/src/hooks/useWebSocket.ts @@ -0,0 +1,168 @@ +// useWebSocket:push-gateway WebSocket 连接 +// 依据:02-architecture-design.md §5 实时推送(P5) +// - 连接 push-gateway WebSocket(NEXT_PUBLIC_PUSH_GATEWAY_WS_URL) +// - 自动重连(指数退避,最大 30s) +// - WebSocket 不可用时降级为 HTTP 轮询(60s) +// - 事件处理:NotificationRequested / GradeRecorded / SchoolAnnouncement / ChildBound / ChildUnbound + +"use client"; + +import { useEffect, useRef, useState, useCallback } from "react"; +import { getToken } from "@/lib/auth"; +import { getBackoffDelay } from "@/lib/utils"; +import type { WebSocketEvent } from "@/types"; + +const WS_URL = process.env.NEXT_PUBLIC_PUSH_GATEWAY_WS_URL || ""; +const POLL_INTERVAL = 60_000; // 60s 轮询降级 +const MAX_RECONNECT_ATTEMPTS = 10; + +type ConnectionStatus = "connecting" | "connected" | "disconnected" | "polling"; + +interface UseWebSocketOptions { + onEvent?: (event: WebSocketEvent) => void; + enabled?: boolean; +} + +export function useWebSocket({ + onEvent, + enabled = true, +}: UseWebSocketOptions = {}) { + const [status, setStatus] = useState("disconnected"); + const wsRef = useRef(null); + const reconnectAttempts = useRef(0); + const reconnectTimer = useRef | null>(null); + const pollTimer = useRef | null>(null); + const onEventRef = useRef(onEvent); + + // 保持 onEvent 引用最新 + useEffect(() => { + onEventRef.current = onEvent; + }, [onEvent]); + + const handleEvent = useCallback((event: WebSocketEvent) => { + onEventRef.current?.(event); + }, []); + + // HTTP 轮询降级 + const startPolling = useCallback(() => { + if (pollTimer.current) return; + setStatus("polling"); + // 立即拉取一次 + void pollNotifications(handleEvent); + pollTimer.current = setInterval(() => { + void pollNotifications(handleEvent); + }, POLL_INTERVAL); + }, [handleEvent]); + + const stopPolling = useCallback(() => { + if (pollTimer.current) { + clearInterval(pollTimer.current); + pollTimer.current = null; + } + }, []); + + // WebSocket 连接 + const connect = useCallback(() => { + if (!enabled || !WS_URL || typeof window === "undefined") { + startPolling(); + return; + } + + const token = getToken(); + if (!token) { + setStatus("disconnected"); + return; + } + + setStatus("connecting"); + + try { + const wsUrl = `${WS_URL}?token=${encodeURIComponent(token)}`; + const ws = new WebSocket(wsUrl); + wsRef.current = ws; + + ws.onopen = () => { + reconnectAttempts.current = 0; + setStatus("connected"); + stopPolling(); + }; + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data) as WebSocketEvent; + handleEvent(data); + } catch { + // 忽略无效消息 + } + }; + + ws.onerror = () => { + // 错误不立即断开,等 onclose 处理 + }; + + ws.onclose = () => { + setStatus("disconnected"); + wsRef.current = null; + + // 重连或降级 + if (reconnectAttempts.current < MAX_RECONNECT_ATTEMPTS) { + const delay = getBackoffDelay(reconnectAttempts.current); + reconnectAttempts.current++; + reconnectTimer.current = setTimeout(() => { + connect(); + }, delay); + } else { + // 超过重连次数,降级为轮询 + startPolling(); + } + }; + } catch { + // WebSocket 创建失败,降级为轮询 + startPolling(); + } + }, [enabled, stopPolling, startPolling, handleEvent]); + + useEffect(() => { + connect(); + + return () => { + if (reconnectTimer.current) { + clearTimeout(reconnectTimer.current); + } + stopPolling(); + wsRef.current?.close(); + wsRef.current = null; + }; + }, [connect, stopPolling]); + + return { + status, + reconnect: connect, + }; +} + +// HTTP 轮询:拉取最新通知 +async function pollNotifications( + onEvent: (event: WebSocketEvent) => void, +): Promise { + try { + const res = await fetch("/api/v1/parent/notifications?since=true", { + headers: { + Authorization: `Bearer ${localStorage.getItem("parent_access_token") ?? ""}`, + }, + }); + if (!res.ok) return; + const json = await res.json(); + // 将新通知转为 WebSocketEvent 格式 + if (json.data?.notifications) { + for (const notif of json.data.notifications) { + onEvent({ + type: "NotificationRequested", + notification: notif, + }); + } + } + } catch { + // 轮询失败静默忽略 + } +} diff --git a/apps/parent-portal/src/lib/auth.test.ts b/apps/parent-portal/src/lib/auth.test.ts new file mode 100644 index 0000000..88ad16d --- /dev/null +++ b/apps/parent-portal/src/lib/auth.test.ts @@ -0,0 +1,182 @@ +// 认证工具单测(含 MSW 集成) +// 依据:02-architecture-design.md §13 测试策略、F12 localStorage token、ISSUE-004 REST 登录 +// 覆盖:token 存储 / isAuthenticated / login REST / refreshAccessToken 竞态防护 / clearAuth + +import { + describe, + it, + expect, + beforeAll, + afterAll, + afterEach, + vi, +} from "vitest"; +import { server } from "@/test/mocks/server"; +import { + getToken, + getRefreshToken, + setTokens, + clearAuth, + getUser, + setUser, + isAuthenticated, + login, + refreshAccessToken, + getAuthHeaders, +} from "./auth"; +import { mockParent } from "@/test/mocks/fixtures"; + +beforeAll(() => server.listen({ onUnhandledRequest: "error" })); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +describe("token 存储", () => { + it("setTokens 写入 access/refresh/expiresAt", () => { + setTokens("access-123", "refresh-456", 3600); + expect(getToken()).toBe("access-123"); + expect(getRefreshToken()).toBe("refresh-456"); + }); + + it("未设置 token 时返回 null", () => { + expect(getToken()).toBeNull(); + expect(getRefreshToken()).toBeNull(); + }); +}); + +describe("getUser / setUser", () => { + it("setUser 后 getUser 返回相同对象", () => { + setUser(mockParent); + const user = getUser(); + expect(user).not.toBeNull(); + expect(user?.id).toBe(mockParent.id); + expect(user?.email).toBe(mockParent.email); + }); + + it("getUser 解析无效 JSON 返回 null", () => { + localStorage.setItem("parent_user_info", "{invalid json"); + expect(getUser()).toBeNull(); + }); + + it("getUser 未设置时返回 null", () => { + expect(getUser()).toBeNull(); + }); +}); + +describe("isAuthenticated", () => { + it("无 token 返回 false", () => { + expect(isAuthenticated()).toBe(false); + }); + + it("有未过期 token 返回 true", () => { + setTokens("access", "refresh", 3600); + expect(isAuthenticated()).toBe(true); + }); + + it("有过期 token 返回 false", () => { + setTokens("access", "refresh", -1); // 已过期 + expect(isAuthenticated()).toBe(false); + }); +}); + +describe("clearAuth", () => { + it("清除所有认证信息", () => { + setTokens("access", "refresh", 3600); + setUser(mockParent); + clearAuth(); + expect(getToken()).toBeNull(); + expect(getRefreshToken()).toBeNull(); + expect(getUser()).toBeNull(); + }); +}); + +describe("login(REST,ISSUE-004)", () => { + it("正确凭据返回用户和 token", async () => { + const result = await login("parent@example.com", "password"); + expect(result.user.email).toBe("parent@example.com"); + expect(result.tokens.accessToken).toBeTruthy(); + expect(result.tokens.refreshToken).toBeTruthy(); + // 写入了 localStorage + expect(getToken()).toBe(result.tokens.accessToken); + }); + + it("错误凭据抛出异常", async () => { + await expect(login("wrong@example.com", "wrong")).rejects.toThrow( + /邮箱或密码错误/, + ); + }); +}); + +describe("refreshAccessToken(竞态防护)", () => { + it("有效 refresh token 返回新 access token", async () => { + setTokens("access", "mock-refresh-token-parent-001", 3600); + const newToken = await refreshAccessToken(); + expect(newToken).toBe("mock-access-token-parent-001-renewed"); + expect(getToken()).toBe("mock-access-token-parent-001-renewed"); + }); + + it("无 refresh token 返回 null", async () => { + clearAuth(); + const result = await refreshAccessToken(); + expect(result).toBeNull(); + }); + + it("无效 refresh token 返回 null", async () => { + setTokens("access", "invalid-refresh", 3600); + const result = await refreshAccessToken(); + expect(result).toBeNull(); + }); + + it("并发请求复用同一 refresh(仅触发一次 fetch)", async () => { + setTokens("access", "mock-refresh-token-parent-001", 3600); + const fetchSpy = vi.spyOn(globalThis, "fetch"); + const p1 = refreshAccessToken(); + const p2 = refreshAccessToken(); + const [t1, t2] = await Promise.all([p1, p2]); + expect(t1).toBe(t2); + // 竞态防护:两个并发调用只触发一次 refresh fetch + const refreshCalls = fetchSpy.mock.calls.filter(([url]) => + String(url).includes("/api/v1/iam/refresh"), + ); + expect(refreshCalls).toHaveLength(1); + fetchSpy.mockRestore(); + }); +}); + +describe("getAuthHeaders", () => { + it("无 token 返回空对象", () => { + clearAuth(); + expect(getAuthHeaders()).toEqual({}); + }); + + it("有 token 返回 Bearer header", () => { + setTokens("my-token", "refresh", 3600); + expect(getAuthHeaders()).toEqual({ Authorization: "Bearer my-token" }); + }); +}); + +describe("logout", () => { + it("清除认证信息", async () => { + setTokens("access", "refresh", 3600); + setUser(mockParent); + const { logout } = await import("./auth"); + // mock window.location.href 赋值(jsdom 不允许直接 spy) + const originalHref = window.location.href; + let assignedHref: string | null = null; + Object.defineProperty(window, "location", { + value: { + ...window.location, + set href(v: string) { + assignedHref = v; + }, + get href() { + return originalHref; + }, + }, + configurable: true, + }); + logout(); + expect(getToken()).toBeNull(); + expect(getUser()).toBeNull(); + expect(assignedHref).toBe("/login"); + }); +}); diff --git a/apps/parent-portal/src/lib/auth.ts b/apps/parent-portal/src/lib/auth.ts new file mode 100644 index 0000000..2ad51e3 --- /dev/null +++ b/apps/parent-portal/src/lib/auth.ts @@ -0,0 +1,192 @@ +// 认证工具:token 存储(localStorage,F12 裁决)+ 登录(REST,ISSUE-004) +// 依据:02-architecture-design.md §4.1、F12 localStorage token、ISSUE-004 登录端点 +// parent-portal 唯一走 REST 的端点:POST /api/v1/iam/login + +import type { ActionState, LoginResponse, UserSession } from "@/types"; + +const TOKEN_KEY = "parent_access_token"; +const REFRESH_TOKEN_KEY = "parent_refresh_token"; +const USER_KEY = "parent_user_info"; +const TOKEN_EXPIRES_KEY = "parent_token_expires_at"; + +/** + * 获取 access token(SSR 安全) + */ +export function getToken(): string | null { + if (typeof window === "undefined") return null; + return localStorage.getItem(TOKEN_KEY); +} + +/** + * 获取 refresh token + */ +export function getRefreshToken(): string | null { + if (typeof window === "undefined") return null; + return localStorage.getItem(REFRESH_TOKEN_KEY); +} + +/** + * 设置 token(含过期时间) + */ +export function setTokens( + accessToken: string, + refreshToken: string, + expiresIn: number, +): void { + if (typeof window === "undefined") return; + localStorage.setItem(TOKEN_KEY, accessToken); + localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); + const expiresAt = Date.now() + expiresIn * 1000; + localStorage.setItem(TOKEN_EXPIRES_KEY, expiresAt.toString()); +} + +/** + * 清除所有认证信息 + */ +export function clearAuth(): void { + if (typeof window === "undefined") return; + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(REFRESH_TOKEN_KEY); + localStorage.removeItem(USER_KEY); + localStorage.removeItem(TOKEN_EXPIRES_KEY); +} + +/** + * 获取当前用户信息 + */ +export function getUser(): UserSession | null { + if (typeof window === "undefined") return null; + const raw = localStorage.getItem(USER_KEY); + if (!raw) return null; + try { + return JSON.parse(raw) as UserSession; + } catch { + return null; + } +} + +/** + * 设置当前用户信息 + */ +export function setUser(user: UserSession): void { + if (typeof window === "undefined") return; + localStorage.setItem(USER_KEY, JSON.stringify(user)); +} + +/** + * 是否已认证(token 未过期) + */ +export function isAuthenticated(): boolean { + if (typeof window === "undefined") return false; + const token = getToken(); + if (!token) return false; + const expiresAt = localStorage.getItem(TOKEN_EXPIRES_KEY); + if (!expiresAt) return true; // 无过期时间视为永久 + return Date.now() < parseInt(expiresAt, 10); +} + +/** + * 登录(REST,ISSUE-004 唯一走 REST 的端点) + * 调用 POST /api/v1/iam/login + */ +export async function login( + email: string, + password: string, +): Promise<{ + user: UserSession; + tokens: { accessToken: string; refreshToken: string; expiresIn: number }; +}> { + const res = await fetch("/api/v1/iam/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Requested-With": "XMLHttpRequest", + }, + body: JSON.stringify({ email, password }), + }); + + const json: ActionState = await res.json(); + + if (!json.success) { + throw new Error(json.error.message); + } + + const { user, tokens } = json.data; + setTokens(tokens.accessToken, tokens.refreshToken, tokens.expiresIn); + setUser(user); + return { user, tokens }; +} + +/** + * 登出 + */ +export function logout(): void { + clearAuth(); + if (typeof window !== "undefined") { + window.location.href = "/login"; + } +} + +/** + * Token 刷新(竞态防护:全局单例 promise 复用) + */ +let refreshPromise: Promise | null = null; + +export async function refreshAccessToken(): Promise { + // 复用进行中的 refresh 请求(避免多请求同时 401 触发多次 refresh) + if (refreshPromise) return refreshPromise; + + const refreshToken = getRefreshToken(); + if (!refreshToken) { + logout(); + return null; + } + + refreshPromise = (async () => { + try { + const res = await fetch("/api/v1/iam/refresh", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Requested-With": "XMLHttpRequest", + }, + body: JSON.stringify({ refreshToken }), + }); + + if (!res.ok) { + logout(); + return null; + } + + const json: ActionState<{ tokens: LoginResponse["tokens"] }> = + await res.json(); + if (!json.success) { + logout(); + return null; + } + + const { + accessToken, + refreshToken: newRefreshToken, + expiresIn, + } = json.data.tokens; + setTokens(accessToken, newRefreshToken, expiresIn); + return accessToken; + } catch { + logout(); + return null; + } finally { + refreshPromise = null; + } + })(); + + return refreshPromise; +} + +/** + * 获取 Authorization header(供 GraphQL client 使用) + */ +export function getAuthHeaders(): Record { + const token = getToken(); + return token ? { Authorization: `Bearer ${token}` } : {}; +} diff --git a/apps/parent-portal/src/lib/graphql-client.ts b/apps/parent-portal/src/lib/graphql-client.ts new file mode 100644 index 0000000..e412db1 --- /dev/null +++ b/apps/parent-portal/src/lib/graphql-client.ts @@ -0,0 +1,72 @@ +// GraphQL client (urql) +// 依据:02-architecture-design.md §4.2 GraphQL 接入、F9 裁决(首次即 GraphQL) +// 端点:POST /api/v1/parent/graphql(经 api-gateway 代理到 parent-bff) +// 认证:Authorization: Bearer (F12 localStorage token) +// 刷新:401 时触发 refreshAccessToken,重试原请求 + +import { Client, fetchExchange, cacheExchange } from "urql"; +import { getAuthHeaders, refreshAccessToken, getToken } from "./auth"; + +const GRAPHQL_ENDPOINT = + process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT || "/api/v1/parent/graphql"; + +/** + * 自定义 fetch:注入 Authorization header + 401 刷新重试 + */ +async function authedFetch( + input: RequestInfo | URL, + init?: RequestInit, +): Promise { + const authHeaders = getAuthHeaders(); + const headers = new Headers(init?.headers || {}); + for (const [k, v] of Object.entries(authHeaders)) { + headers.set(k, v); + } + headers.set("X-Requested-With", "XMLHttpRequest"); + + let response = await fetch(input, { ...init, headers }); + + // 401 → 刷新 token 后重试一次 + if (response.status === 401) { + const newToken = await refreshAccessToken(); + if (newToken) { + headers.set("Authorization", `Bearer ${newToken}`); + response = await fetch(input, { ...init, headers }); + } + } + + return response; +} + +/** + * 创建 urql client(单例) + */ +export function createGraphQLClient(): Client { + return new Client({ + url: GRAPHQL_ENDPOINT, + fetch: authedFetch as unknown as typeof fetch, + exchanges: [cacheExchange, fetchExchange], + fetchOptions: (): RequestInit => { + const token = getToken(); + const headers: Record = {}; + if (token) { + headers.Authorization = `Bearer ${token}`; + } + return { headers }; + }, + }); +} + +// 全局单例(SSR 安全:仅客户端创建) +let client: Client | null = null; + +export function getGraphQLClient(): Client { + if (typeof window === "undefined") { + // SSR:每次创建新实例(避免请求间状态泄漏) + return createGraphQLClient(); + } + if (!client) { + client = createGraphQLClient(); + } + return client; +} diff --git a/apps/parent-portal/src/lib/graphql/operations.ts b/apps/parent-portal/src/lib/graphql/operations.ts new file mode 100644 index 0000000..3f6ae34 --- /dev/null +++ b/apps/parent-portal/src/lib/graphql/operations.ts @@ -0,0 +1,210 @@ +// GraphQL operations 文档 +// 依据:02-architecture-design.md §4.2 GraphQL 接入、F9 裁决(首次即 GraphQL) +// 端点:POST /api/v1/parent/graphql +// 命名:查询 PascalCase(CurrentUser / MyChildren / ChildSummary ...) +// +// 所有查询/变更与 parent-bff 契约对齐: +// - 查询:currentUser / myChildren / childSummary / childGrades / childAttendance / +// childHomework / childWeakness / childTrend / myNotifications / myNotificationPreferences +// - 变更:markAsRead / markAllAsRead / updateNotificationPreferences + +import { gql } from "urql"; + +// ===== 查询 ===== + +export const CURRENT_USER = gql` + query CurrentUser { + currentUser { + id + email + name + roles + permissions + dataScope + schoolId + } + } +`; + +export const MY_CHILDREN = gql` + query MyChildren { + myChildren { + id + name + avatar + grade + schoolName + classId + className + isArchived + } + } +`; + +export const CHILD_SUMMARY = gql` + query ChildSummary($childId: ID!) { + childSummary(childId: $childId) { + childId + avgScore + classRank + classSize + attendanceRate + pendingHomeworkCount + recentGradeTrend + recentScores { + examId + examName + examDate + subject + studentScore + classAverage + classMax + classMin + gradeLevel + } + upcomingEvents { + id + type + title + dueDate + } + } + } +`; + +export const CHILD_GRADES = gql` + query ChildGrades($childId: ID!, $subject: String, $limit: Int) { + childGrades(childId: $childId, subject: $subject, limit: $limit) { + examId + examName + examDate + subject + studentScore + classAverage + classMax + classMin + gradeLevel + } + } +`; + +export const CHILD_ATTENDANCE = gql` + query ChildAttendance($childId: ID!, $startDate: String!, $endDate: String!) { + childAttendance( + childId: $childId + startDate: $startDate + endDate: $endDate + ) { + id + date + status + note + } + } +`; + +export const CHILD_HOMEWORK = gql` + query ChildHomework($childId: ID!, $status: String) { + childHomework(childId: $childId, status: $status) { + id + title + subject + className + assignedDate + dueDate + status + score + maxScore + feedback + } + } +`; + +export const CHILD_WEAKNESS = gql` + query ChildWeakness($childId: ID!) { + childWeakness(childId: $childId) { + id + knowledgePoint + masteryLevel + subject + recommendation + } + } +`; + +export const CHILD_TREND = gql` + query ChildTrend($childId: ID!, $period: TrendPeriod!) { + childTrend(childId: $childId, period: $period) { + childId + period + dataPoints { + date + score + subject + } + } + } +`; + +export const MY_NOTIFICATIONS = gql` + query MyNotifications($unreadOnly: Boolean, $limit: Int) { + myNotifications(unreadOnly: $unreadOnly, limit: $limit) { + id + childId + eventType + title + body + read + createdAt + actionUrl + pinned + } + } +`; + +export const MY_NOTIFICATION_PREFERENCES = gql` + query MyNotificationPreferences { + myNotificationPreferences { + parentId + preferences + defaults + updatedAt + } + } +`; + +// ===== 变更 ===== + +export const MARK_AS_READ = gql` + mutation MarkAsRead($notificationId: ID!) { + markAsRead(notificationId: $notificationId) { + id + read + } + } +`; + +export const MARK_ALL_AS_READ = gql` + mutation MarkAllAsRead { + markAllAsRead { + count + } + } +`; + +export const UPDATE_NOTIFICATION_PREFERENCES = gql` + mutation UpdateNotificationPreferences( + $parentId: ID! + $preferences: JSON! + $defaults: JSON! + ) { + updateNotificationPreferences( + parentId: $parentId + preferences: $preferences + defaults: $defaults + ) { + parentId + updatedAt + } + } +`; diff --git a/apps/parent-portal/src/lib/i18n.ts b/apps/parent-portal/src/lib/i18n.ts new file mode 100644 index 0000000..0e63d14 --- /dev/null +++ b/apps/parent-portal/src/lib/i18n.ts @@ -0,0 +1,107 @@ +// i18n 国际化(P6-4) +// 依据:02-architecture-design.md §17 国际化、F4 裁决(i18n key 命名 error..) +// - 支持 zh-CN(默认)+ en-US +// - 错误码 key:error..(F4) +// - 简单翻译表(后续可迁移到 next-intl) + +export type Locale = "zh-CN" | "en-US"; + +const DEFAULT_LOCALE: Locale = "zh-CN"; + +const translations: Record> = { + "zh-CN": { + // 通用 + "common.loading": "加载中...", + "common.error": "加载失败", + "common.retry": "重试", + "common.save": "保存", + "common.cancel": "取消", + "common.confirm": "确认", + + // 导航 + "nav.dashboard": "仪表盘", + "nav.grades": "成绩", + "nav.attendance": "考勤", + "nav.homework": "作业", + "nav.weakness": "学情", + "nav.notifications": "通知", + "nav.preferences": "偏好", + + // 子女切换 + "child.switcher.empty": "未绑定子女", + "child.switcher.select": "选择子女", + + // 错误码(F4 命名:error..) + "error.iam.invalid_credentials": "邮箱或密码错误", + "error.iam.invalid_refresh": "refresh token 无效", + "error.iam.token_expired": "token 已过期", + "error.parent.child_not_found": "子女不存在", + "error.parent.permission_denied": "无权限访问", + "error.parent.network_error": "网络错误", + "error.parent.graphql_error": "数据加载失败", + + // 通知事件 + "notification.grade_recorded": "成绩发布", + "notification.homework_graded": "作业评分", + "notification.homework_assigned": "作业布置", + "notification.exam_published": "考试发布", + "notification.attendance_alert": "考勤提醒", + "notification.school_announcement": "学校公告", + "notification.teacher_message": "老师消息", + "notification.fee_reminder": "缴费提醒", + "notification.event_invitation": "活动邀请", + }, + "en-US": { + "common.loading": "Loading...", + "common.error": "Failed to load", + "common.retry": "Retry", + "common.save": "Save", + "common.cancel": "Cancel", + "common.confirm": "Confirm", + + "nav.dashboard": "Dashboard", + "nav.grades": "Grades", + "nav.attendance": "Attendance", + "nav.homework": "Homework", + "nav.weakness": "Analysis", + "nav.notifications": "Notifications", + "nav.preferences": "Preferences", + + "child.switcher.empty": "No child bound", + "child.switcher.select": "Select child", + + "error.iam.invalid_credentials": "Invalid email or password", + "error.iam.invalid_refresh": "Invalid refresh token", + "error.iam.token_expired": "Token expired", + "error.parent.child_not_found": "Child not found", + "error.parent.permission_denied": "Permission denied", + "error.parent.network_error": "Network error", + "error.parent.graphql_error": "Failed to load data", + + "notification.grade_recorded": "Grade Published", + "notification.homework_graded": "Homework Graded", + "notification.homework_assigned": "Homework Assigned", + "notification.exam_published": "Exam Published", + "notification.attendance_alert": "Attendance Alert", + "notification.school_announcement": "School Announcement", + "notification.teacher_message": "Teacher Message", + "notification.fee_reminder": "Fee Reminder", + "notification.event_invitation": "Event Invitation", + }, +}; + +export function getLocale(): Locale { + if (typeof navigator !== "undefined") { + const lang = navigator.language; + if (lang.startsWith("en")) return "en-US"; + } + return DEFAULT_LOCALE; +} + +export function t(key: string, locale: Locale = getLocale()): string { + return ( + translations[locale]?.[key] ?? translations[DEFAULT_LOCALE]?.[key] ?? key + ); +} + +export { translations }; diff --git a/apps/parent-portal/src/lib/permissions.ts b/apps/parent-portal/src/lib/permissions.ts new file mode 100644 index 0000000..de66165 --- /dev/null +++ b/apps/parent-portal/src/lib/permissions.ts @@ -0,0 +1,26 @@ +// 权限点常量 +// 依据:F7 裁决(权限点命名 _[_]) +// 前端禁止硬编码 role === "xxx",统一使用 usePermission().hasPermission()(project_rules §3.1) + +export const PERMISSIONS = { + // 仪表盘 + DASHBOARD_VIEW: "DASHBOARD_VIEW", + + // 子女成绩 + CHILD_GRADE_VIEW: "CHILD_GRADE_VIEW", + + // 子女考勤 + CHILD_ATTENDANCE_VIEW: "CHILD_ATTENDANCE_VIEW", + + // 子女作业 + CHILD_HOMEWORK_VIEW: "CHILD_HOMEWORK_VIEW", + + // 子女学情 + CHILD_WEAKNESS_VIEW: "CHILD_WEAKNESS_VIEW", + + // 通知 + NOTIFICATION_VIEW: "NOTIFICATION_VIEW", + NOTIFICATION_PREFERENCE_UPDATE: "NOTIFICATION_PREFERENCE_UPDATE", +} as const; + +export type Permission = (typeof PERMISSIONS)[keyof typeof PERMISSIONS]; diff --git a/apps/parent-portal/src/lib/query-client.ts b/apps/parent-portal/src/lib/query-client.ts new file mode 100644 index 0000000..c0cd23c --- /dev/null +++ b/apps/parent-portal/src/lib/query-client.ts @@ -0,0 +1,28 @@ +// TanStack Query client(服务端数据缓存) +// 依据:02-architecture-design.md §4.3 状态管理分层(Server Cache 层) +// 与 urql 互补:urql 管 GraphQL,TanStack Query 管 REST/复合查询 + +import { QueryClient } from "@tanstack/react-query"; + +export function createQueryClient(): QueryClient { + return new QueryClient({ + defaultOptions: { + queries: { + // 重试策略:仅网络错误重试,401/403/404 不重试 + retry: (failureCount, error: unknown) => { + if (error instanceof Error && error.message.includes("401")) { + return false; + } + return failureCount < 2; + }, + staleTime: 30 * 1000, // 30s 内不重新请求 + gcTime: 5 * 60 * 1000, // 5min 后回收 + refetchOnWindowFocus: false, + refetchOnReconnect: "always", + }, + mutations: { + retry: false, + }, + }, + }); +} diff --git a/apps/parent-portal/src/lib/schemas/notification-preferences.ts b/apps/parent-portal/src/lib/schemas/notification-preferences.ts new file mode 100644 index 0000000..a1e599c --- /dev/null +++ b/apps/parent-portal/src/lib/schemas/notification-preferences.ts @@ -0,0 +1,57 @@ +// 通知偏好 Zod schema +// 依据:02-architecture-design.md §14 通知偏好数据模型、project_rules §3.8 输入验证 +// - 三维矩阵:children[eventType][channel] = enabled +// - 用于表单校验 + localStorage 序列化校验 + +import { z } from "zod"; + +const NOTIFICATION_EVENT_TYPES = [ + "grade_recorded", + "homework_graded", + "homework_assigned", + "exam_published", + "attendance_alert", + "school_announcement", + "teacher_message", + "fee_reminder", + "event_invitation", +] as const; + +const NOTIFICATION_CHANNELS = [ + "in_app", + "push", + "sms", + "email", + "wechat", +] as const; + +const channelPrefSchema = z + .object({ + in_app: z.boolean().optional(), + push: z.boolean().optional(), + sms: z.boolean().optional(), + email: z.boolean().optional(), + wechat: z.boolean().optional(), + }) + .partial(); + +const eventTypePrefSchema = z.record( + z.enum(NOTIFICATION_EVENT_TYPES), + channelPrefSchema, +); + +export const notificationPreferencesSchema = z.object({ + parentId: z.string(), + preferences: z.record(z.string(), eventTypePrefSchema), + defaults: eventTypePrefSchema, + updatedAt: z.string(), +}); + +export type NotificationPreferencesInput = z.input< + typeof notificationPreferencesSchema +>; +export type NotificationPreferencesOutput = z.output< + typeof notificationPreferencesSchema +>; + +export { NOTIFICATION_EVENT_TYPES, NOTIFICATION_CHANNELS }; diff --git a/apps/parent-portal/src/lib/utils.test.ts b/apps/parent-portal/src/lib/utils.test.ts new file mode 100644 index 0000000..c5c7f00 --- /dev/null +++ b/apps/parent-portal/src/lib/utils.test.ts @@ -0,0 +1,275 @@ +// 工具函数单测 +// 依据:02-architecture-design.md §13 测试策略、project_rules §3.4 TypeScript 规则 + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { + cn, + formatDate, + formatDateTime, + formatRelativeTime, + formatNumber, + formatPercent, + generateId, + scoreToGrade, + gradeToChinese, + getLocalStorage, + setLocalStorage, + removeLocalStorage, + debounce, + throttle, + getBackoffDelay, + generateTraceId, +} from "./utils"; + +describe("cn", () => { + it("合并多个类名", () => { + expect(cn("foo", "bar")).toBe("foo bar"); + }); + + it("支持条件类名", () => { + const isHidden = false; + const isVisible = true; + expect(cn("base", isHidden && "hidden", isVisible && "visible")).toBe( + "base visible", + ); + }); + + it("合并 Tailwind 冲突类(后者优先)", () => { + expect(cn("px-2", "px-4")).toBe("px-4"); + }); +}); + +describe("formatDate", () => { + it("格式化 ISO 字符串(zh-CN)", () => { + const result = formatDate("2026-07-10T00:00:00Z", "zh-CN"); + expect(result).toContain("2026"); + expect(result).toContain("7"); + expect(result).toContain("10"); + }); + + it("格式化 Date 对象", () => { + const d = new Date("2026-07-10T00:00:00Z"); + const result = formatDate(d, "en-US"); + expect(result).toContain("2026"); + }); + + it("默认 locale 为 zh-CN", () => { + const result = formatDate("2026-07-10T00:00:00Z"); + expect(result).toContain("2026"); + }); +}); + +describe("formatDateTime", () => { + it("包含日期和时间", () => { + const result = formatDateTime("2026-07-10T09:30:00Z", "zh-CN"); + expect(result).toContain("2026"); + }); +}); + +describe("formatRelativeTime", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-07-10T12:00:00Z")); + }); + + it("秒级(刚刚)", () => { + const result = formatRelativeTime("2026-07-10T11:59:55Z", "zh-CN"); + expect(result).toMatch(/秒|second/); + }); + + it("分钟级", () => { + const result = formatRelativeTime("2026-07-10T11:30:00Z", "zh-CN"); + expect(result).toMatch(/分|minute/); + }); + + it("小时级", () => { + const result = formatRelativeTime("2026-07-10T09:00:00Z", "zh-CN"); + expect(result).toMatch(/小时|hour/); + }); + + it("天级", () => { + const result = formatRelativeTime("2026-07-08T09:00:00Z", "zh-CN"); + expect(result).toMatch(/天|day/); + }); +}); + +describe("formatNumber", () => { + it("格式化整数(zh-CN 千分位)", () => { + expect(formatNumber(1234567, "zh-CN")).toBe("1,234,567"); + }); + + it("格式化小数", () => { + const result = formatNumber(1234.56, "en-US"); + expect(result).toBe("1,234.56"); + }); +}); + +describe("formatPercent", () => { + it("格式化百分比(0-1 → 0-100%)", () => { + const result = formatPercent(0.96, "zh-CN"); + expect(result).toContain("96"); + expect(result).toContain("%"); + }); + + it("保留指定位数", () => { + const result = formatPercent(0.9633, "en-US", 2); + expect(result).toBe("96.33%"); + }); +}); + +describe("generateId", () => { + it("返回字符串", () => { + expect(typeof generateId()).toBe("string"); + }); + + it("每次调用返回不同值", () => { + expect(generateId()).not.toBe(generateId()); + }); +}); + +describe("scoreToGrade", () => { + it("A 等级(>=90%)", () => { + expect(scoreToGrade(95, 100)).toBe("A"); + expect(scoreToGrade(90, 100)).toBe("A"); + }); + + it("B 等级(80-89%)", () => { + expect(scoreToGrade(89, 100)).toBe("B"); + expect(scoreToGrade(80, 100)).toBe("B"); + }); + + it("C 等级(70-79%)", () => { + expect(scoreToGrade(75, 100)).toBe("C"); + expect(scoreToGrade(70, 100)).toBe("C"); + }); + + it("D 等级(60-69%)", () => { + expect(scoreToGrade(65, 100)).toBe("D"); + expect(scoreToGrade(60, 100)).toBe("D"); + }); + + it("F 等级(<60%)", () => { + expect(scoreToGrade(59, 100)).toBe("F"); + expect(scoreToGrade(0, 100)).toBe("F"); + }); + + it("支持非 100 满分", () => { + expect(scoreToGrade(45, 50)).toBe("A"); // 90% + expect(scoreToGrade(29, 50)).toBe("F"); // 58% + }); +}); + +describe("gradeToChinese", () => { + it("A → 优秀", () => { + expect(gradeToChinese("A")).toBe("优秀"); + }); + it("B → 良好", () => { + expect(gradeToChinese("B")).toBe("良好"); + }); + it("C → 及格", () => { + expect(gradeToChinese("C")).toBe("及格"); + }); + it("D → 及格", () => { + expect(gradeToChinese("D")).toBe("及格"); + }); + it("F → 不及格", () => { + expect(gradeToChinese("F")).toBe("不及格"); + }); + it("未知等级原样返回", () => { + expect(gradeToChinese("X")).toBe("X"); + }); +}); + +describe("localStorage 工具(SSR 安全)", () => { + it("setLocalStorage / getLocalStorage", () => { + expect(setLocalStorage("foo", "bar")).toBe(true); + expect(getLocalStorage("foo")).toBe("bar"); + }); + + it("removeLocalStorage", () => { + setLocalStorage("foo", "bar"); + removeLocalStorage("foo"); + expect(getLocalStorage("foo")).toBeNull(); + }); + + it("未设置的 key 返回 null", () => { + expect(getLocalStorage("not-exist")).toBeNull(); + }); +}); + +describe("debounce", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + it("延迟执行", () => { + const fn = vi.fn(); + const debounced = debounce(fn, 100); + debounced(); + expect(fn).not.toHaveBeenCalled(); + vi.advanceTimersByTime(100); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it("多次调用只执行最后一次", () => { + const fn = vi.fn(); + const debounced = debounce(fn, 100); + debounced("a"); + debounced("b"); + debounced("c"); + vi.advanceTimersByTime(100); + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith("c"); + }); +}); + +describe("throttle", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-07-10T00:00:00Z")); + }); + + it("首次立即执行", () => { + const fn = vi.fn(); + const throttled = throttle(fn, 100); + throttled(); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it("节流窗口内只执行一次", () => { + const fn = vi.fn(); + const throttled = throttle(fn, 100); + throttled(); + throttled(); + throttled(); + expect(fn).toHaveBeenCalledTimes(1); + }); +}); + +describe("getBackoffDelay", () => { + it("指数退避", () => { + expect(getBackoffDelay(0, 1000)).toBe(1000); + expect(getBackoffDelay(1, 1000)).toBe(2000); + expect(getBackoffDelay(2, 1000)).toBe(4000); + expect(getBackoffDelay(3, 1000)).toBe(8000); + }); + + it("上限 30 秒", () => { + expect(getBackoffDelay(10, 1000)).toBe(30000); + }); + + it("默认 baseDelay 1000", () => { + expect(getBackoffDelay(2)).toBe(4000); + }); +}); + +describe("generateTraceId", () => { + it("返回 parent- 前缀的字符串", () => { + const id = generateTraceId(); + expect(id).toMatch(/^parent-/); + }); + + it("每次调用返回不同值", () => { + expect(generateTraceId()).not.toBe(generateTraceId()); + }); +}); diff --git a/apps/parent-portal/src/lib/utils.ts b/apps/parent-portal/src/lib/utils.ts new file mode 100644 index 0000000..222db85 --- /dev/null +++ b/apps/parent-portal/src/lib/utils.ts @@ -0,0 +1,207 @@ +// 工具函数 +// 依据:project_rules §3.4 TypeScript 规则、§3.9 Tailwind 规范 + +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +/** + * 类名合并工具(project_rules §3.9) + * 支持条件类名,合并 Tailwind 类 + */ +export function cn(...inputs: ClassValue[]): string { + return twMerge(clsx(inputs)); +} + +/** + * 格式化日期(i18n,02 §17.4 国际化格式) + */ +export function formatDate( + date: string | Date, + locale: string = "zh-CN", +): string { + const d = typeof date === "string" ? new Date(date) : date; + return new Intl.DateTimeFormat(locale, { + year: "numeric", + month: "long", + day: "numeric", + }).format(d); +} + +/** + * 格式化日期时间 + */ +export function formatDateTime( + date: string | Date, + locale: string = "zh-CN", +): string { + const d = typeof date === "string" ? new Date(date) : date; + return new Intl.DateTimeFormat(locale, { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(d); +} + +/** + * 格式化相对时间(如"3小时前") + */ +export function formatRelativeTime( + date: string | Date, + locale: string = "zh-CN", +): string { + const d = typeof date === "string" ? new Date(date) : date; + const now = Date.now(); + const diff = now - d.getTime(); + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); + if (days > 0) return rtf.format(-days, "day"); + if (hours > 0) return rtf.format(-hours, "hour"); + if (minutes > 0) return rtf.format(-minutes, "minute"); + return rtf.format(-seconds, "second"); +} + +/** + * 格式化数字(02 §17.4) + */ +export function formatNumber(value: number, locale: string = "zh-CN"): string { + return new Intl.NumberFormat(locale).format(value); +} + +/** + * 格式化百分比 + */ +export function formatPercent( + value: number, + locale: string = "zh-CN", + digits: number = 1, +): string { + return new Intl.NumberFormat(locale, { + style: "percent", + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }).format(value); +} + +/** + * 生成唯一 ID(用于客户端临时标识) + */ +export function generateId(): string { + return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; +} + +/** + * 成绩等级映射(02 §17.4) + */ +export function scoreToGrade(score: number, maxScore: number = 100): string { + const percentage = (score / maxScore) * 100; + if (percentage >= 90) return "A"; + if (percentage >= 80) return "B"; + if (percentage >= 70) return "C"; + if (percentage >= 60) return "D"; + return "F"; +} + +/** + * 成绩等级中文映射 + */ +export function gradeToChinese(grade: string): string { + const map: Record = { + A: "优秀", + B: "良好", + C: "及格", + D: "及格", + F: "不及格", + }; + return map[grade] ?? grade; +} + +/** + * 安全获取 localStorage(SSR 安全) + */ +export function getLocalStorage(key: string): string | null { + if (typeof window === "undefined") return null; + try { + return window.localStorage.getItem(key); + } catch { + return null; + } +} + +/** + * 安全设置 localStorage + */ +export function setLocalStorage(key: string, value: string): boolean { + if (typeof window === "undefined") return false; + try { + window.localStorage.setItem(key, value); + return true; + } catch { + return false; + } +} + +/** + * 安全删除 localStorage + */ +export function removeLocalStorage(key: string): void { + if (typeof window === "undefined") return; + try { + window.localStorage.removeItem(key); + } catch { + // 忽略 + } +} + +/** + * 防抖 + */ +export function debounce void>( + fn: T, + delay: number, +): (...args: Parameters) => void { + let timer: ReturnType | null = null; + return (...args: Parameters) => { + if (timer) clearTimeout(timer); + timer = setTimeout(() => fn(...args), delay); + }; +} + +/** + * 节流 + */ +export function throttle void>( + fn: T, + interval: number, +): (...args: Parameters) => void { + let lastTime = 0; + return (...args: Parameters) => { + const now = Date.now(); + if (now - lastTime >= interval) { + lastTime = now; + fn(...args); + } + }; +} + +/** + * 指数退避延迟 + */ +export function getBackoffDelay( + attempt: number, + baseDelay: number = 1000, +): number { + return Math.min(baseDelay * Math.pow(2, attempt), 30000); +} + +/** + * 生成 trace_id + */ +export function generateTraceId(): string { + return `parent-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; +} diff --git a/apps/parent-portal/src/lib/web-vitals.ts b/apps/parent-portal/src/lib/web-vitals.ts new file mode 100644 index 0000000..f895ee5 --- /dev/null +++ b/apps/parent-portal/src/lib/web-vitals.ts @@ -0,0 +1,60 @@ +// Web Vitals 上报(P6-1) +// 依据:02-architecture-design.md §12 可观测性 +// - 采集 CLS / LCP / FID / FCP / TTFP / INP +// - 上报到 /api/v1/parent/web-vitals(经 api-gateway → data-ana) +// - 开发环境输出到 console + +"use client"; + +interface WebVitalMetric { + name: string; + value: number; + rating: "good" | "needs-improvement" | "poor"; + delta: number; + id: string; +} + +const REPORT_URL = "/api/v1/parent/web-vitals"; + +export async function reportWebVitals(metric: WebVitalMetric): Promise { + if (process.env.NODE_ENV === "development") { + // 开发环境输出到 console + console.debug( + `[Web Vitals] ${metric.name}: ${metric.value} (${metric.rating})`, + ); + } + + // 生产环境上报 + if (process.env.NODE_ENV === "production") { + try { + const body = { + name: metric.name, + value: metric.value, + rating: metric.rating, + delta: metric.delta, + id: metric.id, + page: window.location.pathname, + ts: Date.now(), + }; + + // 使用 sendBeacon 优先(页面卸载时不丢失) + if (navigator.sendBeacon) { + const blob = new Blob([JSON.stringify(body)], { + type: "application/json", + }); + navigator.sendBeacon(REPORT_URL, blob); + } else { + fetch(REPORT_URL, { + method: "POST", + body: JSON.stringify(body), + headers: { "Content-Type": "application/json" }, + keepalive: true, + }).catch(() => { + // 上报失败静默忽略 + }); + } + } catch { + // 忽略 + } + } +} diff --git a/apps/parent-portal/src/middleware.ts b/apps/parent-portal/src/middleware.ts new file mode 100644 index 0000000..232c652 --- /dev/null +++ b/apps/parent-portal/src/middleware.ts @@ -0,0 +1,58 @@ +// Next.js middleware:CSP 安全头 + 认证守卫 +// 依据:02-architecture-design.md §13 安全硬化(P6-6)、project_rules §4 安全规范 +// - CSP:限制脚本/样式/图片/连接来源 +// - 认证守卫:/parent/* 路由检查 token(SSR 阶段重定向) + +import { NextResponse, type NextRequest } from "next/server"; + +const PROTECTED_ROUTES = ["/parent"]; + +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + const response = NextResponse.next(); + + // CSP 安全头 + const csp = [ + "default-src 'self'", + "script-src 'self' 'unsafe-inline' 'unsafe-eval'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: https:", + "font-src 'self' data:", + "connect-src 'self' ws: wss:", + "frame-ancestors 'self'", + "base-uri 'self'", + "form-action 'self'", + ].join("; "); + + response.headers.set("Content-Security-Policy", csp); + response.headers.set("X-Frame-Options", "SAMEORIGIN"); + response.headers.set("X-Content-Type-Options", "nosniff"); + response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); + response.headers.set( + "Permissions-Policy", + "camera=(), microphone=(), geolocation=()", + ); + + // 认证守卫(SSR 阶段) + if (PROTECTED_ROUTES.some((route) => pathname.startsWith(route))) { + const token = request.cookies.get("parent_access_token")?.value; + // 注意:F12 裁决使用 localStorage token,SSR 阶段无法读取 + // 此处仅做 cookie 兜底检查,客户端 parent/layout.tsx 做完整守卫 + if (!token) { + // 检查是否有 Authorization header(MF 模式由 Shell 注入) + const authHeader = request.headers.get("authorization"); + if (!authHeader) { + const loginUrl = new URL("/login", request.url); + return NextResponse.redirect(loginUrl); + } + } + } + + return response; +} + +export const config = { + matcher: [ + "/((?!api|_next/static|_next/image|favicon.ico|mockServiceWorker.js|manifest.json|icon-).*)", + ], +}; diff --git a/apps/parent-portal/src/store/child-store.test.ts b/apps/parent-portal/src/store/child-store.test.ts new file mode 100644 index 0000000..8db2697 --- /dev/null +++ b/apps/parent-portal/src/store/child-store.test.ts @@ -0,0 +1,135 @@ +// Zustand child-store 单测 +// 依据:02-architecture-design.md §4.3 状态管理分层、ISSUE-009 纯前端切换 +// 覆盖:setChildren 默认选中 / switchChild 不调后端 / localStorage 持久化 / reset + +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { useChildStore } from "./child-store"; +import type { ChildInfo } from "@/types"; + +const mockChildren: ChildInfo[] = [ + { + id: "student-001", + name: "张小明", + grade: "grade.7", + schoolName: "实验中学", + classId: "class-7-1", + className: "初一(1)班", + }, + { + id: "student-002", + name: "张小红", + grade: "grade.5", + schoolName: "实验小学", + classId: "class-5-2", + className: "五年级(2)班", + }, +]; + +beforeEach(() => { + localStorage.clear(); + // 重置 store 到初始状态 + useChildStore.setState({ + children: [], + currentChildId: null, + isLoading: false, + }); +}); + +describe("setChildren", () => { + it("设置子女列表", () => { + useChildStore.getState().setChildren(mockChildren); + expect(useChildStore.getState().children).toHaveLength(2); + }); + + it("无 currentChildId 时默认选第一个活跃子女", () => { + useChildStore.getState().setChildren(mockChildren); + expect(useChildStore.getState().currentChildId).toBe("student-001"); + }); + + it("已存在的 currentChildId 保持不变", () => { + localStorage.setItem("parent_current_child_id", "student-002"); + useChildStore.setState({ currentChildId: "student-002" }); + useChildStore.getState().setChildren(mockChildren); + expect(useChildStore.getState().currentChildId).toBe("student-002"); + }); + + it("currentChildId 不在列表中时重选第一个", () => { + useChildStore.setState({ currentChildId: "student-999" }); + useChildStore.getState().setChildren(mockChildren); + expect(useChildStore.getState().currentChildId).toBe("student-001"); + }); + + it("跳过已归档子女作为默认选择", () => { + const withArchived: ChildInfo[] = [ + { ...mockChildren[0]!, isArchived: true }, + mockChildren[1]!, + ]; + useChildStore.getState().setChildren(withArchived); + expect(useChildStore.getState().currentChildId).toBe("student-002"); + }); + + it("默认选中后写入 localStorage", () => { + useChildStore.getState().setChildren(mockChildren); + expect(localStorage.getItem("parent_current_child_id")).toBe("student-001"); + }); + + it("空列表时 currentChildId 为 null", () => { + useChildStore.getState().setChildren([]); + expect(useChildStore.getState().currentChildId).toBeNull(); + }); +}); + +describe("switchChild(ISSUE-009:纯前端切换)", () => { + beforeEach(() => { + useChildStore.getState().setChildren(mockChildren); + }); + + it("切换到列表中存在的子女", () => { + useChildStore.getState().switchChild("student-002"); + expect(useChildStore.getState().currentChildId).toBe("student-002"); + }); + + it("切换不存在的子女被忽略", () => { + useChildStore.getState().switchChild("student-999"); + expect(useChildStore.getState().currentChildId).toBe("student-001"); + }); + + it("切换后写入 localStorage", () => { + useChildStore.getState().switchChild("student-002"); + expect(localStorage.getItem("parent_current_child_id")).toBe("student-002"); + }); + + it("切换不触发后端请求(无 fetch 调用)", () => { + const fetchSpy = vi.spyOn(globalThis, "fetch"); + useChildStore.getState().switchChild("student-002"); + expect(fetchSpy).not.toHaveBeenCalled(); + fetchSpy.mockRestore(); + }); +}); + +describe("setLoading", () => { + it("设置 loading 状态", () => { + useChildStore.getState().setLoading(true); + expect(useChildStore.getState().isLoading).toBe(true); + useChildStore.getState().setLoading(false); + expect(useChildStore.getState().isLoading).toBe(false); + }); +}); + +describe("reset", () => { + it("清空所有状态", () => { + useChildStore.getState().setChildren(mockChildren); + useChildStore.getState().switchChild("student-002"); + useChildStore.getState().setLoading(true); + useChildStore.getState().reset(); + expect(useChildStore.getState().children).toEqual([]); + expect(useChildStore.getState().currentChildId).toBeNull(); + expect(useChildStore.getState().isLoading).toBe(false); + }); + + it("清空 localStorage 中的 current_child_id", () => { + useChildStore.getState().setChildren(mockChildren); + useChildStore.getState().reset(); + expect(localStorage.getItem("parent_current_child_id")).toBeNull(); + }); +}); diff --git a/apps/parent-portal/src/store/child-store.ts b/apps/parent-portal/src/store/child-store.ts new file mode 100644 index 0000000..a21219c --- /dev/null +++ b/apps/parent-portal/src/store/child-store.ts @@ -0,0 +1,90 @@ +// Zustand store:子女切换状态 +// 依据:02-architecture-design.md §4.3 状态管理分层(Client Business 层) +// - ISSUE-009 裁决:switchChild 纯前端状态,不调后端 +// - 多子女切换:currentChildId 持久化到 localStorage +// - 跨标签同步:通过 BroadcastChannel 通知(P4-8 实现 hook,此处仅广播事件) + +import { create } from "zustand"; +import type { ChildInfo } from "@/types"; + +const CURRENT_CHILD_KEY = "parent_current_child_id"; + +// BroadcastChannel(跨标签同步,ISSUE-009 + P4-8) +let bc: BroadcastChannel | null = null; +if (typeof window !== "undefined" && "BroadcastChannel" in window) { + bc = new BroadcastChannel("parent-sync"); +} + +interface ChildStoreState { + children: ChildInfo[]; + currentChildId: string | null; + isLoading: boolean; + setChildren: (children: ChildInfo[]) => void; + switchChild: (childId: string) => void; + setLoading: (loading: boolean) => void; + reset: () => void; +} + +function loadCurrentChildId(): string | null { + if (typeof window === "undefined") return null; + return localStorage.getItem(CURRENT_CHILD_KEY); +} + +function persistCurrentChildId(childId: string): void { + if (typeof window === "undefined") return; + localStorage.setItem(CURRENT_CHILD_KEY, childId); +} + +export const useChildStore = create((set, get) => ({ + children: [], + currentChildId: loadCurrentChildId(), + isLoading: false, + + setChildren: (children) => { + const currentId = get().currentChildId; + // 若当前未选中或已选中的不在列表中,默认选第一个(非归档) + const activeChildren = children.filter((c) => !c.isArchived); + let nextCurrentId = currentId; + if (!nextCurrentId || !children.some((c) => c.id === nextCurrentId)) { + nextCurrentId = activeChildren[0]?.id ?? children[0]?.id ?? null; + if (nextCurrentId) { + persistCurrentChildId(nextCurrentId); + } + } + set({ children, currentChildId: nextCurrentId }); + }, + + // ISSUE-009:纯前端切换,不调后端 + switchChild: (childId) => { + const exists = get().children.some((c) => c.id === childId); + if (!exists) return; + persistCurrentChildId(childId); + set({ currentChildId: childId }); + + // 广播跨标签同步事件(P4-8) + bc?.postMessage({ + type: "child-switched", + childId, + ts: Date.now(), + source: "unknown", + }); + }, + + setLoading: (isLoading) => set({ isLoading }), + + reset: () => { + if (typeof window !== "undefined") { + localStorage.removeItem(CURRENT_CHILD_KEY); + } + set({ children: [], currentChildId: null, isLoading: false }); + }, +})); + +// 监听跨标签同步(P4-8:接收其他标签的切换事件) +if (bc && typeof window !== "undefined") { + bc.onmessage = (event) => { + if (event.data?.type === "child-switched") { + useChildStore.setState({ currentChildId: event.data.childId }); + } + }; +} diff --git a/apps/parent-portal/src/test/mocks/browser.ts b/apps/parent-portal/src/test/mocks/browser.ts new file mode 100644 index 0000000..1a54439 --- /dev/null +++ b/apps/parent-portal/src/test/mocks/browser.ts @@ -0,0 +1,9 @@ +// MSW browser worker(浏览器环境,用于开发) +// 依据:02-architecture-design.md §8 MSW +// 启用条件:NEXT_PUBLIC_API_MOCKING=enabled +// 需先执行 `npx msw init public/` 生成 mockServiceWorker.js + +import { setupWorker } from "msw/browser"; +import { handlers } from "./handlers"; + +export const worker = setupWorker(...handlers); diff --git a/apps/parent-portal/src/test/mocks/fixtures.ts b/apps/parent-portal/src/test/mocks/fixtures.ts new file mode 100644 index 0000000..a5f62e1 --- /dev/null +++ b/apps/parent-portal/src/test/mocks/fixtures.ts @@ -0,0 +1,322 @@ +// MSW mock fixtures:模拟 parent-bff GraphQL + iam login 响应 +// 依据: +// - ISSUE-010 iam GetChildrenByParent P0 阻塞 → 用 mock(student-001 + student-002) +// - 02-architecture-design.md §8 MSW 模拟未就绪上游 +// - F9 GraphQL(所有查询走 GraphQL) +// - ISSUE-004 登录端点走 REST + +import type { + UserSession, + ChildInfo, + ChildSummary, + GradeDataPoint, + AttendanceRecord, + HomeworkItem, + WeaknessPoint, + LearningTrend, + NotificationItem, + NotificationPreferences, + LoginResponse, +} from "@/types"; + +// ===== 用户 ===== +export const mockParent: UserSession = { + id: "parent-001", + email: "parent@example.com", + name: "张父", + roles: ["parent"], + permissions: [ + "DASHBOARD_VIEW", + "CHILD_GRADE_VIEW", + "CHILD_ATTENDANCE_VIEW", + "CHILD_HOMEWORK_VIEW", + "CHILD_WEAKNESS_VIEW", + "NOTIFICATION_VIEW", + "NOTIFICATION_PREFERENCE_UPDATE", + ], + dataScope: "CHILDREN", + schoolId: "school-001", +}; + +// ===== 子女列表(ISSUE-010:iam P0 阻塞,用 mock)===== +export const mockChildren: ChildInfo[] = [ + { + id: "student-001", + name: "张小明", + grade: "grade.7", + schoolName: "实验中学", + classId: "class-7-1", + className: "初一(1)班", + }, + { + id: "student-002", + name: "张小红", + grade: "grade.5", + schoolName: "实验小学", + classId: "class-5-2", + className: "五年级(2)班", + }, +]; + +// ===== 成绩数据点 ===== +export const mockGradeDataPoints: GradeDataPoint[] = [ + { + examId: "exam-001", + examName: "期中数学测验", + examDate: "2026-03-15T09:00:00Z", + subject: "数学", + studentScore: 92, + classAverage: 78, + classMax: 100, + classMin: 45, + gradeLevel: "A", + }, + { + examId: "exam-002", + examName: "期中语文测验", + examDate: "2026-03-16T09:00:00Z", + subject: "语文", + studentScore: 85, + classAverage: 75, + classMax: 95, + classMin: 52, + gradeLevel: "B", + }, + { + examId: "exam-003", + examName: "期中英语测验", + examDate: "2026-03-17T09:00:00Z", + subject: "英语", + studentScore: 88, + classAverage: 72, + classMax: 98, + classMin: 40, + gradeLevel: "B", + }, + { + examId: "exam-004", + examName: "月考物理", + examDate: "2026-04-10T09:00:00Z", + subject: "物理", + studentScore: 95, + classAverage: 80, + classMax: 100, + classMin: 50, + gradeLevel: "A", + }, +]; + +// ===== 子女概览 ===== +export const mockChildSummary: ChildSummary = { + childId: "student-001", + avgScore: 90, + classRank: 5, + classSize: 45, + attendanceRate: 0.96, + pendingHomeworkCount: 3, + recentGradeTrend: "up", + recentScores: mockGradeDataPoints, + upcomingEvents: [ + { + id: "event-001", + type: "exam", + title: "期末数学考试", + dueDate: "2026-07-20T09:00:00Z", + }, + { + id: "event-002", + type: "homework", + title: "物理实验报告", + dueDate: "2026-07-12T23:59:59Z", + }, + ], +}; + +// ===== 考勤记录 ===== +export const mockAttendance: AttendanceRecord[] = (() => { + const records: AttendanceRecord[] = []; + const baseDate = new Date("2026-06-01"); + for (let i = 0; i < 22; i++) { + const d = new Date(baseDate); + d.setDate(d.getDate() + i); + // 周末跳过 + const day = d.getDay(); + if (day === 0 || day === 6) continue; + let status: AttendanceRecord["status"] = "present"; + if (i === 5) status = "late"; + if (i === 12) status = "leave"; + records.push({ + id: `att-${i}`, + date: d.toISOString().slice(0, 10), + status, + }); + } + return records; +})(); + +// ===== 作业列表 ===== +export const mockHomework: HomeworkItem[] = [ + { + id: "hw-001", + title: "数学第三章习题", + subject: "数学", + className: "初一(1)班", + assignedDate: "2026-07-01T09:00:00Z", + dueDate: "2026-07-10T23:59:59Z", + status: "in_progress", + maxScore: 100, + }, + { + id: "hw-002", + title: "语文古诗文背诵", + subject: "语文", + className: "初一(1)班", + assignedDate: "2026-06-28T09:00:00Z", + dueDate: "2026-07-08T23:59:59Z", + status: "submitted", + maxScore: 50, + }, + { + id: "hw-003", + title: "英语听力练习", + subject: "英语", + className: "初一(1)班", + assignedDate: "2026-07-05T09:00:00Z", + dueDate: "2026-07-12T23:59:59Z", + status: "not_started", + maxScore: 30, + }, + { + id: "hw-004", + title: "物理实验报告", + subject: "物理", + className: "初一(1)班", + assignedDate: "2026-06-20T09:00:00Z", + dueDate: "2026-06-25T23:59:59Z", + status: "graded", + score: 28, + maxScore: 30, + feedback: "实验步骤完整,数据分析准确。", + }, +]; + +// ===== 学情薄弱点 ===== +export const mockWeakness: WeaknessPoint[] = [ + { + id: "wp-001", + knowledgePoint: "一元二次方程", + masteryLevel: 0.45, + subject: "数学", + recommendation: "建议复习配方法,完成针对性练习 20 道", + }, + { + id: "wp-002", + knowledgePoint: "文言文虚词", + masteryLevel: 0.6, + subject: "语文", + recommendation: "建议背诵常见虚词用法表", + }, + { + id: "wp-003", + knowledgePoint: "现在完成时", + masteryLevel: 0.72, + subject: "英语", + }, +]; + +// ===== 学习趋势 ===== +export const mockLearningTrend: LearningTrend = { + childId: "student-001", + period: "month", + dataPoints: [ + { date: "2026-03", score: 82, subject: "数学" }, + { date: "2026-04", score: 88, subject: "数学" }, + { date: "2026-05", score: 92, subject: "数学" }, + { date: "2026-06", score: 90, subject: "数学" }, + ], +}; + +// ===== 通知列表 ===== +export const mockNotifications: NotificationItem[] = [ + { + id: "notif-001", + childId: "student-001", + eventType: "grade_recorded", + title: "grade_recorded.title", + body: "grade_recorded.body", + read: false, + createdAt: new Date(Date.now() - 3600000).toISOString(), + actionUrl: "/parent/grades?childId=student-001", + pinned: true, + }, + { + id: "notif-002", + childId: "student-001", + eventType: "homework_assigned", + title: "homework_assigned.title", + body: "homework_assigned.body", + read: false, + createdAt: new Date(Date.now() - 7200000).toISOString(), + actionUrl: "/parent/homework?childId=student-001", + }, + { + id: "notif-003", + childId: null, + eventType: "school_announcement", + title: "school_announcement.title", + body: "school_announcement.body", + read: true, + createdAt: new Date(Date.now() - 86400000).toISOString(), + }, +]; + +// ===== 通知偏好默认值(ISSUE-033:P4 localStorage 降级)===== +export const mockNotificationPreferences: NotificationPreferences = { + parentId: "parent-001", + preferences: { + "student-001": { + grade_recorded: { in_app: true, push: true }, + homework_graded: { in_app: true, push: true }, + homework_assigned: { in_app: true, push: true }, + exam_published: { in_app: true, push: true, sms: false }, + attendance_alert: { in_app: true, push: true, sms: true }, + school_announcement: { in_app: true }, + teacher_message: { in_app: true, push: true }, + fee_reminder: { in_app: true, push: true, sms: true }, + event_invitation: { in_app: true, push: true }, + }, + "student-002": { + grade_recorded: { in_app: true, push: true }, + homework_graded: { in_app: true, push: true }, + homework_assigned: { in_app: true, push: true }, + exam_published: { in_app: true, push: true }, + attendance_alert: { in_app: true, push: true, sms: true }, + school_announcement: { in_app: true }, + teacher_message: { in_app: true, push: true }, + fee_reminder: { in_app: true, push: true, sms: true }, + event_invitation: { in_app: true, push: true }, + }, + }, + defaults: { + grade_recorded: { in_app: true, push: true }, + homework_graded: { in_app: true, push: true }, + homework_assigned: { in_app: true, push: true }, + exam_published: { in_app: true, push: true }, + attendance_alert: { in_app: true, push: true, sms: true }, + school_announcement: { in_app: true }, + teacher_message: { in_app: true, push: true }, + fee_reminder: { in_app: true, push: true, sms: true }, + event_invitation: { in_app: true, push: true }, + }, + updatedAt: new Date().toISOString(), +}; + +// ===== 登录响应(ISSUE-004:REST 登录)===== +export const mockLoginResponse: LoginResponse = { + user: mockParent, + tokens: { + accessToken: "mock-access-token-parent-001", + refreshToken: "mock-refresh-token-parent-001", + expiresIn: 3600, + }, +}; diff --git a/apps/parent-portal/src/test/mocks/handlers.ts b/apps/parent-portal/src/test/mocks/handlers.ts new file mode 100644 index 0000000..ed7bb88 --- /dev/null +++ b/apps/parent-portal/src/test/mocks/handlers.ts @@ -0,0 +1,188 @@ +// MSW handlers:parent-bff GraphQL + iam REST mock +// 依据: +// - 02-architecture-design.md §8 MSW 模拟未就绪上游 +// - F9 GraphQL(查询走 GraphQL) +// - ISSUE-004 登录走 REST +// - ISSUE-010 iam GetChildrenByParent P0 阻塞 → myChildren 返回 mock +// - ISSUE-033 通知偏好 P4 localStorage 降级 → updateNotificationPreferences 仍 mock 200 + +import { http, HttpResponse, graphql } from "msw"; +import type { ActionState, LoginResponse } from "@/types"; +import { + mockParent, + mockChildren, + mockChildSummary, + mockGradeDataPoints, + mockAttendance, + mockHomework, + mockWeakness, + mockLearningTrend, + mockNotifications, + mockNotificationPreferences, + mockLoginResponse, +} from "./fixtures"; + +// ===== REST handlers(iam login/refresh,ISSUE-004)===== +const restHandlers = [ + http.post("/api/v1/iam/login", async ({ request }) => { + const body = (await request.json()) as { email: string; password: string }; + // mock:任意 parent@example.com / password 通过 + if (body.email === "parent@example.com" && body.password === "password") { + const res: ActionState = { + success: true, + data: mockLoginResponse, + }; + return HttpResponse.json(res); + } + const err: ActionState = { + success: false, + error: { + code: "IAM_INVALID_CREDENTIALS", + message: "邮箱或密码错误", + }, + }; + return HttpResponse.json(err, { status: 401 }); + }), + + http.post("/api/v1/iam/refresh", async ({ request }) => { + const body = (await request.json()) as { refreshToken: string }; + if (body.refreshToken === "mock-refresh-token-parent-001") { + const res: ActionState<{ tokens: LoginResponse["tokens"] }> = { + success: true, + data: { + tokens: { + accessToken: "mock-access-token-parent-001-renewed", + refreshToken: "mock-refresh-token-parent-001-renewed", + expiresIn: 3600, + }, + }, + }; + return HttpResponse.json(res); + } + return HttpResponse.json( + { + success: false, + error: { code: "IAM_INVALID_REFRESH", message: "refresh token 无效" }, + }, + { status: 401 }, + ); + }), +]; + +// ===== GraphQL handlers(parent-bff,F9)===== +const graphqlHandlers = [ + graphql.query("CurrentUser", () => { + return HttpResponse.json({ + data: { + currentUser: mockParent, + }, + }); + }), + + graphql.query("MyChildren", () => { + return HttpResponse.json({ + data: { + myChildren: mockChildren, + }, + }); + }), + + graphql.query("ChildSummary", ({ variables }) => { + const childId = variables.childId as string; + return HttpResponse.json({ + data: { + childSummary: { ...mockChildSummary, childId }, + }, + }); + }), + + graphql.query("ChildGrades", () => { + return HttpResponse.json({ + data: { + childGrades: mockGradeDataPoints, + }, + }); + }), + + graphql.query("ChildAttendance", () => { + return HttpResponse.json({ + data: { + childAttendance: mockAttendance, + }, + }); + }), + + graphql.query("ChildHomework", () => { + return HttpResponse.json({ + data: { + childHomework: mockHomework, + }, + }); + }), + + graphql.query("ChildWeakness", () => { + return HttpResponse.json({ + data: { + childWeakness: mockWeakness, + }, + }); + }), + + graphql.query("ChildTrend", ({ variables }) => { + return HttpResponse.json({ + data: { + childTrend: { + ...mockLearningTrend, + childId: variables.childId as string, + }, + }, + }); + }), + + graphql.query("MyNotifications", () => { + return HttpResponse.json({ + data: { + myNotifications: mockNotifications, + }, + }); + }), + + graphql.query("MyNotificationPreferences", () => { + return HttpResponse.json({ + data: { + myNotificationPreferences: mockNotificationPreferences, + }, + }); + }), + + graphql.mutation("MarkAsRead", ({ variables }) => { + const notificationId = variables.notificationId as string; + return HttpResponse.json({ + data: { + markAsRead: { id: notificationId, read: true }, + }, + }); + }), + + graphql.mutation("MarkAllAsRead", () => { + return HttpResponse.json({ + data: { + markAllAsRead: { count: mockNotifications.length }, + }, + }); + }), + + // ISSUE-033:P4 localStorage 降级,但仍 mock 返回 200(前端优先用 localStorage) + graphql.mutation("UpdateNotificationPreferences", ({ variables }) => { + return HttpResponse.json({ + data: { + updateNotificationPreferences: { + parentId: (variables as { parentId: string }).parentId, + updatedAt: new Date().toISOString(), + }, + }, + }); + }), +]; + +export const handlers = [...restHandlers, ...graphqlHandlers]; diff --git a/apps/parent-portal/src/test/mocks/server.ts b/apps/parent-portal/src/test/mocks/server.ts new file mode 100644 index 0000000..66e433c --- /dev/null +++ b/apps/parent-portal/src/test/mocks/server.ts @@ -0,0 +1,7 @@ +// MSW server(Node 环境,用于 Vitest 测试) +// 依据:02-architecture-design.md §8 MSW、vitest.config.ts + +import { setupServer } from "msw/node"; +import { handlers } from "./handlers"; + +export const server = setupServer(...handlers); diff --git a/apps/parent-portal/src/test/setup.ts b/apps/parent-portal/src/test/setup.ts new file mode 100644 index 0000000..61d96ba --- /dev/null +++ b/apps/parent-portal/src/test/setup.ts @@ -0,0 +1,57 @@ +// Vitest 测试环境初始化 +// 依据:vitest.config.ts setupFiles + +import { beforeEach } from "vitest"; +import "@testing-library/jest-dom/vitest"; + +// jsdom 环境补充:matchMedia(recharts 等库依赖) +if (!window.matchMedia) { + Object.defineProperty(window, "matchMedia", { + writable: true, + value: (query: string): MediaQueryList => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + }), + }); +} + +// jsdom 环境补充:IntersectionObserver +if (!window.IntersectionObserver) { + class MockIntersectionObserver { + observe() {} + unobserve() {} + disconnect() {} + takeRecords() { + return []; + } + } + Object.defineProperty(window, "IntersectionObserver", { + writable: true, + value: MockIntersectionObserver, + }); +} + +// jsdom 环境补充:ResizeObserver(recharts 依赖) +if (!window.ResizeObserver) { + class MockResizeObserver { + observe() {} + unobserve() {} + disconnect() {} + } + Object.defineProperty(window, "ResizeObserver", { + writable: true, + value: MockResizeObserver, + }); +} + +// 清理 localStorage +beforeEach(() => { + localStorage.clear(); + sessionStorage.clear(); +}); diff --git a/apps/parent-portal/src/types/index.ts b/apps/parent-portal/src/types/index.ts new file mode 100644 index 0000000..6e8c7b0 --- /dev/null +++ b/apps/parent-portal/src/types/index.ts @@ -0,0 +1,247 @@ +// parent-portal 类型定义 +// 依据:02-architecture-design.md §2 领域模型、§14 通知偏好数据模型、§15 详细组件设计 + +// ===== 会话状态(Session)===== +export interface UserSession { + id: string; + email: string; + name: string; + roles: string[]; + permissions: string[]; + dataScope: DataScope; + schoolId?: string; +} + +export type DataScope = + | "ALL" + | "SCHOOL" + | "GRADE" + | "CLASS" + | "SUBJECT" + | "CHILDREN" // 家长特有:仅自己绑定的子女 + | "SELF"; + +// ===== 视口(Viewport)===== +export interface ViewportItem { + key: string; + label: string; + route: string; + icon: string | null; + sortOrder: string; + requiredPermission: string | null; + scope: "parent"; +} + +// ===== 子女信息(ChildInfo)===== +export interface ChildInfo { + id: string; + name: string; + avatar?: string; + grade: string; // i18n key + schoolName: string; + classId: string; + className?: string; + isArchived?: boolean; // 已归档(转学/离校) +} + +// ===== 子女仪表盘概览(childSummary)===== +export interface ChildSummary { + childId: string; + avgScore: number; + classRank: number; + classSize: number; + attendanceRate: number; // 0-1 + pendingHomeworkCount: number; + recentGradeTrend: "up" | "down" | "stable"; + recentScores: GradeDataPoint[]; + upcomingEvents?: UpcomingEvent[]; +} + +export interface UpcomingEvent { + id: string; + type: "exam" | "homework" | "activity"; + title: string; + dueDate: string; // ISO 8601 +} + +// ===== 成绩(Grade)===== +export interface GradeDataPoint { + examId: string; + examName: string; + examDate: string; // ISO 8601 + subject: string; + studentScore: number; + classAverage?: number; + classMax?: number; + classMin?: number; + gradeLevel: "A" | "B" | "C" | "D" | "F"; +} + +// ===== 考勤(Attendance)===== +export interface AttendanceRecord { + id: string; + date: string; // ISO 8601 date + status: "present" | "absent" | "late" | "leave"; + note?: string; +} + +// ===== 作业(Homework)===== +export interface HomeworkItem { + id: string; + title: string; + subject: string; + className: string; + assignedDate: string; // ISO 8601 + dueDate: string; // ISO 8601 + status: "not_started" | "in_progress" | "submitted" | "graded"; + score?: number; + maxScore: number; + feedback?: string; +} + +// ===== 学情分析 ===== +export interface WeaknessPoint { + id: string; + knowledgePoint: string; + masteryLevel: number; // 0-1 + subject: string; + recommendation?: string; +} + +export interface LearningTrend { + childId: string; + period: "week" | "month" | "semester"; + dataPoints: TrendDataPoint[]; +} + +export interface TrendDataPoint { + date: string; + score: number; + subject?: string; +} + +// ===== 通知 ===== +export interface NotificationItem { + id: string; + childId: string | null; + eventType: NotificationEventType; + title: string; // i18n key + body: string; // i18n key + 参数 + read: boolean; + createdAt: string; // ISO 8601 + actionUrl?: string; + pinned?: boolean; +} + +// ===== 通知偏好(02 §14 通知偏好数据模型)===== +export type NotificationEventType = + | "grade_recorded" + | "homework_graded" + | "homework_assigned" + | "exam_published" + | "attendance_alert" + | "school_announcement" + | "teacher_message" + | "fee_reminder" + | "event_invitation"; + +export type NotificationChannel = + "in_app" | "push" | "sms" | "email" | "wechat"; + +export interface NotificationPreferences { + parentId: string; + // 三维矩阵:children[eventType][channel] = enabled + // Partial 内层:允许部分 eventType 未设置(新增子女时不必预填全部事件) + preferences: Record< + string, // childId("*" 表示全部子女) + Partial< + Record< + NotificationEventType, + Partial> + > + > + >; + // 全局默认 + defaults: Record< + NotificationEventType, + Partial> + >; + updatedAt: string; // ISO 8601 +} + +export interface AvailableChannels { + in_app: boolean; + push: boolean; + sms: boolean; + email: boolean; + wechat: boolean; +} + +// ===== API 错误 ===== +export interface ApiError { + code: string; + message: string; + details?: unknown; + traceId?: string; +} + +// ===== ActionState(统一响应契约)===== +export type ActionState = + { success: true; data: T } | { success: false; error: ApiError }; + +// ===== 登录响应 ===== +export interface LoginResponse { + user: UserSession; + tokens: { + accessToken: string; + refreshToken: string; + expiresIn: number; + }; +} + +// ===== WebSocket 推送事件(P5)===== +export type WebSocketEvent = + | { + type: "NotificationRequested"; + notification: NotificationItem; + } + | { + type: "GradeRecorded"; + childId: string; + examId: string; + examName: string; + score: number; + } + | { + type: "SchoolAnnouncement"; + title: string; + body: string; + } + | { + type: "ChildUnbound"; + childId: string; + } + | { + type: "ChildBound"; + child: ChildInfo; + }; + +// ===== 跨标签同步消息 ===== +export type SyncMessage = + | { + type: "child-switched"; + childId: string; + ts: number; + source: string; + } + | { + type: "child-unbound"; + childId: string; + ts: number; + source: string; + } + | { + type: "preferences-updated"; + ts: number; + source: string; + }; diff --git a/apps/parent-portal/tailwind.config.js b/apps/parent-portal/tailwind.config.js new file mode 100644 index 0000000..4385ed6 --- /dev/null +++ b/apps/parent-portal/tailwind.config.js @@ -0,0 +1,62 @@ +/** @type {import('tailwindcss').Config} */ +// parent-portal Tailwind 配置 +// 复用 teacher-portal 设计令牌(与 Shell 共享) +// 依据:02-architecture-design.md §9 设计令牌三层(复用 packages/ui-tokens) +// project_rules §3.10 设计令牌规范(禁止 #hex / 禁止硬编码字体 / 禁止任意值) + +module.exports = { + content: ["./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: { + colors: { + paper: "hsl(var(--paper))", + ink: { + DEFAULT: "hsl(var(--ink))", + muted: "hsl(var(--ink-muted))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + soft: "hsl(var(--accent-soft))", + }, + rule: "hsl(var(--rule))", + success: "hsl(var(--success))", + warning: "hsl(var(--warning))", + danger: "hsl(var(--danger))", + }, + fontFamily: { + serif: ["var(--font-serif)"], + sans: ["var(--font-sans)"], + mono: ["var(--font-mono)"], + }, + fontSize: { + xs: ["var(--font-size-1)", "var(--line-height-tight)"], + sm: ["var(--font-size-2)", "var(--line-height-normal)"], + base: ["var(--font-size-3)", "var(--line-height-normal)"], + lg: ["var(--font-size-4)", "var(--line-height-normal)"], + xl: ["var(--font-size-5)", "var(--line-height-tight)"], + "2xl": ["var(--font-size-6)", "var(--line-height-tight)"], + "3xl": ["var(--font-size-7)", "var(--line-height-tight)"], + }, + spacing: { + 1: "var(--space-1)", + 2: "var(--space-2)", + 3: "var(--space-3)", + 4: "var(--space-4)", + 5: "var(--space-5)", + 6: "var(--space-6)", + 8: "var(--space-8)", + 10: "var(--space-10)", + 12: "var(--space-12)", + 16: "var(--space-16)", + }, + borderRadius: { + DEFAULT: "var(--radius-md)", + sm: "var(--radius-sm)", + md: "var(--radius-md)", + lg: "var(--radius-lg)", + full: "9999px", + }, + }, + }, + plugins: [], +}; diff --git a/apps/parent-portal/tsconfig.json b/apps/parent-portal/tsconfig.json new file mode 100644 index 0000000..4e297c4 --- /dev/null +++ b/apps/parent-portal/tsconfig.json @@ -0,0 +1,32 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2022", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "preserve", + "allowJs": true, + "noEmit": true, + "incremental": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + }, + "isolatedModules": true + }, + "include": [ + "next-env.d.ts", + "src/**/*", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/apps/parent-portal/vitest.config.ts b/apps/parent-portal/vitest.config.ts new file mode 100644 index 0000000..eff8b71 --- /dev/null +++ b/apps/parent-portal/vitest.config.ts @@ -0,0 +1,42 @@ +// Vitest 配置 +// 依据:02-architecture-design.md §13 测试策略分层 +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; +import { resolve } from "node:path"; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": resolve(__dirname, "./src"), + }, + }, + test: { + environment: "jsdom", + globals: true, + setupFiles: ["./src/test/setup.ts"], + include: [ + "src/**/*.{test,spec}.{ts,tsx}", + "src/**/__tests__/**/*.{ts,tsx}", + ], + exclude: ["node_modules", ".next", "e2e"], + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + include: ["src/**/*.{ts,tsx}"], + exclude: [ + "src/**/*.d.ts", + "src/test/**", + "src/mocks/**", + "src/types/**", + "src/**/*.config.{ts,tsx}", + ], + thresholds: { + statements: 85, + branches: 75, + functions: 85, + lines: 85, + }, + }, + }, +}); diff --git a/docs/troubleshooting/known-issues.md b/docs/troubleshooting/known-issues.md index e3b072e..6f05d14 100644 --- a/docs/troubleshooting/known-issues.md +++ b/docs/troubleshooting/known-issues.md @@ -434,24 +434,36 @@ > parent-portal 复用 teacher-portal Shell 暴露的 AppShell / usePermission / ApiClient / ErrorBoundary / 设计令牌 / A11y 工具集,本节仅记录 parent-portal **特有**的"场景→技术"映射,通用前端映射见 §2.12。 -| 场景 | 技术/规则 | -| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| P4 端口分配 | parent-portal = **4002**(004 §1.2 强制 4 端 4000-4003;ai07 初稿误用 3002 与 iam 冲突,ai15 修订) | -| P4 MF Remote 角色 | next.config.js 配 `@module-federation/nextjs-mf` 暴露 `./ChildSwitcher` 等家长端特有组件,复用 Shell 提供的 `AppShell`/`usePermission`/`ApiClient`/`RequirePermission` | -| P4 DataScope CHILDREN | 家长仅能查看自己绑定子女的数据(004 §5.4 L0 SELF 变体),前端通过 ApiClient 透传 userId + childId,下游服务按 DataScope 过滤 | -| P4 多子女切换 ChildSwitcher | Zustand slice(`selectedChildId`)+ 切换后 `queryClient.invalidateQueries({ queryKey: ['child', prevId] })`;多子女 Tab(≤3)或下拉(≥4);空态/单子女/多子女三态分支 | -| P4 多子女数量边界 | 0 子女(EmptyChildState 引导绑定)→ 1 子女(隐藏切换器)→ 2-3(Tab Bar)→ 4-10(下拉)→ >10(搜索 + 列表);超出 10 触发性能预警 | -| P4 通知偏好三维矩阵 | `Record>>`;react-hook-form + Zod 校验;默认值矩阵(成绩发布=邮件+短信、作业批改=邮件、教师沟通=短信、学校通知=邮件);渠道可用性校验 | -| P4 跨标签同步 | BroadcastChannel API(`parent-portal-sync`)+ localStorage 持久化 + LWW(Last-Write-Wins)冲突解决;同步 selectedChildId / 通知偏好 / 已读状态 | -| P4 隐私合规(COPPA/FERPA/PIPL) | 未成年人数据最小化采集;通知偏好默认 opt-in;数据保留 7 年后自动归档;前端不缓存敏感字段到 localStorage(仅 childId + 已读位图) | -| P4 i18n 5 语言含 RTL | next-intl 支持 zh-CN / en-US / zh-TW / ja-JP / ar-SA;ar-SA 用 `dir="rtl"` + Tailwind `rtl:` 变体;翻译文件按域分 namespace(parent/grades/notifications/preferences) | -| P4 PWA + Service Worker | manifest.json(name/short_name/icons/theme_color)+ Service Worker 缓存策略(App Shell precache + API stale-while-revalidate + 离线兜底页);P5+ 支持离线查看子女成绩缓存 | -| P4 API 契约版本管理 | 主版本 URL `/api/v1/*` → `/api/v2/*` + 子版本响应头 `X-API-Version` + Deprecation/Sunset 头埋点 + 低带宽场景 `X-Fields` 字段裁剪(GraphQL 风格,BFF REST 透传) | -| P4 性能预算 | Bundle ≤ 80KB JS / 20KB CSS / 200KB 总;recharts 懒加载;MF shared 单例避免 React 重复打包;dynamic import 非首屏组件;预加载子女数据 | -| P4 监控与降级 | 10 项前端指标(MF 加载失败率/ApiClient 错误率/Web Vitals/切换延迟等)+ 9 项降级策略(MF 失败回退独立页/BFF 5xx 降级缓存/WS 重试指数退避等) | -| P4 模块演化与解耦 | 拆分触发条件(Bundle > 120KB / 团队 > 3 人 / 独立部署需求);拆分后 MF 配置从 Shell 暴露;状态管理迁移路径(Zustand → Server Component + Server Actions);MF 升级路径(MF 2.0 → 3.0) | -| P4 iam 家长-学生关联 P0 阻塞 | iam 缺 `GetChildrenByParent` proto RPC + `GET /iam/children` REST + `iam_student_guardians` 表;ai02 补全前 parent-portal 仅能假设单子女硬编码 childId 开发调试 | -| P4 BFF 错误码前缀 | **BFF_PARENT\_=parent-bff**(coord final §B5,统一 `BFF_XXX_` 风格);前端按 `BFF_PARENT_*` 前缀路由 i18n key,与 BFF_TEACHER_/BFF_STUDENT_ 对齐 | +| 场景 | 技术/规则 | +| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| P4 端口分配 | parent-portal = **4002**(004 §1.2 强制 4 端 4000-4003;ai07 初稿误用 3002 与 iam 冲突,ai15 修订) | +| P4 MF Remote 角色 | next.config.js 配 `@module-federation/nextjs-mf` 暴露 `./ChildSwitcher` 等家长端特有组件,复用 Shell 提供的 `AppShell`/`usePermission`/`ApiClient`/`RequirePermission` | +| P4 DataScope CHILDREN | 家长仅能查看自己绑定子女的数据(004 §5.4 L0 SELF 变体),前端通过 ApiClient 透传 userId + childId,下游服务按 DataScope 过滤 | +| P4 多子女切换 ChildSwitcher | Zustand slice(`selectedChildId`)+ 切换后 `queryClient.invalidateQueries({ queryKey: ['child', prevId] })`;多子女 Tab(≤3)或下拉(≥4);空态/单子女/多子女三态分支 | +| P4 多子女数量边界 | 0 子女(EmptyChildState 引导绑定)→ 1 子女(隐藏切换器)→ 2-3(Tab Bar)→ 4-10(下拉)→ >10(搜索 + 列表);超出 10 触发性能预警 | +| P4 通知偏好三维矩阵 | `Record>>`;react-hook-form + Zod 校验;默认值矩阵(成绩发布=邮件+短信、作业批改=邮件、教师沟通=短信、学校通知=邮件);渠道可用性校验 | +| P4 跨标签同步 | BroadcastChannel API(`parent-portal-sync`)+ localStorage 持久化 + LWW(Last-Write-Wins)冲突解决;同步 selectedChildId / 通知偏好 / 已读状态 | +| P4 隐私合规(COPPA/FERPA/PIPL) | 未成年人数据最小化采集;通知偏好默认 opt-in;数据保留 7 年后自动归档;前端不缓存敏感字段到 localStorage(仅 childId + 已读位图) | +| P4 i18n 5 语言含 RTL | next-intl 支持 zh-CN / en-US / zh-TW / ja-JP / ar-SA;ar-SA 用 `dir="rtl"` + Tailwind `rtl:` 变体;翻译文件按域分 namespace(parent/grades/notifications/preferences) | +| P4 PWA + Service Worker | manifest.json(name/short_name/icons/theme_color)+ Service Worker 缓存策略(App Shell precache + API stale-while-revalidate + 离线兜底页);P5+ 支持离线查看子女成绩缓存 | +| P4 API 契约版本管理 | 主版本 URL `/api/v1/*` → `/api/v2/*` + 子版本响应头 `X-API-Version` + Deprecation/Sunset 头埋点 + 低带宽场景 `X-Fields` 字段裁剪(GraphQL 风格,BFF REST 透传) | +| P4 性能预算 | Bundle ≤ 80KB JS / 20KB CSS / 200KB 总;recharts 懒加载;MF shared 单例避免 React 重复打包;dynamic import 非首屏组件;预加载子女数据 | +| P4 监控与降级 | 10 项前端指标(MF 加载失败率/ApiClient 错误率/Web Vitals/切换延迟等)+ 9 项降级策略(MF 失败回退独立页/BFF 5xx 降级缓存/WS 重试指数退避等) | +| P4 模块演化与解耦 | 拆分触发条件(Bundle > 120KB / 团队 > 3 人 / 独立部署需求);拆分后 MF 配置从 Shell 暴露;状态管理迁移路径(Zustand → Server Component + Server Actions);MF 升级路径(MF 2.0 → 3.0) | +| P4 iam 家长-学生关联 P0 阻塞 | iam 缺 `GetChildrenByParent` proto RPC + `GET /iam/children` REST + `iam_student_guardians` 表;ai02 补全前 parent-portal 仅能假设单子女硬编码 childId 开发调试 | +| P4 BFF 错误码前缀 | **BFF_PARENT\_=parent-bff**(coord final §B5,统一 `BFF_XXX_` 风格);前端按 `BFF_PARENT_*` 前缀路由 i18n key,与 BFF_TEACHER_/BFF_STUDENT_ 对齐 | +| P4 认证 token 存储 | localStorage(F12 裁决,P4 用 localStorage,非 cookie);refreshAccessToken 竞态防护用 inflight Promise 复用,并发请求只触发一次 refresh fetch | +| P4 switchChild 纯前端 | ISSUE-009 裁决:切换子女不调后端,仅 Zustand slice + localStorage 持久化 selectedChildId;切换后由前端 invalidateQueries 触发数据刷新 | +| P4 通知偏好 localStorage 降级 | ISSUE-033 裁决:P4 localStorage 为准,更新先写本地再尝试 GraphQL mutation(失败不阻塞);P5+ 切换到后端为主存储 | +| P4 上游 mock 策略 | MSW 拦截 parent-bff GraphQL + iam REST;iam GetChildrenByParent P0 阻塞用 mock fixtures(student-001 + student-002 双子女);MSW server 在 providers 启动 | +| TS noUncheckedIndexedAccess | Record 索引访问返回 `T \| undefined`;内层 Record 改 `Partial>` 或空对象回退(`?? {}`)避免 undefined 传播 | +| urql fetchOptions headers 类型 | 返回类型须用 `Record` 累积再展开,不可用条件对象(`{...(token?{Authorization}:{})}`)否则 TS 推断不兼容 | +| Vitest Promise 身份比较 | `expect(p1).toBe(p2)` 对 Promise 不可靠(async 包装产生新 Promise);改为验证行为副作用(如 fetch 调用次数 === 1 证明竞态防护生效) | +| jsdom window.location.href | 设置触发 "Not implemented: navigation" stderr 警告(非错误,测试仍通过);mock 用 `Object.defineProperty(window, "location", {...})` 替代 `vi.spyOn` | +| MSW 响应过快观察加载态 | MSW 默认同步响应,加载态测试看不到 "加载中";用 `server.use()` + `delay(ms)` 延迟响应再断言按钮文本 | +| Testing Library getByRole 误匹配 | `getByRole("button", { name: /未读/ })` 会匹配通知项 aria-label="未读";改用 `getAllByRole("button")` + find 文本内容过滤精确选中筛选按钮 | +| ESLint flat config 测试文件 | 放宽规则需含 `*.test.tsx`/`*.spec.tsx`/`__tests__/**` 模式,否则 tsx 测试文件按严格规则报错 | +| Dockerfile 多阶段构建 | G1 裁决首次即多阶段:builder(node:20-alpine 跑 build)+ runner(node:20-alpine 跑 server),端口 4002,HEALTHCHECK 指向 `/api/health`,非 root 用户 nextjs | ### 2.14 parent-bff(TS/NestJS,P4) diff --git a/eslint.config.js b/eslint.config.js index 92bf4a8..5c9bec1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -48,7 +48,14 @@ module.exports = tseslint.config( // 测试文件放宽规则 { - files: ['**/*.test.ts', '**/*.spec.ts', '**/test/**'], + files: [ + '**/*.test.ts', + '**/*.test.tsx', + '**/*.spec.ts', + '**/*.spec.tsx', + '**/test/**', + '**/__tests__/**', + ], rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-non-null-assertion': 'off', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef4a295..d4b8763 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,11 +45,120 @@ importers: specifier: ^8.0.0 version: 8.63.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + apps/parent-portal: + dependencies: + '@hookform/resolvers': + specifier: ^3.9.0 + version: 3.10.0(react-hook-form@7.81.0(react@18.3.1)) + '@module-federation/nextjs-mf': + specifier: ^8.3.0 + version: 8.8.70(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-jsx@5.1.1(@babel/core@7.29.7)(react@18.3.1))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16)) + '@tanstack/react-query': + specifier: ^5.59.0 + version: 5.101.2(react@18.3.1) + '@urql/next': + specifier: ^1.1.0 + version: 1.1.5(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(urql@4.2.2(@urql/core@5.2.0(graphql@16.14.2))(react@18.3.1)) + clsx: + specifier: ^2.1.0 + version: 2.1.1 + graphql: + specifier: ^16.9.0 + version: 16.14.2 + next: + specifier: ^14.2.0 + version: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nuqs: + specifier: ^1.19.0 + version: 1.20.0(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + react: + specifier: ^18.3.0 + version: 18.3.1 + react-dom: + specifier: ^18.3.0 + version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.53.0 + version: 7.81.0(react@18.3.1) + recharts: + specifier: ^2.12.0 + version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: ^2.5.0 + version: 2.6.1 + urql: + specifier: ^4.2.0 + version: 4.2.2(@urql/core@5.2.0(graphql@16.14.2))(react@18.3.1) + zod: + specifier: ^3.23.0 + version: 3.25.76 + zustand: + specifier: ^4.5.0 + version: 4.5.7(@types/react@18.3.31)(react@18.3.1) + devDependencies: + '@axe-core/playwright': + specifier: ^4.10.0 + version: 4.12.1(playwright-core@1.61.1) + '@playwright/test': + specifier: ^1.47.0 + version: 1.61.1 + '@testing-library/jest-dom': + specifier: ^6.5.0 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.0.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/user-event': + specifier: ^14.5.0 + version: 14.6.1(@testing-library/dom@10.4.1) + '@types/node': + specifier: ^22.0.0 + version: 22.20.0 + '@types/react': + specifier: ^18.3.0 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.31) + '@vitejs/plugin-react': + specifier: ^4.3.0 + version: 4.7.0(vite@5.4.21(@types/node@22.20.0)(terser@5.48.0)) + '@vitest/coverage-v8': + specifier: ^2.1.0 + version: 2.1.9(vitest@2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0)) + autoprefixer: + specifier: ^10.4.0 + version: 10.5.2(postcss@8.5.16) + eslint: + specifier: ^9.0.0 + version: 9.39.4(jiti@2.6.1) + eslint-plugin-jsx-a11y: + specifier: ^6.10.0 + version: 6.10.2(eslint@9.39.4(jiti@2.6.1)) + jsdom: + specifier: ^25.0.0 + version: 25.0.1 + msw: + specifier: ^2.4.0 + version: 2.15.0(@types/node@22.20.0)(typescript@5.9.3) + postcss: + specifier: ^8.4.0 + version: 8.5.16 + tailwindcss: + specifier: ^3.4.0 + version: 3.4.19(tsx@4.23.0)(yaml@2.9.0) + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0) + apps/teacher-portal: dependencies: next: specifier: ^14.2.0 - version: 14.2.35(@opentelemetry/api@1.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.0 version: 18.3.1 @@ -82,8 +191,88 @@ importers: specifier: ^5.6.0 version: 5.9.3 + packages/hooks: + dependencies: + '@edu/ui-components': + specifier: workspace:* + version: link:../ui-components + react: + specifier: ^18.3.0 + version: 18.3.1 + react-dom: + specifier: ^18.3.0 + version: 18.3.1(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.0 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.31) + typescript: + specifier: ^5.6.0 + version: 5.9.3 + packages/shared-proto: {} + packages/shared-ts: + dependencies: + '@nestjs/common': + specifier: ^10.4.0 + version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': + specifier: ^10.4.0 + version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@paralleldrive/cuid2': + specifier: ^2.2.2 + version: 2.3.1 + drizzle-orm: + specifier: ^0.31.0 + version: 0.31.4(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(@types/react@18.3.31)(better-sqlite3@11.10.0)(mysql2@3.22.6(@types/node@22.20.0))(react@18.3.1) + kafkajs: + specifier: ^2.2.0 + version: 2.2.4 + pino: + specifier: ^9.4.0 + version: 9.14.0 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.0 + version: 7.8.2 + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.20.0 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + + packages/ui-components: + dependencies: + '@edu/ui-tokens': + specifier: workspace:* + version: link:../ui-tokens + react: + specifier: ^18.3.0 + version: 18.3.1 + react-dom: + specifier: ^18.3.0 + version: 18.3.1(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.0 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.31) + typescript: + specifier: ^5.6.0 + version: 5.9.3 + + packages/ui-tokens: {} + scripts/arch-scan: dependencies: better-sqlite3: @@ -107,7 +296,7 @@ importers: version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^10.4.0 - version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^10.4.0 version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) @@ -116,7 +305,7 @@ importers: version: 1.9.1 '@opentelemetry/auto-instrumentations-node': specifier: ^0.50.0 - version: 0.50.2(@opentelemetry/api@1.9.1) + version: 0.50.2(@opentelemetry/api@1.9.1)(encoding@0.1.13) '@opentelemetry/exporter-trace-otlp-http': specifier: ^0.53.0 version: 0.53.0(@opentelemetry/api@1.9.1) @@ -177,7 +366,7 @@ importers: version: 10.0.0 '@vitest/coverage-v8': specifier: ^2.1.0 - version: 2.1.9(vitest@2.1.9(@types/node@22.20.0)(terser@5.48.0)) + version: 2.1.9(vitest@2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0)) drizzle-kit: specifier: ^0.24.0 version: 0.24.2 @@ -195,7 +384,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.0 - version: 2.1.9(@types/node@22.20.0)(terser@5.48.0) + version: 2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0) services/content: dependencies: @@ -204,7 +393,7 @@ importers: version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^10.4.0 - version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^10.4.0 version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) @@ -213,7 +402,7 @@ importers: version: 1.9.1 '@opentelemetry/auto-instrumentations-node': specifier: ^0.55.0 - version: 0.55.3(@opentelemetry/api@1.9.1) + version: 0.55.3(@opentelemetry/api@1.9.1)(encoding@0.1.13) '@opentelemetry/exporter-trace-otlp-http': specifier: ^0.55.0 version: 0.55.0(@opentelemetry/api@1.9.1) @@ -262,7 +451,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.0 - version: 2.1.9(@types/node@22.20.0)(terser@5.48.0) + version: 2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0) services/core-edu: dependencies: @@ -271,7 +460,7 @@ importers: version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^10.4.0 - version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^10.4.0 version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) @@ -280,7 +469,7 @@ importers: version: 1.9.1 '@opentelemetry/auto-instrumentations-node': specifier: ^0.50.0 - version: 0.50.2(@opentelemetry/api@1.9.1) + version: 0.50.2(@opentelemetry/api@1.9.1)(encoding@0.1.13) '@opentelemetry/exporter-trace-otlp-http': specifier: ^0.53.0 version: 0.53.0(@opentelemetry/api@1.9.1) @@ -341,7 +530,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.0 - version: 2.1.9(@types/node@22.20.0)(terser@5.48.0) + version: 2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0) services/iam: dependencies: @@ -350,7 +539,7 @@ importers: version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^10.4.0 - version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^10.4.0 version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) @@ -359,7 +548,7 @@ importers: version: 1.9.1 '@opentelemetry/auto-instrumentations-node': specifier: ^0.50.0 - version: 0.50.2(@opentelemetry/api@1.9.1) + version: 0.50.2(@opentelemetry/api@1.9.1)(encoding@0.1.13) '@opentelemetry/exporter-trace-otlp-http': specifier: ^0.53.0 version: 0.53.0(@opentelemetry/api@1.9.1) @@ -368,7 +557,7 @@ importers: version: 0.53.0(@opentelemetry/api@1.9.1) bcrypt: specifier: ^5.1.0 - version: 5.1.1 + version: 5.1.1(encoding@0.1.13) drizzle-orm: specifier: ^0.31.0 version: 0.31.4(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(@types/react@18.3.31)(better-sqlite3@11.10.0)(mysql2@3.22.6(@types/node@22.20.0))(react@18.3.1) @@ -423,7 +612,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.0 - version: 2.1.9(@types/node@22.20.0)(terser@5.48.0) + version: 2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0) services/msg: dependencies: @@ -435,7 +624,7 @@ importers: version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^10.4.0 - version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^10.4.0 version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) @@ -444,7 +633,7 @@ importers: version: 1.9.1 '@opentelemetry/auto-instrumentations-node': specifier: ^0.55.0 - version: 0.55.3(@opentelemetry/api@1.9.1) + version: 0.55.3(@opentelemetry/api@1.9.1)(encoding@0.1.13) '@opentelemetry/exporter-trace-otlp-http': specifier: ^0.55.0 version: 0.55.0(@opentelemetry/api@1.9.1) @@ -496,7 +685,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.0 - version: 2.1.9(@types/node@22.20.0)(terser@5.48.0) + version: 2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0) services/teacher-bff: dependencies: @@ -505,7 +694,7 @@ importers: version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^10.4.0 - version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^10.4.0 version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) @@ -514,7 +703,7 @@ importers: version: 1.9.1 '@opentelemetry/auto-instrumentations-node': specifier: ^0.50.0 - version: 0.50.2(@opentelemetry/api@1.9.1) + version: 0.50.2(@opentelemetry/api@1.9.1)(encoding@0.1.13) '@opentelemetry/exporter-trace-otlp-http': specifier: ^0.53.0 version: 0.53.0(@opentelemetry/api@1.9.1) @@ -557,10 +746,21 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.0 - version: 2.1.9(@types/node@22.20.0)(terser@5.48.0) + version: 2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0) packages: + '@0no-co/graphql.web@1.3.2': + resolution: {integrity: sha512-Q1+pRlLhE31GOY/2c9BAEnFTNxO7Awtc6fhhEDlxyCBQ2N0IhD32cPVvPChrK9mwBNSgRdW/sF1kd2e0ojHj1Q==} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + graphql: + optional: true + + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -587,10 +787,52 @@ packages: resolution: {integrity: sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==} engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@axe-core/playwright@4.12.1': + resolution: {integrity: sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==} + peerDependencies: + playwright-core: '>= 1.0.0' + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} @@ -599,11 +841,43 @@ packages: resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.29.7': resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} @@ -687,6 +961,34 @@ packages: resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} engines: {node: '>=v18'} + '@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'} + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -698,6 +1000,15 @@ packages: resolution: {integrity: sha512-hjkrWvmVpCQiy2Km7LpVUmky8MbinNc2DzbpBaXSjgQYIXRK9LJ5BOfqldSBjp9E+U1I539LYAaSxxikQdXLLQ==} engines: {node: '>=18'} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.is' @@ -1317,6 +1628,11 @@ packages: engines: {node: '>=6'} hasBin: true + '@hookform/resolvers@3.10.0': + resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} + peerDependencies: + react-hook-form: ^7.0.0 + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1337,6 +1653,41 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/confirm@6.1.1': + resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@11.2.1': + resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.7': + resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/type@4.0.7': + resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@ioredis/commands@1.10.0': resolution: {integrity: sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==} @@ -1351,6 +1702,9 @@ packages: '@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'} @@ -1379,6 +1733,120 @@ packages: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true + '@module-federation/bridge-react-webpack-plugin@2.7.0': + resolution: {integrity: sha512-+7eYeJnIaofQHha8CK+FxPAXMIigd2xwONHi9rlYpdGqpCBIggRKMqL0b1owyDDNiAPF2lWUbxeNm0oUfF7GbA==} + + '@module-federation/cli@2.7.0': + resolution: {integrity: sha512-Nx5PQFmYqiiiIaU8uyzFcm9j8bNGb0SEo1GvbjI4ehfRJ5a92sUjNE++Q3o6zmVDI4wDXIhpGxioYdXi5QF40w==} + engines: {node: '>=16.0.0'} + hasBin: true + + '@module-federation/dts-plugin@2.7.0': + resolution: {integrity: sha512-mVKeGUf/7iqRMAFfikhsr2zXtA70WuzJdS1bPqqoeLQNRGXBLDjedcDG26RpIlsvuZcmIOqgN5Z6qw7Bh/HZUg==} + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 || ^6.0.0 + vue-tsc: '>=1.0.24' + peerDependenciesMeta: + vue-tsc: + optional: true + + '@module-federation/enhanced@2.7.0': + resolution: {integrity: sha512-1ZaiFIsFdH68MLoU7jrYxwhDt4WbDqmuTcnVsRqp9QZhZsex9h2zkeNpZdctL2Q1BtGsuNJ4ngJCOmO84O+6CQ==} + hasBin: true + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 || ^6.0.0 + vue-tsc: '>=1.0.24' + webpack: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + vue-tsc: + optional: true + webpack: + optional: true + + '@module-federation/error-codes@2.7.0': + resolution: {integrity: sha512-syToF3H77IbBhJ7auGMCIb5ZDmJ5tvaqSeEvncJrOCq7JBT96F4UDlQDyNh6kCVznvYqqHsPeEMrfI9b5/Omlg==} + + '@module-federation/inject-external-runtime-core-plugin@2.7.0': + resolution: {integrity: sha512-8xHVUWsnlYd1vQPUjVEO+OPBhBjbdur+jp33QwqwkJkSUW/WOgnybCevVyfiA05aaiSyPo1PluQPT6uhImVb7A==} + peerDependencies: + '@module-federation/runtime-tools': 2.7.0 + + '@module-federation/managers@2.7.0': + resolution: {integrity: sha512-cWohiUvSrY6dIfhwsRABmEWfvJiAD5u/gxU7XRk7bx5nYPj7qn5gvYWJtvLNWzenluHZR6sib+03QMlyo+MYKQ==} + + '@module-federation/manifest@2.7.0': + resolution: {integrity: sha512-phK5/pK/e0JyjMh7a2tvRXUFE0WCG9lxu4BtBQllZXNO0zjrroOHl2s/0sSUCdOq6NIL1HF9wTAudYiCKmNFjA==} + + '@module-federation/nextjs-mf@8.8.70': + resolution: {integrity: sha512-ZV2WrottzEH77FfmbJ4UV73cJqprcAiEkIHYSEMEscvKNpRrRSyped7kvmI+agOZvpdCkldmP2E7cx6ErZLkLg==} + peerDependencies: + next: ^12 || ^13 || ^14 || ^15 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + styled-jsx: '*' + webpack: ^5.40.0 + peerDependenciesMeta: + next: + optional: true + react: + optional: true + react-dom: + optional: true + styled-jsx: + optional: true + webpack: + optional: true + + '@module-federation/node@2.7.46': + resolution: {integrity: sha512-LgrV5NU8SHKznzxl1gAtAYiWT0lFe9K8+mYNZ1atGkhpQiSeQFVsQbObZq5USs0dgjmpZtLtkwfFOQ66fKyNRA==} + peerDependencies: + webpack: ^5.40.0 + peerDependenciesMeta: + webpack: + optional: true + + '@module-federation/rspack@2.7.0': + resolution: {integrity: sha512-VbYc/5cpIze16ysBZJvmeXU7NN8tAs6Q/MdDsllIwO+Ir7JIEXgGrs5Bs+K7BuAu3HpxyRC8KanJTD+JB91+WA==} + peerDependencies: + '@rspack/core': ^0.7.0 || ^1.0.0 || ^2.0.0-0 + typescript: ^4.9.0 || ^5.0.0 || ^6.0.0 + vue-tsc: '>=1.0.24' + peerDependenciesMeta: + typescript: + optional: true + vue-tsc: + optional: true + + '@module-federation/runtime-core@2.7.0': + resolution: {integrity: sha512-5ROZLVIeV9YnWO2RwCwSHcy6sh48yclErO/2GZ2Xe8lFubPrFirgU8pbwBjZw+All0ZzN44BGS4ECRMVFzVcpg==} + + '@module-federation/runtime-tools@2.7.0': + resolution: {integrity: sha512-AY61QeZ0jV0GywgR9j3Yd37KiXoY6gaSYABhjDF3q7XL9PNtb2Ezm1F/985wqaKJYX+qJvYv+PMH6hTvyHdPYQ==} + + '@module-federation/runtime@2.7.0': + resolution: {integrity: sha512-UtLozKKNvhT0D1+F0MEWsAmddJ39ItKW15E22LVMAwXmYZRSnzIvJQ2Y6kQ4LwhWABsw/GRSpUDex5OfhpSQPw==} + + '@module-federation/sdk@2.7.0': + resolution: {integrity: sha512-piiLEjaIdjbNq8E11Di6vsfryhcdN/+sBCH6NjG7gSU2VHHoHEayZQWWL7VVdS6rVjexF9McLhPTSrl0adUU+A==} + + '@module-federation/third-party-dts-extractor@2.7.0': + resolution: {integrity: sha512-hZJKkngQ4hwe0U+vrT3KOsU11qoHCQK0wB4x1bXoTgpTfV9j5NSuddOyRmwbvXpir9bDWh3xy7ghqsx3mFf3kA==} + + '@module-federation/webpack-bundler-runtime@2.7.0': + resolution: {integrity: sha512-3qLRIqcZVBNgJrZpiEzcTP8a6+mCdUV1QGk/XljawsQHyNieMVdLQtdLvkoF5Z5kRXNMovq+wv3vchKOMWyA8w==} + + '@mswjs/interceptors@0.41.9': + resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} + engines: {node: '>=18'} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@nestjs/cli@10.4.9': resolution: {integrity: sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA==} engines: {node: '>= 16.14'} @@ -1494,6 +1962,10 @@ packages: cpu: [x64] os: [win32] + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1511,6 +1983,18 @@ packages: engines: {node: '>=8.0.0', npm: '>=5.0.0'} hasBin: true + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/deferred-promise@3.0.0': + resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api-logs@0.53.0': resolution: {integrity: sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==} engines: {node: '>=14'} @@ -2503,6 +2987,9 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -2510,6 +2997,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} + engines: {node: '>=18'} + hasBin: true + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -2537,6 +3029,9 @@ packages: '@protobufjs/utf8@1.1.2': resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/rollup-android-arm-eabi@4.62.2': resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} cpu: [arm] @@ -2675,6 +3170,86 @@ packages: cpu: [x64] os: [win32] + '@rspack/binding-darwin-arm64@2.1.3': + resolution: {integrity: sha512-oOGI0RSL89Ehu9T22rugmfUY9OC2eBqLMeWRYsu7bhlUrjoXeVfGBBSEXCse666BQ1sAiM8hD/k7nqVria/okQ==} + cpu: [arm64] + os: [darwin] + + '@rspack/binding-darwin-x64@2.1.3': + resolution: {integrity: sha512-sVqWXNiFTMXAyN362y6IA+eJc8LXZKfHdhEJ/zDuMmRp+u2IvhgaF8tk3vX/OmeB9jydVjySijuiqk8No/FoCA==} + cpu: [x64] + os: [darwin] + + '@rspack/binding-linux-arm64-gnu@2.1.3': + resolution: {integrity: sha512-aCy9Zli/2Qf+Ee5otXfFQ6mhv5fEyn0wIoBVmouqtJoqOO21et6UTtJ+LHLsMDolwGLyHERAljeSFSmYX3/O5A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-arm64-musl@2.1.3': + resolution: {integrity: sha512-flIE7eluz0d21Fn28EVm3vPwoJooOSqtmjLFVSuOMcoCbwV9clfor195oIrAppp/W7dL/3XquFuVfrsa01Jy8Q==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rspack/binding-linux-riscv64-gnu@2.1.3': + resolution: {integrity: sha512-yojg8elye1nhsNeGncw0NrZ4pMGF7NVebR80CLg72TXyzfYwFJlFTdU5yUYb1Gy+JXIvrSCwzQt2QkyiEvmkfg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-riscv64-musl@2.1.3': + resolution: {integrity: sha512-6PvbXb3FOK4X3S5QGvoSW/sqExmsvAoPnQ/YSFrXvTphkXFezA7wnobmGBHT8JQP31hcFRzJHIZSIOKktzSzzg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rspack/binding-linux-x64-gnu@2.1.3': + resolution: {integrity: sha512-lMXjoGKf0SnviH596fmTszgtnXLHmWOoE90G8grG9MvKVa3pelRmfps5ewZL9s8ENf3NXRfOxIhIf/M9as6MqA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-x64-musl@2.1.3': + resolution: {integrity: sha512-0esT35v7pW2ZsJTMc/zDUHwpNXlPqyUCyuiLJvrAAUcdENrgOVe4DmFrgVJ2hwqI4GjeN1VBnGRJ8c+edAHH5w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rspack/binding-wasm32-wasi@2.1.3': + resolution: {integrity: sha512-UsrDjD59UEP0mhfN/Z+uTc3vLgiUvIr+mn92WC1sbQi9gtZohTYvaQYFhWuMkBqsACGcmZp704JAbbSrVrVYCA==} + cpu: [wasm32] + + '@rspack/binding-win32-arm64-msvc@2.1.3': + resolution: {integrity: sha512-42RS/SwKBTkNvXIPZXqTn0yEFN8zwGRfRe/ly0GDvPp/KxpLMFWxJmLxgtoLompU+0UCGvV4KpcBENt3oWs6BA==} + cpu: [arm64] + os: [win32] + + '@rspack/binding-win32-ia32-msvc@2.1.3': + resolution: {integrity: sha512-thk43H1JHHbetNF3txcsGXeOwZi2m6Luf/uVUqNORXjxr5VV8woHAgaAx4QXVZRg70z1VDjd8mBWnHBnKI0FGA==} + cpu: [ia32] + os: [win32] + + '@rspack/binding-win32-x64-msvc@2.1.3': + resolution: {integrity: sha512-ObaUcj+BHo/aBL1weyM3orApaHyAR5Phr3YNEpQEifxjZbNKM1iO5X5prN4OqEv3H+7o5e/Wh9Fy3N7Vvd+psA==} + cpu: [x64] + os: [win32] + + '@rspack/binding@2.1.3': + resolution: {integrity: sha512-4UGXJqUHmm36tWG1GgFZz3p8sQ5JuSgWI+gq1xPPoihS41uYJL3cXuJnirTeWsmVrusmYZTQhgU3tl3VYGcJYg==} + + '@rspack/core@2.1.3': + resolution: {integrity: sha512-1iGnxLrP+iyY0ZSjLZeQTaxrISpQz4yLyqJPmaF/l4uw27/dBcrloszAeLOJQ9jiv7EJtU0T5zB+LOFPpivIxA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@module-federation/runtime-tools': ^0.24.1 || ^2.0.0 + '@swc/helpers': ^0.5.23 + peerDependenciesMeta: + '@module-federation/runtime-tools': + optional: true + '@swc/helpers': + optional: true + '@sqltools/formatter@1.2.5': resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} @@ -2687,6 +3262,43 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@tanstack/query-core@5.101.2': + resolution: {integrity: sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==} + + '@tanstack/react-query@5.101.2': + resolution: {integrity: sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==} + peerDependencies: + react: ^18 || ^19 + + '@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 + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@tokenizer/inflate@0.2.7': resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} engines: {node: '>=18'} @@ -2697,12 +3309,30 @@ packages: '@ts-morph/common@0.25.0': resolution: {integrity: sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/aws-lambda@8.10.143': resolution: {integrity: sha512-u5vzlcR14ge/4pMTTMDQr3MF0wEe38B2F9o84uC4F43vN5DGTy63npRrB6jQhyt+C0lGv4ZfiRcRkqJoZuPnmg==} '@types/aws-lambda@8.10.147': resolution: {integrity: sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew==} + '@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/bcrypt@5.0.2': resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==} @@ -2733,6 +3363,33 @@ packages: '@types/conventional-commits-parser@5.0.2': resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -2798,6 +3455,9 @@ packages: '@types/react@18.3.31': resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} + '@types/semver@7.5.8': + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + '@types/send@0.17.6': resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} @@ -2807,9 +3467,15 @@ packages: '@types/serve-static@1.15.10': resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + '@types/shimmer@1.2.0': resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} @@ -2875,6 +3541,22 @@ packages: resolution: {integrity: sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@urql/core@5.2.0': + resolution: {integrity: sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A==} + + '@urql/next@1.1.5': + resolution: {integrity: sha512-xCNzgf6IarQ99FN4IPvj5nwkeX1z/mpO9+R54StZOK445lXlYbQnI8s4b9tNqTHbB09gt/vKE3ByRh2NLFMyfw==} + peerDependencies: + next: '>=13.0.0' + react: '>=18.0.0' + urql: ^4.0.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/coverage-v8@2.1.9': resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} peerDependencies: @@ -2994,6 +3676,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adm-zip@0.5.10: + resolution: {integrity: sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==} + engines: {node: '>=6.0'} + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -3053,6 +3739,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -3089,23 +3779,60 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + 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'} + array-back@6.2.3: resolution: {integrity: sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==} engines: {node: '>=12.17'} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -3117,10 +3844,22 @@ packages: peerDependencies: postcss: ^8.1.0 + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + aws-ssl-profiles@1.1.2: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} + axe-core@4.12.1: + resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -3313,6 +4052,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + cluster-key-slot@1.1.1: resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} engines: {node: '>=0.10.0'} @@ -3334,6 +4077,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + command-line-args@6.0.2: resolution: {integrity: sha512-AIjYVxrV9X752LmPDLbVYv8aMCuHPSLZJXEo2qo/xJfv+NYhaZ4sMSF01rM+gHPaMgvPM0l5D/F+Qx+i2WfSmQ==} engines: {node: '>=12.20'} @@ -3347,6 +4094,10 @@ packages: resolution: {integrity: sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==} engines: {node: '>=12.20.0'} + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + commander@13.1.0: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} @@ -3399,6 +4150,9 @@ packages: engines: {node: '>=16'} hasBin: true + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} @@ -3406,6 +4160,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3439,22 +4197,96 @@ packages: typescript: optional: true + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + 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==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true + 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==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + dargs@8.1.0: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} @@ -3478,6 +4310,12 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -3512,6 +4350,14 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} @@ -3523,6 +4369,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -3537,6 +4387,15 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} @@ -3659,10 +4518,17 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -3670,6 +4536,10 @@ packages: resolution: {integrity: sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==} engines: {node: '>=10.13.0'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -3681,6 +4551,14 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -3696,6 +4574,18 @@ packages: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} + engines: {node: '>= 0.4'} + esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -3742,6 +4632,12 @@ packages: peerDependencies: eslint: '>=7.0.0' + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -3812,6 +4708,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -3835,6 +4734,9 @@ packages: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3848,6 +4750,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.4.1: + resolution: {integrity: sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==} + engines: {node: '>=6.0.0'} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -3861,9 +4767,18 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.1.3: resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -3929,6 +4844,10 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -3940,6 +4859,10 @@ packages: typescript: '>3.6.0' webpack: ^5.11.0 + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} @@ -3971,6 +4894,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + 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} @@ -3979,6 +4907,13 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gauge@3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} @@ -3995,6 +4930,14 @@ packages: generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -4015,6 +4958,10 @@ packages: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} @@ -4053,6 +5000,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + google-logging-utils@0.0.2: resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} engines: {node: '>=14'} @@ -4064,6 +5015,14 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql@16.14.2: + resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -4075,10 +5034,18 @@ packages: has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + 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'} + has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} @@ -4086,6 +5053,9 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + headers-polyfill@5.0.1: + resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} + help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} @@ -4093,6 +5063,10 @@ packages: resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} engines: {node: '>=14'} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -4100,6 +5074,10 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -4121,6 +5099,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + iconv-lite@0.7.3: resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} @@ -4150,6 +5132,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'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -4172,6 +5158,14 @@ packages: resolution: {integrity: sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==} engines: {node: '>=18'} + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + ioredis@5.11.1: resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} engines: {node: '>=12.22.0'} @@ -4180,21 +5174,57 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-core-module@2.16.2: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -4207,6 +5237,10 @@ packages: resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -4215,6 +5249,21 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -4223,9 +5272,24 @@ packages: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -4234,17 +5298,49 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + is-text-path@2.0.0: resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} engines: {node: '>=8'} + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -4276,6 +5372,10 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -4291,6 +5391,20 @@ packages: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true + 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-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} @@ -4335,6 +5449,10 @@ packages: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} @@ -4348,6 +5466,13 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -4436,6 +5561,9 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + long-timeout@0.1.1: + resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} + long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -4449,10 +5577,21 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru.min@1.1.4: resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + 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==} @@ -4534,6 +5673,10 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -4564,6 +5707,9 @@ packages: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -4585,6 +5731,16 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.15.0: + resolution: {integrity: sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + multer@2.0.2: resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} engines: {node: '>= 10.16.0'} @@ -4596,6 +5752,10 @@ packages: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + mysql2@3.22.6: resolution: {integrity: sha512-fPKmeDGUzvFP7bMD5SASlJ5zIgvCC4hbanTmhbUlEmhyrY1hR4Hi3xLOWgTd3luYjLVifx6uGvMNJ2m/LlqEpg==} engines: {node: '>= 8.0'} @@ -4680,6 +5840,10 @@ packages: resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} engines: {node: '>=18'} + node-schedule@2.1.1: + resolution: {integrity: sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==} + engines: {node: '>=6'} + nopt@5.0.0: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} engines: {node: '>=6'} @@ -4697,6 +5861,14 @@ packages: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} deprecated: This package is no longer supported. + nuqs@1.20.0: + resolution: {integrity: sha512-nGVfv7eWMNxAzOJ9n8ARTo6ObqeEr1ETYZ+dIMCg/VfGUoZoPrqyTOndIvQIgUzK3pIC41mTXg10JJxh9ziEhw==} + peerDependencies: + next: '>=13.4 <14.0.2 || ^14.0.3' + + nwsapi@2.2.24: + resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -4709,6 +5881,22 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -4744,6 +5932,13 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -4771,6 +5966,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -4811,6 +6009,9 @@ packages: path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -4878,10 +6079,24 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-import@15.1.0: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -4964,6 +6179,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} + process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} @@ -4975,6 +6194,9 @@ packages: resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} engines: {node: ^16 || ^18 || >=20} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + protobufjs@7.6.5: resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} engines: {node: '>=12.0.0'} @@ -5017,6 +6239,37 @@ packages: peerDependencies: react: ^18.3.1 + react-hook-form@7.81.0: + resolution: {integrity: sha512-ocbmr2p5KBMoAfj4WCUvped33lVi1Kd5DuDUvQDnB6VEAacOjPI/jMbtDdbhco4y9ct4xUuCmMY0b/C9L0QHjw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -5040,6 +6293,20 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -5051,6 +6318,14 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + repeat-string@1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} @@ -5091,6 +6366,9 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + rettime@0.11.11: + resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -5108,6 +6386,12 @@ 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==} + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -5125,9 +6409,21 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -5135,6 +6431,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -5142,6 +6442,10 @@ packages: resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} engines: {node: '>= 10.13.0'} + schema-utils@4.3.0: + resolution: {integrity: sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==} + engines: {node: '>= 10.13.0'} + schema-utils@4.3.3: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} @@ -5156,6 +6460,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -5172,10 +6481,21 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@3.1.2: + resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -5233,6 +6553,9 @@ packages: sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + sorted-array-functions@1.3.0: + resolution: {integrity: sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -5273,10 +6596,17 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -5293,6 +6623,22 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -5312,6 +6658,10 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -5358,15 +6708,29 @@ packages: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + table-layout@4.1.1: resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} engines: {node: '>=12.17'} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tailwind-merge@2.6.1: + resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==} + tailwindcss@3.4.19: resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} engines: {node: '>=14.0.0'} hasBin: true + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} @@ -5454,6 +6818,9 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -5480,6 +6847,20 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts-core@7.4.8: + resolution: {integrity: sha512-c1P7u0EhACHj7lPy4MJm8iTFEU8+nB0LCtddH0fhP7noaVoXAqafMtOOeX+ulpuPBqnrRgRhw494RICT3mbhnw==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tldts@7.4.8: + resolution: {integrity: sha512-htwgN/8KRB3z3vnC0BOETVh2m499g5GmyTK9Wq5JBLX3FNz6tSBveAd+fQhzy9hkjif8vy2jwDMR1sGhLtZl2A==} + hasBin: true + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -5496,9 +6877,21 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -5542,10 +6935,30 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + type-fest@5.8.0: + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + engines: {node: '>=20'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} @@ -5630,6 +7043,10 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -5640,6 +7057,10 @@ packages: resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==} engines: {node: '>=18.17'} + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} @@ -5652,6 +7073,13 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + + upath@2.0.1: + resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} + engines: {node: '>=4'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -5661,6 +7089,17 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urql@4.2.2: + resolution: {integrity: sha512-3GgqNa6iF7bC4hY/ImJKN4REQILcSU9VKcKL8gfELZM8mM5BnLH1BsCc8kBdnVGD1LIFOs4W3O2idNHhON1r0w==} + peerDependencies: + '@urql/core': ^5.0.0 + react: '>= 16.8.0' + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5680,6 +7119,9 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vite-node@2.1.9: resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -5741,6 +7183,10 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + watchpack@2.5.2: resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} engines: {node: '>=10.13.0'} @@ -5751,6 +7197,10 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + webpack-node-externals@3.0.0: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} @@ -5769,9 +7219,38 @@ packages: webpack-cli: optional: true + 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'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -5785,6 +7264,9 @@ packages: wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + wonka@6.3.6: + resolution: {integrity: sha512-MXH+6mDHAZ2GuMpgKS055FR6v0xVP3XwquxIMYXgiW+FejHQlMGlvVRZT4qMCxR+bEo/FCtIdKxwej9WV3YQag==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -5812,6 +7294,25 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + 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==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -5820,6 +7321,9 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -5855,8 +7359,29 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + snapshots: + '@0no-co/graphql.web@1.3.2(graphql@16.14.2)': + optionalDependencies: + graphql: 16.14.2 + + '@adobe/css-tools@4.5.0': {} + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -5896,20 +7421,128 @@ snapshots: transitivePeerDependencies: - chokidar + '@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 + + '@axe-core/playwright@4.12.1(playwright-core@1.61.1)': + dependencies: + axe-core: 4.12.1 + playwright-core: 1.61.1 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@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.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.5 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + '@babel/helper-string-parser@7.29.7': {} '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + '@babel/parser@7.29.7': dependencies: '@babel/types': 7.29.7 + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -6032,6 +7665,26 @@ snapshots: '@types/conventional-commits-parser': 5.0.2 chalk: 5.6.2 + '@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': {} + '@drizzle-team/brocli@0.10.2': {} '@elastic/elasticsearch@8.19.2': @@ -6056,6 +7709,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 @@ -6411,6 +8080,10 @@ snapshots: protobufjs: 7.6.5 yargs: 17.7.3 + '@hookform/resolvers@3.10.0(react-hook-form@7.81.0(react@18.3.1))': + dependencies: + react-hook-form: 7.81.0(react@18.3.1) + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -6427,6 +8100,33 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@inquirer/ansi@2.0.7': {} + + '@inquirer/confirm@6.1.1(@types/node@22.20.0)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@22.20.0) + '@inquirer/type': 4.0.7(@types/node@22.20.0) + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/core@11.2.1(@types/node@22.20.0)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@22.20.0) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/figures@2.0.7': {} + + '@inquirer/type@4.0.7(@types/node@22.20.0)': + optionalDependencies: + '@types/node': 22.20.0 + '@ioredis/commands@1.10.0': {} '@isaacs/cliui@8.0.2': @@ -6445,6 +8145,11 @@ snapshots: '@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/source-map@0.3.11': @@ -6467,12 +8172,12 @@ snapshots: '@lukeed/csprng@1.1.0': {} - '@mapbox/node-pre-gyp@1.0.11': + '@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)': dependencies: detect-libc: 2.1.2 https-proxy-agent: 5.0.1 make-dir: 3.1.0 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 @@ -6482,6 +8187,186 @@ snapshots: - encoding - supports-color + '@module-federation/bridge-react-webpack-plugin@2.7.0': + dependencies: + '@module-federation/sdk': 2.7.0 + '@types/semver': 7.5.8 + semver: 7.6.3 + + '@module-federation/cli@2.7.0(typescript@5.9.3)': + dependencies: + '@module-federation/dts-plugin': 2.7.0(typescript@5.9.3) + '@module-federation/sdk': 2.7.0 + commander: 11.1.0 + jiti: 2.4.2 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - vue-tsc + + '@module-federation/dts-plugin@2.7.0(typescript@5.9.3)': + dependencies: + '@module-federation/error-codes': 2.7.0 + '@module-federation/managers': 2.7.0 + '@module-federation/sdk': 2.7.0 + '@module-federation/third-party-dts-extractor': 2.7.0 + adm-zip: 0.5.10 + isomorphic-ws: 5.0.0(ws@8.21.0) + node-schedule: 2.1.1 + typescript: 5.9.3 + undici: 7.28.0 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@module-federation/enhanced@2.7.0(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16))': + dependencies: + '@module-federation/bridge-react-webpack-plugin': 2.7.0 + '@module-federation/cli': 2.7.0(typescript@5.9.3) + '@module-federation/dts-plugin': 2.7.0(typescript@5.9.3) + '@module-federation/error-codes': 2.7.0 + '@module-federation/inject-external-runtime-core-plugin': 2.7.0(@module-federation/runtime-tools@2.7.0) + '@module-federation/managers': 2.7.0 + '@module-federation/manifest': 2.7.0(typescript@5.9.3) + '@module-federation/rspack': 2.7.0(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(typescript@5.9.3) + '@module-federation/runtime-tools': 2.7.0 + '@module-federation/sdk': 2.7.0 + '@module-federation/webpack-bundler-runtime': 2.7.0 + schema-utils: 4.3.0 + tapable: 2.3.0 + upath: 2.0.1 + optionalDependencies: + typescript: 5.9.3 + webpack: 5.97.1(postcss@8.5.16) + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - utf-8-validate + + '@module-federation/error-codes@2.7.0': {} + + '@module-federation/inject-external-runtime-core-plugin@2.7.0(@module-federation/runtime-tools@2.7.0)': + dependencies: + '@module-federation/runtime-tools': 2.7.0 + + '@module-federation/managers@2.7.0': + dependencies: + '@module-federation/sdk': 2.7.0 + empathic: 2.0.0 + + '@module-federation/manifest@2.7.0(typescript@5.9.3)': + dependencies: + '@module-federation/dts-plugin': 2.7.0(typescript@5.9.3) + '@module-federation/managers': 2.7.0 + '@module-federation/sdk': 2.7.0 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - vue-tsc + + '@module-federation/nextjs-mf@8.8.70(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-jsx@5.1.1(@babel/core@7.29.7)(react@18.3.1))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16))': + dependencies: + '@module-federation/enhanced': 2.7.0(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16)) + '@module-federation/node': 2.7.46(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16)) + '@module-federation/runtime': 2.7.0 + '@module-federation/runtime-core': 2.7.0 + '@module-federation/sdk': 2.7.0 + '@module-federation/webpack-bundler-runtime': 2.7.0 + fast-glob: 3.3.3 + optionalDependencies: + next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(@babel/core@7.29.7)(react@18.3.1) + webpack: 5.97.1(postcss@8.5.16) + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - typescript + - utf-8-validate + - vue-tsc + + '@module-federation/node@2.7.46(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16))': + dependencies: + '@module-federation/enhanced': 2.7.0(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16)) + '@module-federation/runtime': 2.7.0 + '@module-federation/sdk': 2.7.0 + encoding: 0.1.13 + node-fetch: 2.7.0(encoding@0.1.13) + tapable: 2.3.0 + optionalDependencies: + webpack: 5.97.1(postcss@8.5.16) + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - typescript + - utf-8-validate + - vue-tsc + + '@module-federation/rspack@2.7.0(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(typescript@5.9.3)': + dependencies: + '@module-federation/bridge-react-webpack-plugin': 2.7.0 + '@module-federation/dts-plugin': 2.7.0(typescript@5.9.3) + '@module-federation/inject-external-runtime-core-plugin': 2.7.0(@module-federation/runtime-tools@2.7.0) + '@module-federation/managers': 2.7.0 + '@module-federation/manifest': 2.7.0(typescript@5.9.3) + '@module-federation/runtime-tools': 2.7.0 + '@module-federation/sdk': 2.7.0 + '@rspack/core': 2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@module-federation/runtime-core@2.7.0': + dependencies: + '@module-federation/error-codes': 2.7.0 + '@module-federation/sdk': 2.7.0 + + '@module-federation/runtime-tools@2.7.0': + dependencies: + '@module-federation/runtime': 2.7.0 + '@module-federation/webpack-bundler-runtime': 2.7.0 + + '@module-federation/runtime@2.7.0': + dependencies: + '@module-federation/error-codes': 2.7.0 + '@module-federation/runtime-core': 2.7.0 + '@module-federation/sdk': 2.7.0 + + '@module-federation/sdk@2.7.0': {} + + '@module-federation/third-party-dts-extractor@2.7.0': + dependencies: + empathic: 2.0.0 + exsolve: 1.0.8 + + '@module-federation/webpack-bundler-runtime@2.7.0': + dependencies: + '@module-federation/error-codes': 2.7.0 + '@module-federation/runtime': 2.7.0 + '@module-federation/sdk': 2.7.0 + + '@mswjs/interceptors@0.41.9': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + '@nestjs/cli@10.4.9': dependencies: '@angular-devkit/core': 17.3.11(chokidar@3.6.0) @@ -6563,10 +8448,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/core@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nuxtjs/opencollective': 0.3.2 + '@nuxtjs/opencollective': 0.3.2(encoding@0.1.13) fast-safe-stringify: 2.1.1 iterare: 1.2.1 path-to-regexp: 3.3.0 @@ -6582,7 +8467,7 @@ snapshots: '@nestjs/platform-express@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)': dependencies: '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) body-parser: 1.20.4 cors: 2.8.5 express: 4.22.1 @@ -6642,6 +8527,8 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.33': optional: true + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6654,14 +8541,25 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@nuxtjs/opencollective@0.3.2': + '@nuxtjs/opencollective@0.3.2(encoding@0.1.13)': dependencies: chalk: 4.1.2 consola: 2.15.3 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) transitivePeerDependencies: - encoding + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/deferred-promise@3.0.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@opentelemetry/api-logs@0.53.0': dependencies: '@opentelemetry/api': 1.9.1 @@ -6676,7 +8574,7 @@ snapshots: '@opentelemetry/api@1.9.1': {} - '@opentelemetry/auto-instrumentations-node@0.50.2(@opentelemetry/api@1.9.1)': + '@opentelemetry/auto-instrumentations-node@0.50.2(@opentelemetry/api@1.9.1)(encoding@0.1.13)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.1) @@ -6723,14 +8621,14 @@ snapshots: '@opentelemetry/resource-detector-aws': 1.12.0(@opentelemetry/api@1.9.1) '@opentelemetry/resource-detector-azure': 0.2.12(@opentelemetry/api@1.9.1) '@opentelemetry/resource-detector-container': 0.4.4(@opentelemetry/api@1.9.1) - '@opentelemetry/resource-detector-gcp': 0.29.13(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-gcp': 0.29.13(@opentelemetry/api@1.9.1)(encoding@0.1.13) '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-node': 0.53.0(@opentelemetry/api@1.9.1) transitivePeerDependencies: - encoding - supports-color - '@opentelemetry/auto-instrumentations-node@0.55.3(@opentelemetry/api@1.9.1)': + '@opentelemetry/auto-instrumentations-node@0.55.3(@opentelemetry/api@1.9.1)(encoding@0.1.13)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) @@ -6777,7 +8675,7 @@ snapshots: '@opentelemetry/resource-detector-aws': 1.12.0(@opentelemetry/api@1.9.1) '@opentelemetry/resource-detector-azure': 0.6.1(@opentelemetry/api@1.9.1) '@opentelemetry/resource-detector-container': 0.6.1(@opentelemetry/api@1.9.1) - '@opentelemetry/resource-detector-gcp': 0.33.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-gcp': 0.33.1(@opentelemetry/api@1.9.1)(encoding@0.1.13) '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-node': 0.57.2(@opentelemetry/api@1.9.1) transitivePeerDependencies: @@ -7924,24 +9822,24 @@ snapshots: '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.42.0 - '@opentelemetry/resource-detector-gcp@0.29.13(@opentelemetry/api@1.9.1)': + '@opentelemetry/resource-detector-gcp@0.29.13(@opentelemetry/api@1.9.1)(encoding@0.1.13)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.42.0 - gcp-metadata: 6.1.1 + gcp-metadata: 6.1.1(encoding@0.1.13) transitivePeerDependencies: - encoding - supports-color - '@opentelemetry/resource-detector-gcp@0.33.1(@opentelemetry/api@1.9.1)': + '@opentelemetry/resource-detector-gcp@0.33.1(@opentelemetry/api@1.9.1)(encoding@0.1.13)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.42.0 - gcp-metadata: 6.1.1 + gcp-metadata: 6.1.1(encoding@0.1.13) transitivePeerDependencies: - encoding - supports-color @@ -8135,11 +10033,19 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@paralleldrive/cuid2@2.3.1': + dependencies: + '@noble/hashes': 1.8.0 + '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.61.1': + dependencies: + playwright: 1.61.1 + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -8160,6 +10066,8 @@ snapshots: '@protobufjs/utf8@1.1.2': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/rollup-android-arm-eabi@4.62.2': optional: true @@ -8235,6 +10143,68 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true + '@rspack/binding-darwin-arm64@2.1.3': + optional: true + + '@rspack/binding-darwin-x64@2.1.3': + optional: true + + '@rspack/binding-linux-arm64-gnu@2.1.3': + optional: true + + '@rspack/binding-linux-arm64-musl@2.1.3': + optional: true + + '@rspack/binding-linux-riscv64-gnu@2.1.3': + optional: true + + '@rspack/binding-linux-riscv64-musl@2.1.3': + optional: true + + '@rspack/binding-linux-x64-gnu@2.1.3': + optional: true + + '@rspack/binding-linux-x64-musl@2.1.3': + optional: true + + '@rspack/binding-wasm32-wasi@2.1.3': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rspack/binding-win32-arm64-msvc@2.1.3': + optional: true + + '@rspack/binding-win32-ia32-msvc@2.1.3': + optional: true + + '@rspack/binding-win32-x64-msvc@2.1.3': + optional: true + + '@rspack/binding@2.1.3': + optionalDependencies: + '@rspack/binding-darwin-arm64': 2.1.3 + '@rspack/binding-darwin-x64': 2.1.3 + '@rspack/binding-linux-arm64-gnu': 2.1.3 + '@rspack/binding-linux-arm64-musl': 2.1.3 + '@rspack/binding-linux-riscv64-gnu': 2.1.3 + '@rspack/binding-linux-riscv64-musl': 2.1.3 + '@rspack/binding-linux-x64-gnu': 2.1.3 + '@rspack/binding-linux-x64-musl': 2.1.3 + '@rspack/binding-wasm32-wasi': 2.1.3 + '@rspack/binding-win32-arm64-msvc': 2.1.3 + '@rspack/binding-win32-ia32-msvc': 2.1.3 + '@rspack/binding-win32-x64-msvc': 2.1.3 + + '@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23)': + dependencies: + '@rspack/binding': 2.1.3 + optionalDependencies: + '@module-federation/runtime-tools': 2.7.0 + '@swc/helpers': 0.5.23 + '@sqltools/formatter@1.2.5': {} '@swc/counter@0.1.3': {} @@ -8248,6 +10218,47 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.8.1 + '@tanstack/query-core@5.101.2': {} + + '@tanstack/react-query@5.101.2(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.101.2 + react: 18.3.1 + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@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.5.0 + 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@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + '@types/react-dom': 18.3.7(@types/react@18.3.31) + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + '@tokenizer/inflate@0.2.7': dependencies: debug: 4.4.3 @@ -8264,10 +10275,38 @@ snapshots: path-browserify: 1.0.1 tinyglobby: 0.2.17 + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + '@types/aws-lambda@8.10.143': {} '@types/aws-lambda@8.10.147': {} + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@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.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + '@types/bcrypt@5.0.2': dependencies: '@types/node': 22.20.0 @@ -8305,6 +10344,30 @@ snapshots: dependencies: '@types/node': 22.20.0 + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -8385,6 +10448,8 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 + '@types/semver@7.5.8': {} + '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 @@ -8400,8 +10465,14 @@ snapshots: '@types/node': 22.20.0 '@types/send': 0.17.6 + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 22.20.0 + '@types/shimmer@1.2.0': {} + '@types/statuses@2.0.6': {} + '@types/tedious@4.0.14': dependencies: '@types/node': 22.20.0 @@ -8499,7 +10570,32 @@ snapshots: '@typescript-eslint/types': 8.63.0 eslint-visitor-keys: 5.0.1 - '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@22.20.0)(terser@5.48.0))': + '@urql/core@5.2.0(graphql@16.14.2)': + dependencies: + '@0no-co/graphql.web': 1.3.2(graphql@16.14.2) + wonka: 6.3.6 + transitivePeerDependencies: + - graphql + + '@urql/next@1.1.5(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(urql@4.2.2(@urql/core@5.2.0(graphql@16.14.2))(react@18.3.1))': + dependencies: + next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + urql: 4.2.2(@urql/core@5.2.0(graphql@16.14.2))(react@18.3.1) + + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.20.0)(terser@5.48.0))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21(@types/node@22.20.0)(terser@5.48.0) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -8513,7 +10609,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.2 tinyrainbow: 1.2.0 - vitest: 2.1.9(@types/node@22.20.0)(terser@5.48.0) + vitest: 2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0) transitivePeerDependencies: - supports-color @@ -8524,12 +10620,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.20.0)(terser@5.48.0))': + '@vitest/mocker@2.1.9(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(vite@5.4.21(@types/node@22.20.0)(terser@5.48.0))': dependencies: '@vitest/spy': 2.1.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: + msw: 2.15.0(@types/node@22.20.0)(typescript@5.9.3) vite: 5.4.21(@types/node@22.20.0)(terser@5.48.0) '@vitest/pretty-format@2.1.9': @@ -8663,6 +10760,8 @@ snapshots: acorn@8.17.0: {} + adm-zip@0.5.10: {} + agent-base@6.0.2: dependencies: debug: 4.4.3 @@ -8727,6 +10826,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} ansis@4.3.1: {} @@ -8765,16 +10866,68 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + array-back@6.2.3: {} + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + array-flatten@1.1.1: {} array-ify@1.0.0: {} + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + array-timsort@1.0.3: {} + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} autoprefixer@10.5.2(postcss@8.5.16): @@ -8786,8 +10939,16 @@ snapshots: postcss: 8.5.16 postcss-value-parser: 4.2.0 + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + aws-ssl-profiles@1.1.2: {} + axe-core@4.12.1: {} + + axobject-query@4.1.0: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -8796,9 +10957,9 @@ snapshots: baseline-browser-mapping@2.10.42: {} - bcrypt@5.1.1: + bcrypt@5.1.1(encoding@0.1.13): dependencies: - '@mapbox/node-pre-gyp': 1.0.11 + '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) node-addon-api: 5.1.0 transitivePeerDependencies: - encoding @@ -8996,6 +11157,8 @@ snapshots: clone@1.0.4: {} + clsx@2.1.1: {} + cluster-key-slot@1.1.1: {} code-block-writer@13.0.3: {} @@ -9010,6 +11173,10 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + command-line-args@6.0.2: dependencies: array-back: 6.2.3 @@ -9024,6 +11191,8 @@ snapshots: table-layout: 4.1.1 typical: 7.3.0 + commander@11.1.0: {} + commander@13.1.0: {} commander@2.20.3: {} @@ -9077,10 +11246,14 @@ snapshots: meow: 12.1.1 split2: 4.2.0 + convert-source-map@2.0.0: {} + cookie-signature@1.0.7: {} cookie@0.7.2: {} + cookie@1.1.1: {} + core-util-is@1.0.3: {} cors@2.8.5: @@ -9113,18 +11286,92 @@ snapshots: optionalDependencies: typescript: 5.9.3 + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 + 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: {} + cssesc@3.0.0: {} + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + damerau-levenshtein@1.0.8: {} + dargs@8.1.0: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + dateformat@4.6.3: {} dayjs@1.11.21: {} @@ -9137,6 +11384,10 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + + decimal.js@10.6.0: {} + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -9161,12 +11412,22 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delayed-stream@1.0.0: {} + delegates@1.0.0: {} denque@2.1.0: {} depd@2.0.0: {} + dequal@2.0.3: {} + destroy@1.2.0: {} detect-libc@2.1.2: {} @@ -9175,6 +11436,15 @@ snapshots: dlv@1.1.3: {} + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.29.7 + csstype: 3.2.3 + dot-prop@5.3.0: dependencies: is-obj: 2.0.0 @@ -9220,8 +11490,14 @@ snapshots: emoji-regex@9.2.2: {} + empathic@2.0.0: {} + encodeurl@2.0.0: {} + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -9231,6 +11507,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 + entities@6.0.1: {} + env-paths@2.2.1: {} environment@1.1.0: {} @@ -9239,6 +11517,70 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.4 + function.prototype.name: 1.2.0 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.22 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -9249,6 +11591,26 @@ snapshots: 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.4 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.4 + + es-to-primitive@1.3.4: + dependencies: + es-abstract-get: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + esbuild-register@3.6.0(esbuild@0.19.12): dependencies: debug: 4.4.3 @@ -9374,6 +11736,25 @@ snapshots: dependencies: eslint: 9.39.4(jiti@2.6.1) + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.12.1 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.4(jiti@2.6.1) + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -9502,6 +11883,8 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter3@4.0.7: {} + eventemitter3@5.0.4: {} events@3.3.0: {} @@ -9558,6 +11941,8 @@ snapshots: transitivePeerDependencies: - supports-color + exsolve@1.0.8: {} + extend@3.0.2: {} external-editor@3.1.0: @@ -9570,6 +11955,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-equals@5.4.1: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9584,8 +11971,18 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + fast-uri@3.1.3: {} + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -9653,6 +12050,10 @@ snapshots: flatted@3.4.2: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -9692,6 +12093,14 @@ snapshots: typescript: 5.7.2 webpack: 5.97.1 + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + forwarded-parse@2.1.2: {} forwarded@0.2.0: {} @@ -9716,11 +12125,28 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true function-bind@1.1.2: {} + function.prototype.name@1.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 + + functions-have-names@1.2.3: {} + gauge@3.0.2: dependencies: aproba: 2.1.0 @@ -9733,20 +12159,20 @@ snapshots: strip-ansi: 6.0.1 wide-align: 1.1.5 - gaxios@6.7.1: + gaxios@6.7.1(encoding@0.1.13): dependencies: extend: 3.0.2 https-proxy-agent: 7.0.6 is-stream: 2.0.1 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) uuid: 9.0.1 transitivePeerDependencies: - encoding - supports-color - gcp-metadata@6.1.1: + gcp-metadata@6.1.1(encoding@0.1.13): dependencies: - gaxios: 6.7.1 + gaxios: 6.7.1(encoding@0.1.13) google-logging-utils: 0.0.2 json-bigint: 1.0.0 transitivePeerDependencies: @@ -9757,6 +12183,10 @@ snapshots: dependencies: is-property: 1.0.2 + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} get-east-asian-width@1.6.0: {} @@ -9781,6 +12211,12 @@ snapshots: get-stream@8.0.1: {} + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -9827,12 +12263,21 @@ snapshots: globals@14.0.0: {} + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + google-logging-utils@0.0.2: {} gopd@1.2.0: {} graceful-fs@4.2.11: {} + graphql@16.14.2: {} + + has-bigints@1.1.0: {} + has-flag@4.0.0: {} has-own-prop@2.0.0: {} @@ -9841,18 +12286,35 @@ snapshots: dependencies: es-define-property: 1.0.1 + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + has-unicode@2.0.1: {} hasown@2.0.4: dependencies: function-bind: 1.1.2 + headers-polyfill@5.0.1: + dependencies: + '@types/set-cookie-parser': 2.4.10 + set-cookie-parser: 3.1.2 + help-me@5.0.0: {} hpagent@1.2.0: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-escaper@2.0.2: {} http-errors@2.0.1: @@ -9863,6 +12325,13 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -9885,6 +12354,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 @@ -9911,6 +12384,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -9958,6 +12433,14 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 6.2.0 + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + + internmap@2.0.3: {} + ioredis@5.11.1: dependencies: '@ioredis/commands': 1.10.0 @@ -9972,18 +12455,62 @@ snapshots: ipaddr.js@1.9.1: {} + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + is-core-module@2.16.2: dependencies: hasown: 2.0.4 + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + is-extglob@2.1.1: {} + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + is-fullwidth-code-point@3.0.0: {} is-fullwidth-code-point@4.0.0: {} @@ -9992,30 +12519,96 @@ snapshots: dependencies: get-east-asian-width: 1.6.0 + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 is-interactive@1.0.0: {} + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-node-process@1.2.0: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} is-obj@2.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-property@1.0.2: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + is-stream@2.0.1: {} is-stream@3.0.0: {} + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + is-text-path@2.0.0: dependencies: text-extensions: 2.4.0 + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 + is-unicode-supported@0.1.0: {} + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + isexe@2.0.0: {} + isomorphic-ws@5.0.0(ws@8.21.0): + dependencies: + ws: 8.21.0 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -10053,6 +12646,8 @@ snapshots: jiti@1.21.7: {} + jiti@2.4.2: {} + jiti@2.6.1: {} joycon@3.1.1: {} @@ -10063,6 +12658,36 @@ snapshots: dependencies: argparse: 2.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.6 + 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.24 + 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.21.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + json-bigint@1.0.0: dependencies: bignumber.js: 9.3.1 @@ -10106,6 +12731,13 @@ snapshots: ms: 2.1.3 semver: 7.8.5 + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -10123,6 +12755,12 @@ snapshots: dependencies: json-buffer: 3.0.1 + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -10211,6 +12849,8 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.2 + long-timeout@0.1.1: {} + long@5.3.2: {} loose-envify@1.4.0: @@ -10221,8 +12861,16 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + lru.min@1.1.4: {} + luxon@3.7.2: {} + + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -10284,6 +12932,8 @@ snapshots: mimic-response@3.1.0: {} + min-indent@1.0.1: {} + minimatch@10.2.5: dependencies: brace-expansion: 5.0.7 @@ -10311,6 +12961,8 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 + mitt@3.0.1: {} + mkdirp-classic@0.5.3: {} mkdirp@0.5.6: @@ -10325,6 +12977,31 @@ snapshots: ms@2.1.3: {} + msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 6.1.1(@types/node@22.20.0) + '@mswjs/interceptors': 0.41.9 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.14.2 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.2 + type-fest: 5.8.0 + until-async: 3.0.2 + yargs: 17.7.3 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + multer@2.0.2: dependencies: append-field: 1.0.0 @@ -10339,6 +13016,8 @@ snapshots: mute-stream@1.0.0: {} + mute-stream@3.0.0: {} + mysql2@3.22.6(@types/node@22.20.0): dependencies: '@types/node': 22.20.0 @@ -10385,7 +13064,7 @@ snapshots: neo4j-driver-core: 5.28.3 rxjs: 7.8.2 - next@14.2.35(@opentelemetry/api@1.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.35 '@swc/helpers': 0.5.5 @@ -10395,7 +13074,7 @@ snapshots: postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(react@18.3.1) + styled-jsx: 5.1.1(@babel/core@7.29.7)(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 14.2.33 '@next/swc-darwin-x64': 14.2.33 @@ -10407,6 +13086,7 @@ snapshots: '@next/swc-win32-ia32-msvc': 14.2.33 '@next/swc-win32-x64-msvc': 14.2.33 '@opentelemetry/api': 1.9.1 + '@playwright/test': 1.61.1 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -10423,12 +13103,20 @@ snapshots: dependencies: lodash: 4.18.1 - node-fetch@2.7.0: + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 node-releases@2.0.50: {} + node-schedule@2.1.1: + dependencies: + cron-parser: 4.9.0 + long-timeout: 0.1.1 + sorted-array-functions: 1.3.0 + nopt@5.0.0: dependencies: abbrev: 1.1.1 @@ -10446,12 +13134,44 @@ snapshots: gauge: 3.0.2 set-blocking: 2.0.0 + nuqs@1.20.0(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + dependencies: + mitt: 3.0.1 + next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + nwsapi@2.2.24: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} object-inspect@1.13.4: {} + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + on-exit-leak-free@2.1.2: {} on-finished@2.4.1: @@ -10497,6 +13217,14 @@ snapshots: os-tmpdir@1.0.2: {} + outvariant@1.4.3: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -10526,6 +13254,10 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} path-browserify@1.0.1: {} @@ -10551,6 +13283,8 @@ snapshots: path-to-regexp@3.3.0: {} + path-to-regexp@6.3.0: {} + path-type@4.0.0: {} pathe@1.1.2: {} @@ -10627,8 +13361,18 @@ snapshots: pirates@4.0.7: {} + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} + possible-typed-array-names@1.1.0: {} + postcss-import@15.1.0(postcss@8.5.16): dependencies: postcss: 8.5.16 @@ -10703,6 +13447,12 @@ snapshots: prettier@3.9.4: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + process-warning@5.0.0: {} process@0.11.10: {} @@ -10712,6 +13462,12 @@ snapshots: '@opentelemetry/api': 1.9.1 tdigest: 0.1.2 + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + protobufjs@7.6.5: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -10768,6 +13524,35 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-hook-form@7.81.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-is@18.3.1: {} + + react-refresh@0.17.0: {} + + react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + fast-equals: 5.4.1 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.7 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -10796,6 +13581,28 @@ snapshots: real-require@0.2.0: {} + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.18.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -10804,6 +13611,26 @@ snapshots: reflect-metadata@0.2.2: {} + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + repeat-string@1.6.1: {} require-directory@2.1.1: {} @@ -10841,6 +13668,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + rettime@0.11.11: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -10880,6 +13709,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.2 fsevents: 2.3.3 + rrweb-cssom@0.7.1: {} + + rrweb-cssom@0.8.0: {} + run-async@2.4.1: {} run-async@3.0.0: {} @@ -10896,12 +13729,35 @@ snapshots: dependencies: tslib: 2.8.1 + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -10912,6 +13768,13 @@ snapshots: ajv: 6.15.0 ajv-keywords: 3.5.2(ajv@6.15.0) + schema-utils@4.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + ajv-keywords: 5.1.0(ajv@8.20.0) + schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 @@ -10925,6 +13788,8 @@ snapshots: semver@6.3.1: {} + semver@7.6.3: {} + semver@7.8.5: {} send@0.19.2: @@ -10956,6 +13821,8 @@ snapshots: set-blocking@2.0.0: {} + set-cookie-parser@3.1.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -10965,6 +13832,19 @@ snapshots: gopd: 1.2.0 has-property-descriptors: 1.0.2 + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + setprototypeof@1.2.0: {} shebang-command@2.0.0: @@ -11031,6 +13911,8 @@ snapshots: dependencies: atomic-sleep: 1.0.0 + sorted-array-functions@1.3.0: {} + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -11056,8 +13938,15 @@ snapshots: std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + streamsearch@1.1.0: {} + strict-event-emitter@0.5.1: {} + string-argv@0.3.2: {} string-width@4.2.3: @@ -11078,6 +13967,36 @@ snapshots: get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.11: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -11094,6 +14013,10 @@ snapshots: strip-final-newline@3.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-json-comments@2.0.1: {} strip-json-comments@3.1.1: {} @@ -11102,10 +14025,12 @@ snapshots: dependencies: '@tokenizer/token': 0.3.0 - styled-jsx@5.1.1(react@18.3.1): + styled-jsx@5.1.1(@babel/core@7.29.7)(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 + optionalDependencies: + '@babel/core': 7.29.7 sucrase@3.35.1: dependencies: @@ -11129,11 +14054,17 @@ snapshots: symbol-observable@4.0.0: {} + symbol-tree@3.2.4: {} + table-layout@4.1.1: dependencies: array-back: 6.2.3 wordwrapjs: 5.1.1 + tagged-tag@1.0.0: {} + + tailwind-merge@2.6.1: {} + tailwindcss@3.4.19(tsx@4.23.0)(yaml@2.9.0): dependencies: '@alloc/quick-lru': 5.2.0 @@ -11162,6 +14093,8 @@ snapshots: - tsx - yaml + tapable@2.3.0: {} + tapable@2.3.3: {} tar-fs@2.1.5: @@ -11202,6 +14135,17 @@ snapshots: optionalDependencies: esbuild: 0.19.12 + terser-webpack-plugin@5.6.1(postcss@8.5.16)(webpack@5.97.1(postcss@8.5.16)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.48.0 + webpack: 5.97.1(postcss@8.5.16) + optionalDependencies: + postcss: 8.5.16 + optional: true + terser-webpack-plugin@5.6.1(webpack@5.97.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -11239,6 +14183,8 @@ snapshots: through@2.3.8: {} + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -11256,6 +14202,18 @@ snapshots: tinyspy@3.0.2: {} + tldts-core@6.1.86: {} + + tldts-core@7.4.8: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tldts@7.4.8: + dependencies: + tldts-core: 7.4.8 + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -11272,8 +14230,20 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.8 + tr46@0.0.3: {} + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} ts-api-utils@2.5.0(typescript@5.9.3): @@ -11318,11 +14288,48 @@ snapshots: type-fest@0.21.3: {} + type-fest@5.8.0: + dependencies: + tagged-tag: 1.0.0 + type-is@1.6.18: dependencies: media-typer: 0.3.0 mime-types: 2.1.35 + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + typedarray@0.0.6: {} typeorm@1.0.0(ioredis@5.11.1)(mysql2@3.22.6(@types/node@22.20.0)): @@ -11367,18 +14374,31 @@ snapshots: uint8array-extras@1.5.0: {} + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + undici-types@6.21.0: {} undici-types@7.18.2: {} undici@6.27.0: {} + undici@7.28.0: {} + unicorn-magic@0.1.0: {} universalify@2.0.1: {} unpipe@1.0.0: {} + until-async@3.0.2: {} + + upath@2.0.1: {} + update-browserslist-db@1.2.3(browserslist@4.28.5): dependencies: browserslist: 4.28.5 @@ -11389,6 +14409,16 @@ snapshots: dependencies: punycode: 2.3.1 + urql@4.2.2(@urql/core@5.2.0(graphql@16.14.2))(react@18.3.1): + dependencies: + '@urql/core': 5.2.0(graphql@16.14.2) + react: 18.3.1 + wonka: 6.3.6 + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} @@ -11399,6 +14429,23 @@ snapshots: vary@1.1.2: {} + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + vite-node@2.1.9(@types/node@22.20.0)(terser@5.48.0): dependencies: cac: 6.7.14 @@ -11427,10 +14474,10 @@ snapshots: fsevents: 2.3.3 terser: 5.48.0 - vitest@2.1.9(@types/node@22.20.0)(terser@5.48.0): + vitest@2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0): dependencies: '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.20.0)(terser@5.48.0)) + '@vitest/mocker': 2.1.9(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(vite@5.4.21(@types/node@22.20.0)(terser@5.48.0)) '@vitest/pretty-format': 2.1.9 '@vitest/runner': 2.1.9 '@vitest/snapshot': 2.1.9 @@ -11451,6 +14498,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.20.0 + jsdom: 25.0.1 transitivePeerDependencies: - less - lightningcss @@ -11462,6 +14510,10 @@ snapshots: - supports-color - terser + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + watchpack@2.5.2: dependencies: graceful-fs: 4.2.11 @@ -11472,6 +14524,8 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} + webpack-node-externals@3.0.0: {} webpack-sources@3.5.1: {} @@ -11554,11 +14608,103 @@ snapshots: - postcss - uglify-js + webpack@5.97.1(postcss@8.5.16): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.9 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.17.0 + browserslist: 4.28.5 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.24.2 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.2 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.3.3 + terser-webpack-plugin: 5.6.1(postcss@8.5.16)(webpack@5.97.1(postcss@8.5.16)) + watchpack: 2.5.2 + webpack-sources: 3.5.1 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + optional: true + + 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 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -11572,6 +14718,8 @@ snapshots: dependencies: string-width: 4.2.3 + wonka@6.3.6: {} + word-wrap@1.2.5: {} wordwrapjs@5.1.1: {} @@ -11602,10 +14750,18 @@ snapshots: wrappy@1.0.2: {} + ws@8.21.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xtend@4.0.2: {} y18n@5.0.8: {} + yallist@3.1.1: {} + yallist@4.0.0: {} yaml@2.9.0: {} @@ -11638,3 +14794,10 @@ snapshots: yocto-queue@1.2.2: {} zod@3.25.76: {} + + zustand@4.5.7(@types/react@18.3.31)(react@18.3.1): + dependencies: + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + react: 18.3.1