commit 24871e213a7724c33f931c597023059d4b08b871 Author: kurihada Date: Wed Mar 18 11:29:54 2026 +0800 chore(repo): reinitialize repository diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6d27f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +.DS_Store +.cache/ +.runtime/ +.playwright-cli/ + +.claude/settings.local.json +config/codex/ + +inbox-worktrees/ + +**/node_modules/ +**/dist/ +**/test-results/ +**/playwright-report/ +**/coverage/ +**/.next/ +**/*.tsbuildinfo + +apps/ + +npm-debug.log* diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..cab7dc9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,135 @@ +# AGENTS.md + +本文档为在此仓库中工作的 AI 编码代理(Codex、Cursor、Copilot 等)提供指导。 + +## 第一性原理 + +请使用第一性原理思考。你不能总是假设我非常清楚自己想要什么和该怎么得到。请保持审慎,从原始需求和问题出发,如果动机和目标不清晰,停下来和我讨论。 + + +## 方案规范 + +当需要你给出修改或重构方案时必须符合以下规范: + +- 不允许给出兼容性或补丁性的方案 +- 不允许过度设计,保持最短路径实现且不能违反第一条要求 +- 不允许自行给出我提供的需求以外的方案,例如一些兜底和降级方案,这可能导致业务逻辑偏移问题 +- 必须确保方案的逻辑正确,必须经过全链路的逻辑验证 + +## 这是什么 + +这是一个多代理 AI 编排系统。 + +## 关键命令 + +### Inbox CLI(核心工具) + +```bash +cd inbox && go build -o /tmp/inbox-host ./cmd/inbox +export INBOX_WORKSPACE=/Users/xd/project/ai-workflow-v2 + +/tmp/inbox-host server --workspaces-dir /Users/xd/project/ai-workflow-v2/inbox-worktrees --port 3000 +/tmp/inbox-host api GET /api/v2/projects +/tmp/inbox-host api POST /api/v2/topics --data '{"workspace_id":"ws_1","title":"Signup Flow","space":"workflow","status":"execution"}' +``` + +### Dashboard(React UI) + +```bash +cd dashboard && npm install && npm run dev # Vite 开发服务器 +cd dashboard && npm run build # 生产构建 → dist/ +``` + +开发模式下,Dashboard 会将 `/api` 代理到 `http://localhost:3000`。 + +### Apps + +```bash +# phonesite(Next.js 静态站点) +cd apps/phonesite && npm install && npm run dev +cd apps/phonesite && npm test # Vitest +cd apps/phonesite && npx playwright test # E2E + +# redbook(全栈:Go + Next.js) +cd apps/redbook/backend && go run ./cmd/server # :8080 +cd apps/redbook/backend && go test ./... +cd apps/redbook/frontend && npm install && npm run dev +cd apps/redbook/frontend && npm test +``` + +## 架构 + +### 当前流程 + +当前激活的流程以 HTTP 为中心: + +1. `inbox server` 启动 Inbox API。 +2. `inbox api ...` 或 Dashboard 调用 `/api/v2/*`。 +3. HTTP 层通过 `internal/store/sqlite` 直接写入 SQLite。 +4. 运行时角色解析从数据库读取,并快照到 workflow run 中。 + +### 运行时目录结构 + +``` +/.runtime/inbox.db # Inbox V2 SQLite 存储 +``` + +### Inbox CLI(Go,`inbox/`) + +单个二进制文件,提供两个命令: +- `server`:启动 HTTP API +- `api`:通用的、面向 AI 的 HTTP 包装器 + +关键模块区域: +- `internal/base/`:通用基础原语 +- `internal/domain/`:稳定的业务实体与规则 +- `internal/app/`:运行时配置与 workflow-run 服务 +- `internal/httpapi/`:`/api/v2/*` 处理器 +- `internal/store/sqlite/`:SQLite schema 与仓储实现 + +### 角色系统 + +角色是数据库驱动的运行时配置,而不是文件驱动的事实来源。 + +- `leader` 负责用户对话、任务拆解、链路编排,以及宿主机上的最终执行决策 +- `worker` 在隔离的 worktree/container 中执行分配的链路任务,并向 `leader` 回传证据 +- `user` 是仅供人类使用的运行时角色,永远不会由 AI 运行时自动执行 + +### Dashboard(`dashboard/`) + +基于 React 19 + TypeScript + Vite + Tailwind + Framer Motion。页面包括:Timeline、Topics、Roles、Executions、Merges。通过轮询 inbox web API 获取更新。 +## 工程规则 + +### 最高优先级:修复根因,而不是增加回退逻辑 + +- 默认直接修复根因。除非用户明确要求这种权衡,否则不要添加 fallback 逻辑、ignore 规则、兼容性分支、防御性特判或“以防万一”的条件分支。 +- 如果真正的问题是糟糕的历史状态、陈旧的运行时产物、脏 worktree 或畸形数据,应通过迁移、清理步骤、修复脚本或定向数据修正直接修复状态,而不是让应用逻辑静默容忍这些问题。 +- 不要把运维残留吸收到产品逻辑中。运行时文件、缓存文件、生成产物、临时目录以及本地 OS 噪音,都应在源头被隔离、迁移或显式清理。 +- 在考虑“增强韧性”的补丁之前,先问自己:“为什么会出现这个坏状态?”以及“能不能直接移除这个坏状态的来源?”优先做源头治理。 +- 如果 fallback 看起来不可避免,先停下来,把这个权衡显式告知用户,再决定是否实现。不要为了短期方便,悄悄加入长期分支逻辑。 +- 优先选择一条干净、易解释的路径,而不是堆叠逻辑去保留旧错误。 + +### 不要添加遗留兼容层 + +- 不要为旧数据、旧字段值、旧路由、旧目录布局、旧分支命名或旧运行时行为添加兼容代码。 +- 不要保留双路径逻辑,例如 `new flow || old flow`、fallback-to-legacy 分支、针对历史数据的静默运行时协调,或把 cleanup-by-ignore 规则塞进核心逻辑里。 +- 如果旧数据或运行时残留有问题,应通过显式迁移、修复脚本、定向 DB 更新或一次性清理直接修正。不要把问题藏进应用逻辑里。 +- 如果旧实现正在被替换,就删除旧路径,而不是长期同时支持两套。 +- 不要留下没有移除计划的“临时” fallback 分支。在这个仓库里,临时兼容逻辑通常就是错误方向。 + +### 清晰的模块归属 + +- 新行为必须放进职责单一、边界明确的模块中。 +- 不要把一个特性零散地分布在 handler、service、helper 和 infra 文件中,而没有清晰的模块边界。 +- HTTP handler 应保持轻量:解析请求、调用 application service、写回响应。 +- Application service 应负责编排和业务流程。 +- git、container、filesystem、process execution 等基础设施细节应封装在职责明确、接口收敛的专用模块之后。 +- 做重构时,优先先提取模块,再把逻辑迁移进去,而不是让部分逻辑继续散落在旧文件中。 + +### 处理破坏性变更的首选方式 + +- 优先干净替换,而不是渐进兼容。 +- 优先 schema/data migration,而不是运行时分支。 +- 优先显式清理,而不是用 ignore 掩盖问题。 +- 优先显式失败,而不是隐藏 fallback 行为。 +- 新模块就位后,优先删除废弃代码。 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9d151f6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,94 @@ +# CLAUDE.md + +This file provides guidance to Claude Code when working with this repository. + +## What This Is + +Multi-agent AI orchestration system. The active V2 model is leader/worker based: the host-side `leader` talks to the user and orchestrates work, while per-chain `worker` runtimes execute tasks in isolated worktrees and containers. + +## Key Commands + +### Inbox CLI + +```bash +cd inbox && go build -o /tmp/inbox-host ./cmd/inbox +export INBOX_WORKSPACE=/Users/xd/project/ai-workflow-v2 + +/tmp/inbox-host server --workspaces-dir /Users/xd/project/ai-workflow-v2/inbox-worktrees --port 3000 +/tmp/inbox-host api GET /api/v2/projects +/tmp/inbox-host api POST /api/v2/topics --data '{"workspace_id":"ws_1","title":"Signup Flow","space":"workflow","status":"execution"}' +``` + +### Dashboard + +```bash +cd dashboard && npm install && npm run dev +cd dashboard && npm run build +``` + +Dashboard dev mode proxies `/api` to `http://localhost:3000`. + +### Container Launch + +```bash +./inbox/launch.sh [workspace-slug] +# Example: ./inbox/launch.sh apps/phonesite phonesite-main +``` + +The host Inbox API must already be running. `launch.sh` registers the `project` / `workspace` in SQLite, creates a git worktree, launches the Podman container, and starts `inbox agent` in API mode. + +## Runtime Model + +Runtime ownership is split as: + +- `project`: long-lived source project such as `apps/blog` +- `workspace`: per-run git worktree / container instance such as `inbox-worktrees/blog-main` + +Persistent runtime state is stored in SQLite, not in worktree files: + +```text +/.runtime/inbox.db +/inbox-worktrees// +/tmp/inbox-codex-* +``` + +Container agents must use: + +- `INBOX_API_URL` +- `INBOX_WORKSPACE_NAME` + +This keeps worktrees code-only and prevents `.runtime` or `.inbox-meta.json` from being written into them. + +## Message Flow + +Messages are authored as Markdown with YAML front matter, but persisted in SQLite: + +1. `inbox send` stores the message body and metadata in SQLite and marks mailbox state +2. `inbox agent --role ` polls inbox state from SQLite or, in container mode, through the host Inbox API +3. Codex output, sessions, chain/task state, and dispatch live logs are written back to SQLite +4. Processed messages are archived by mailbox-state update, not by moving files on disk + +## Roles + +Roles are DB-backed runtime configuration, not file-backed prompts. + +- `leader` — host-side orchestrator for user dialogue, chain/task decomposition, and execution control +- `worker` — per-chain executor inside isolated worktrees/containers +- `user` — human-only participant; never auto-executed by the AI runtime + +Legacy fixed roles such as `product`, `backend`, `frontend`, `reviewer`, and discovery roles are not part of the active V2 execution model. + +## Dashboard + +`dashboard/` is a React 19 + TypeScript + Vite app that polls the Inbox Web API for: + +- messages +- role status +- executions +- merge requests +- workflow board state +- leader/worker thread activity + +## Skills + +Reusable skill definitions live in `config/codex/skills/`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9889800 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +[中文版](README.zh-CN.md) + +# ai-workflow-v2 + +This repository is organized as a small set of independent modules instead of one mixed runtime tree. + +## Modules + +| Module | Path | Purpose | +| --- | --- | --- | +| Inbox host | `inbox/` | Go control plane: CLI, HTTP API, runtime storage, agent dispatch | +| Dashboard | `dashboard/` | React operations UI for the inbox host | +| Blog backend | `apps/blog/backend/` | Sample Express API | +| Blog frontend | `apps/blog/frontend/` | Sample React client | +| Blog E2E | `apps/blog/e2e/` | Playwright black-box tests for the blog app | + +Module boundaries are documented in [docs/modules.md](docs/modules.md). + +## Repository Rules + +- Modules are developed and tested independently. +- Cross-module communication should happen through HTTP or CLI boundaries, not direct source imports. +- Runtime data, local databases, worktrees, caches, and build artifacts are generated files and are ignored by default. + +## Quick Start + +Use the root `Makefile` as the stable entrypoint: + +```bash +make help +make build-inbox +make test-inbox +make test-dashboard +make test-blog-backend +make test-blog-frontend +make test-blog-e2e +``` + +## Independent Module Usage + +### Inbox host + +```bash +cd inbox +go build ./cmd/inbox +go test ./... +``` + +More detail: [inbox/README.md](inbox/README.md) + +### Dashboard + +```bash +cd dashboard +npm ci +npm run dev +npm test +``` + +More detail: [dashboard/README.md](dashboard/README.md) + +### Blog app + +```bash +cd apps/blog/backend && npm ci && npm test +cd apps/blog/frontend && npm ci && npm test +cd apps/blog/e2e && npm ci && npm test +``` + +More detail: [apps/blog/README.md](apps/blog/README.md) + +## Testing Matrix + +- Fast checks: `make test-all` +- Browser/E2E checks: `make test-all-e2e` +- Full dependency install for JS modules: `make deps-all` + +## Goal + +Keep each module independently runnable, independently testable, and clear about what it owns. diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..4008a71 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,80 @@ +[English](README.md) + +# ai-workflow-v2 + +这个仓库现在按“独立模块”来组织,而不是把运行时、缓存和多套旧代码混在一起。 + +## 模块划分 + +| 模块 | 路径 | 作用 | +| --- | --- | --- | +| Inbox host | `inbox/` | Go 控制面:CLI、HTTP API、运行时存储、Agent 调度 | +| Dashboard | `dashboard/` | 面向 Inbox host 的 React 运维界面 | +| Blog backend | `apps/blog/backend/` | 示例 Express API | +| Blog frontend | `apps/blog/frontend/` | 示例 React 客户端 | +| Blog E2E | `apps/blog/e2e/` | 面向 blog 应用的 Playwright 黑盒测试 | + +模块边界说明见 [docs/modules.md](docs/modules.md)。 + +## 仓库规则 + +- 每个模块都应该可以独立开发、独立测试。 +- 跨模块只通过 HTTP 或 CLI 边界通信,不直接相互引用源码。 +- 运行时数据、本地数据库、worktree、缓存和构建产物默认都不进仓库。 + +## 快速开始 + +统一从根目录 `Makefile` 进入: + +```bash +make help +make build-inbox +make test-inbox +make test-dashboard +make test-blog-backend +make test-blog-frontend +make test-blog-e2e +``` + +## 各模块独立使用 + +### Inbox host + +```bash +cd inbox +go build ./cmd/inbox +go test ./... +``` + +更多说明见 [inbox/README.md](inbox/README.md)。 + +### Dashboard + +```bash +cd dashboard +npm ci +npm run dev +npm test +``` + +更多说明见 [dashboard/README.md](dashboard/README.md)。 + +### Blog 应用 + +```bash +cd apps/blog/backend && npm ci && npm test +cd apps/blog/frontend && npm ci && npm test +cd apps/blog/e2e && npm ci && npm test +``` + +更多说明见 [apps/blog/README.md](apps/blog/README.md)。 + +## 测试入口 + +- 快速测试:`make test-all` +- 浏览器 / E2E:`make test-all-e2e` +- 一次性安装全部 JS 依赖:`make deps-all` + +## 当前目标 + +让每个模块边界清晰、职责单一,并且默认就是“方便使用、方便测试”的状态。 diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 0000000..4645062 --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,34 @@ +# Dashboard Module + +`dashboard/` is the operations UI for the inbox host. + +## Owns + +- Page routing and layout +- API client for `/api/*` +- Presentation components and hooks +- Dashboard-specific Playwright and Vitest suites + +## Dependency Rule + +The dashboard may call inbox HTTP APIs, but it must not import code from `inbox/`. + +## Local Commands + +```bash +npm ci +npm run dev +npm run build +npm test +npm run test:e2e +``` + +From the repository root: + +```bash +make deps-dashboard +make dev-dashboard +make build-dashboard +make test-dashboard +make test-dashboard-e2e +``` diff --git a/dashboard/eslint.config.js b/dashboard/eslint.config.js new file mode 100644 index 0000000..f5f9dcc --- /dev/null +++ b/dashboard/eslint.config.js @@ -0,0 +1,38 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["dist/**", "node_modules/**", "coverage/**", "test-results/**"], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["src/**/*.{ts,tsx}"], + languageOptions: { + globals: { + ...globals.browser, + }, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": "off", + "@typescript-eslint/consistent-type-imports": [ + "error", + { prefer: "type-imports", disallowTypeAnnotations: false }, + ], + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + "@typescript-eslint/no-explicit-any": "error", + }, + }, +); diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 0000000..930d446 --- /dev/null +++ b/dashboard/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + Delivery Console + + +
+ + + diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json new file mode 100644 index 0000000..8fea3eb --- /dev/null +++ b/dashboard/package-lock.json @@ -0,0 +1,6365 @@ +{ + "name": "inbox-dashboard", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "inbox-dashboard", + "version": "0.0.1", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-popover": "^1.1.15", + "@xyflow/react": "^12.10.1", + "framer-motion": "^11.15.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router": "^7.13.1" + }, + "devDependencies": { + "@eslint/js": "^9.23.0", + "@playwright/test": "^1.52.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.20", + "eslint": "^9.23.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "jsdom": "^26.0.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.0", + "typescript": "^5.6.0", + "typescript-eslint": "^8.27.0", + "vite": "^6.0.0", + "vitest": "^3.0.8" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "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 + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", + "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/type-utils": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", + "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", + "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.1", + "@typescript-eslint/types": "^8.57.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", + "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", + "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", + "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", + "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", + "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.1", + "@typescript-eslint/tsconfig-utils": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", + "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", + "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xyflow/react": { + "version": "12.10.1", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.1.tgz", + "integrity": "sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.75", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.75", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.75.tgz", + "integrity": "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "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.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", + "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.1", + "@typescript-eslint/parser": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "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 + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "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 + } + } + } + } +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 0000000..256174b --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,49 @@ +{ + "name": "inbox-dashboard", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "typecheck": "tsc -b --pretty false", + "lint": "eslint src --ext .ts,.tsx", + "lint:fix": "eslint src --ext .ts,.tsx --fix", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test", + "test:e2e:headed": "playwright test --headed" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-popover": "^1.1.15", + "@xyflow/react": "^12.10.1", + "framer-motion": "^11.15.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router": "^7.13.1" + }, + "devDependencies": { + "@eslint/js": "^9.23.0", + "@playwright/test": "^1.52.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.20", + "eslint": "^9.23.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "jsdom": "^26.0.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.0", + "typescript": "^5.6.0", + "typescript-eslint": "^8.27.0", + "vite": "^6.0.0", + "vitest": "^3.0.8" + } +} diff --git a/dashboard/playwright.config.ts b/dashboard/playwright.config.ts new file mode 100644 index 0000000..5cac3a7 --- /dev/null +++ b/dashboard/playwright.config.ts @@ -0,0 +1,23 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e", + fullyParallel: true, + retries: 0, + reporter: "list", + use: { + baseURL: "http://127.0.0.1:4173", + trace: "retain-on-failure", + }, + webServer: { + command: "npm run dev -- --host 127.0.0.1 --port 4173", + url: "http://127.0.0.1:4173", + reuseExistingServer: !process.env.CI, + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}); diff --git a/dashboard/postcss.config.js b/dashboard/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/dashboard/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx new file mode 100644 index 0000000..4eaa17f --- /dev/null +++ b/dashboard/src/App.tsx @@ -0,0 +1,86 @@ +import type { ElementType } from "react"; +import { Routes, Route, Navigate, useLocation } from "react-router"; +import DashboardLayout from "./layouts/DashboardLayout"; +import WorkflowPrototype from "./components/WorkflowPrototype"; +import RoleStatus from "./components/RoleStatus"; +import SkillCatalogManager from "./components/SkillCatalogManager"; +import ExecutionTable from "./components/ExecutionTable"; +import { + dashboardSections, + getWorkspaceFromPathname, + isWorkspaceScopedSection, +} from "./routes"; + +const workspaceSectionComponents = { + workflow: WorkflowPrototype, + roles: RoleStatus, + executions: ExecutionTable, +} as const; + +type WorkspaceSectionId = keyof typeof workspaceSectionComponents; +type NonWorkflowWorkspaceSectionId = Exclude; + +function getWorkspaceSectionComponent( + section: WorkspaceSectionId, +) { + return workspaceSectionComponents[section]; +} + +function isNonWorkflowWorkspaceSection( + section: (typeof dashboardSections)[number], +): section is Extract<(typeof dashboardSections)[number], { id: NonWorkflowWorkspaceSectionId }> { + return section.id !== "workflow" && section.id !== "skills" && isWorkspaceScopedSection(section.id); +} + +function WorkspaceRoute({ + component: Component, +}: { + component: ElementType<{ workspace: string }>; +}) { + const location = useLocation(); + return ; +} + +export default function App() { + return ( + + } /> + } /> + }> + {dashboardSections.map((section) => ( + + : + } + /> + ))} + } + /> + } + /> + } + /> + {dashboardSections + .filter(isNonWorkflowWorkspaceSection) + .map((section) => ( + } + /> + ))} + } /> + + + ); +} diff --git a/dashboard/src/api/client.test.ts b/dashboard/src/api/client.test.ts new file mode 100644 index 0000000..ede2276 --- /dev/null +++ b/dashboard/src/api/client.test.ts @@ -0,0 +1,563 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + answerHumanTask, + confirmTopicPlan, + fetchRoleSkills, + fetchRoles, + fetchWorkflowBoard, + fetchWorkspaces, + sendMessage, + stopTopic, +} from "./client"; + +const originalFetch = globalThis.fetch; + +const rawWorkspace = { + id: "workspace-1", + project_id: "project-1", + slug: "alpha", + name: "alpha", + root_path: "/tmp/alpha", + base_branch: "", + worktree_branch: "", + runtime_backend: "container", + status: "active", + provision_state: "ready", + provision_error: "", + last_provisioned_at: "2026-03-13T10:00:00Z", + container_state: "running", + created_at: "", + updated_at: "", +}; + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +afterEach(() => { + globalThis.fetch = originalFetch; + vi.restoreAllMocks(); +}); + +describe("dashboard api client", () => { + it("parses successful workspace responses", async () => { + globalThis.fetch = vi.fn() + .mockResolvedValueOnce( + jsonResponse({ + projects: [ + { + id: "project-1", + slug: "demo", + name: "Demo", + root_path: "/tmp/demo", + }, + ], + }), + ) + .mockResolvedValueOnce( + jsonResponse({ + workspaces: [ + { + id: "workspace-1", + project_id: "project-1", + slug: "alpha", + name: "alpha", + root_path: "/tmp/alpha", + base_branch: "", + worktree_branch: "", + runtime_backend: "container", + status: "", + provision_state: "ready", + provision_error: "", + last_provisioned_at: "2026-03-13T10:00:00Z", + container_state: "running", + created_at: "", + updated_at: "", + }, + ], + }), + ) as typeof fetch; + + await expect(fetchWorkspaces()).resolves.toEqual([ + { + id: "workspace-1", + name: "alpha", + slug: "alpha", + project_slug: "demo", + project_dir: "/tmp/demo", + path: "/tmp/alpha", + base_branch: "", + worktree_branch: "", + runtime_backend: "container", + container_name: "", + status: "", + provision_state: "ready", + provision_error: "", + last_provisioned_at: "2026-03-13T10:00:00Z", + container_state: "running", + created_at: "", + updated_at: "", + }, + ]); + }); + + it("maps abort errors to a timeout message", async () => { + globalThis.fetch = vi.fn(async () => { + throw new DOMException("Aborted", "AbortError"); + }) as typeof fetch; + + await expect(fetchWorkspaces()).rejects.toThrow( + "Request timed out. Check the local API and try again.", + ); + }); + + it("reads structured server errors from json responses", async () => { + globalThis.fetch = vi.fn(async () => + new Response(JSON.stringify({ message: "Bad topic payload" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }), + ) as typeof fetch; + + await expect( + sendMessage({ + workspace: "alpha", + to: "leader", + topic: "bad", + body: "broken", + }), + ).rejects.toThrow("Bad topic payload"); + }); + + it("surfaces unreadable JSON responses", async () => { + globalThis.fetch = vi.fn(async () => + new Response("{", { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ) as typeof fetch; + + await expect(fetchWorkspaces()).rejects.toThrow("Server returned an unreadable response."); + }); + + it("falls back to a 404 message when the server returns no readable body", async () => { + globalThis.fetch = vi.fn(async () => + new Response("", { + status: 404, + headers: { "Content-Type": "application/json" }, + }), + ) as typeof fetch; + + await expect(fetchWorkspaces()).rejects.toThrow("Resource not found."); + }); + + it("maps rate limits to a retryable message", async () => { + globalThis.fetch = vi.fn(async () => + new Response("", { + status: 429, + headers: { "Content-Type": "application/json" }, + }), + ) as typeof fetch; + + await expect(fetchWorkspaces()).rejects.toThrow( + "Too many requests. Try again in a moment.", + ); + }); + + it("loads global roles without adding a workspace query", async () => { + globalThis.fetch = vi.fn() + .mockResolvedValueOnce( + jsonResponse({ + roles: [ + { + name: "leader", + description: "Owns scope.", + sort_order: 100, + pending: 0, + session: null, + }, + ], + }), + ) as typeof fetch; + + await expect(fetchRoles("")).resolves.toEqual([ + { + name: "leader", + description: "Owns scope.", + sort_order: 100, + pending: 0, + session: null, + }, + ]); + + expect(globalThis.fetch).toHaveBeenCalledWith( + "/api/v2/dashboard/roles", + expect.objectContaining({ + signal: expect.any(AbortSignal), + }), + ); + }); + + it("normalizes nullable workflow board arrays", async () => { + globalThis.fetch = vi.fn().mockResolvedValueOnce( + jsonResponse({ + topics: [ + { + name: "launch-dashboard", + message_count: 2, + latest_stage: "execution", + latest_time: "2026-03-09T10:10:00Z", + running_roles: null, + waiting_roles: null, + }, + ], + active_topic: "launch-dashboard", + board: { + topic: { + name: "launch-dashboard", + latest_stage: "execution", + message_count: 2, + created_at: "2026-03-09T10:00:00Z", + updated_at: "2026-03-09T10:10:00Z", + status: "execution", + }, + summary: { + running_count: 0, + waiting_count: 0, + active_roles: null, + last_event_at: "2026-03-09T10:10:00Z", + }, + agents: null, + links: null, + events: null, + pending_human_tasks: null, + }, + }), + ) as typeof fetch; + + await expect(fetchWorkflowBoard("alpha")).resolves.toEqual({ + topics: [ + { + name: "launch-dashboard", + message_count: 2, + latest_stage: "execution", + latest_time: "2026-03-09T10:10:00Z", + status: "", + running_roles: [], + waiting_roles: [], + }, + ], + active_topic: "launch-dashboard", + board: { + topic: { + name: "launch-dashboard", + latest_stage: "execution", + message_count: 2, + created_at: "2026-03-09T10:00:00Z", + updated_at: "2026-03-09T10:10:00Z", + status: "execution", + }, + plan: null, + summary: { + running_count: 0, + waiting_count: 0, + active_roles: [], + last_event_at: "2026-03-09T10:10:00Z", + }, + agents: [], + lanes: [], + tasks: [], + links: [], + events: [], + pending_human_tasks: [], + }, + }); + }); + + it("reads role skills through explicit workspace context", async () => { + globalThis.fetch = vi.fn() + .mockResolvedValueOnce(jsonResponse({ workspaces: [rawWorkspace] })) + .mockResolvedValueOnce( + jsonResponse({ + role: { + name: "worker", + title: "Worker", + description: "Handles execution.", + is_enabled: true, + is_builtin: true, + sort_order: 200, + }, + prompts: [], + config: {}, + bindings: [ + { + workspace_id: "workspace-1", + skill_id: "skill_1", + is_enabled: true, + sort_order: 100, + config: { category: "adaptation" }, + }, + ], + }), + ) + .mockResolvedValueOnce( + jsonResponse({ + skills: [ + { + id: "skill_1", + skill_key: "adapt", + name: "adapt", + description: "Adapt designs.", + source_type: "other", + content_markdown: "---\\nname: adapt\\n---", + status: "active", + }, + ], + }), + ) as typeof fetch; + + await expect(fetchRoleSkills("alpha", "worker")).resolves.toEqual([ + { + id: "adapt", + name: "adapt", + description: "Adapt designs.", + category: "adaptation", + enabled: true, + missing: false, + }, + ]); + }); + + it("reads role skills without workspace context from global config", async () => { + globalThis.fetch = vi.fn() + .mockResolvedValueOnce( + jsonResponse({ + role: { + name: "worker", + title: "Worker", + description: "Handles execution.", + is_enabled: true, + is_builtin: true, + sort_order: 200, + }, + prompts: [], + config: {}, + bindings: [], + }), + ) + .mockResolvedValueOnce( + jsonResponse({ + skills: [ + { + id: "skill_1", + skill_key: "adapt", + name: "adapt", + description: "Adapt designs.", + source_type: "adaptation", + content_markdown: "---\\nname: adapt\\n---", + status: "active", + }, + ], + }), + ) as typeof fetch; + + await expect(fetchRoleSkills("", "worker")).resolves.toEqual([ + { + id: "adapt", + name: "adapt", + description: "Adapt designs.", + category: "adaptation", + enabled: false, + missing: false, + }, + ]); + }); + + it("returns the created message id from the send response", async () => { + globalThis.fetch = vi.fn() + .mockResolvedValueOnce( + jsonResponse({ + workspaces: [ + { + id: "workspace-1", + project_id: "project-1", + slug: "alpha", + name: "alpha", + root_path: "/tmp/alpha", + base_branch: "main", + worktree_branch: "worktree/alpha", + runtime_backend: "container", + status: "active", + provision_state: "ready", + provision_error: "", + last_provisioned_at: "", + container_state: "missing", + created_at: "", + updated_at: "", + }, + ], + }), + ) + .mockResolvedValueOnce(jsonResponse({ topics: [] })) + .mockResolvedValueOnce( + jsonResponse({ + id: "topic-1", + workspace_id: "workspace-1", + slug: "topic", + title: "topic", + space: "workflow", + status: "plan", + summary: "", + created_at: "2026-03-10T09:15:00Z", + updated_at: "2026-03-10T09:15:00Z", + }, 201), + ) + .mockResolvedValueOnce( + jsonResponse({ + status: "delivered", + id: "20260310T092000Z-topic.md", + }, 201), + ) as typeof fetch; + + await expect( + sendMessage({ + workspace: "alpha", + to: "leader", + topic: "topic", + body: "Need scope.", + stage: "plan", + type: "chat", + }), + ).resolves.toEqual({ + status: "delivered", + file: "20260310T092000Z-topic.md", + }); + + expect(globalThis.fetch).toHaveBeenLastCalledWith( + "/api/v2/topics/topic-1/messages", + expect.objectContaining({ + method: "POST", + body: JSON.stringify({ + workspace_id: "workspace-1", + from_role_name: "user", + to_expr: "leader", + type: "chat", + stage: "plan", + reply_to_message_id: undefined, + body_markdown: "Need scope.", + }), + }), + ); + }); + + it("posts human task answers to the dedicated host-side endpoint", async () => { + globalThis.fetch = vi.fn(async () => jsonResponse({ status: "ok" })) as typeof fetch; + + await expect( + answerHumanTask("human-task-1", "Clarify the delivery boundary."), + ).resolves.toBeUndefined(); + + expect(globalThis.fetch).toHaveBeenCalledWith( + "/api/v2/human-tasks/human-task-1/answer", + expect.objectContaining({ + method: "POST", + body: JSON.stringify({ + body_markdown: "Clarify the delivery boundary.", + }), + }), + ); + }); + + it("stops a topic through the topic stop endpoint", async () => { + globalThis.fetch = vi.fn() + .mockResolvedValueOnce( + jsonResponse({ + workspaces: [rawWorkspace], + }), + ) + .mockResolvedValueOnce( + jsonResponse({ + topics: [{ + id: "topic-1", + workspace_id: "workspace-1", + slug: "topic", + title: "topic", + space: "workflow", + status: "execution", + created_at: "2026-03-10T09:15:00Z", + updated_at: "2026-03-10T09:15:00Z", + }], + }), + ) + .mockResolvedValueOnce( + jsonResponse({ + id: "topic-1", + workspace_id: "workspace-1", + slug: "topic", + title: "topic", + space: "workflow", + status: "cancelled", + created_at: "2026-03-10T09:15:00Z", + updated_at: "2026-03-10T09:20:00Z", + closed_at: "2026-03-10T09:20:00Z", + }, 202), + ) as typeof fetch; + + await expect(stopTopic("alpha", "topic")).resolves.toBeUndefined(); + + expect(globalThis.fetch).toHaveBeenLastCalledWith( + "/api/v2/topics/topic-1/stop", + expect.objectContaining({ + method: "POST", + }), + ); + }); + + it("confirms a topic plan through the topic confirm endpoint", async () => { + globalThis.fetch = vi.fn() + .mockResolvedValueOnce( + jsonResponse({ + workspaces: [rawWorkspace], + }), + ) + .mockResolvedValueOnce( + jsonResponse({ + topics: [{ + id: "topic-1", + workspace_id: "workspace-1", + slug: "topic", + title: "topic", + space: "workflow", + status: "awaiting_confirmation", + created_at: "2026-03-10T09:15:00Z", + updated_at: "2026-03-10T09:15:00Z", + }], + }), + ) + .mockResolvedValueOnce( + jsonResponse({ + id: "topic-1", + workspace_id: "workspace-1", + slug: "topic", + title: "topic", + space: "workflow", + status: "execution", + created_at: "2026-03-10T09:15:00Z", + updated_at: "2026-03-10T09:20:00Z", + }, 202), + ) as typeof fetch; + + await expect(confirmTopicPlan("alpha", "topic")).resolves.toBeUndefined(); + + expect(globalThis.fetch).toHaveBeenLastCalledWith( + "/api/v2/topics/topic-1/confirm", + expect.objectContaining({ + method: "POST", + }), + ); + }); + +}); diff --git a/dashboard/src/api/client.ts b/dashboard/src/api/client.ts new file mode 100644 index 0000000..60a13db --- /dev/null +++ b/dashboard/src/api/client.ts @@ -0,0 +1,222 @@ +import type { + DispatchLog, + RoleInfo, + WorkflowBoardResponse, + Workspace, +} from "../types"; +import { + API_PREFIX, + fetchJson, + sendNoContent, + writeJson, +} from "./http"; +import { + fetchProjectsRaw, + fetchWorkspacesRaw, + listTopicsByWorkspaceID, + normalizeWorkflowBoardResponse, + normalizeWorkspace, + resolveTopic, + resolveWorkspace, + resolveWorkspaceID, + type RawTopic, + type TopicSpace, +} from "./internal"; + +export { + answerHumanTask, + deleteSkill, + fetchRoleDetail, + fetchRoleSkills, + fetchSkills, + type RoleSkillOverrideUpdate, + type SkillCatalogSaveInput, + updateRole, + updateRoleSkills, + upsertSkill, +} from "./roleConfig"; + +export async function fetchWorkspaces(): Promise { + const [projects, workspaces] = await Promise.all([ + fetchProjectsRaw(), + fetchWorkspacesRaw(), + ]); + const projectByID = new Map(projects.map((item) => [item.id, item])); + return workspaces.map((item) => normalizeWorkspace(item, projectByID)); +} + +export async function fetchDispatch(ws: string): Promise { + const data = await fetchJson<{ logs: DispatchLog[] }>( + `${API_PREFIX}/dashboard/dispatch?workspace=${encodeURIComponent(ws)}`, + ); + return data.logs ?? []; +} + +export async function fetchRoles(ws?: string): Promise { + const q = ws?.trim() + ? `?workspace=${encodeURIComponent(ws.trim())}` + : ""; + const data = await fetchJson<{ roles: RoleInfo[] }>( + `${API_PREFIX}/dashboard/roles${q}`, + ); + return data.roles ?? []; +} + +export async function fetchWorkflowBoard( + workspace: string, + topic?: string, +): Promise { + const params = new URLSearchParams({ workspace }); + if (topic?.trim()) { + params.set("topic", topic.trim()); + } + const data = await fetchJson( + `${API_PREFIX}/dashboard/workflow/board?${params.toString()}`, + ); + return normalizeWorkflowBoardResponse(data); +} + +export async function sendMessage(params: { + workspace: string; + to: string; + topic: string; + body: string; + type?: string; + stage?: string; + reply_to?: string; +}): Promise<{ status: string; file: string }> { + const workspaceRecord = await resolveWorkspace(params.workspace); + const workspaceID = workspaceRecord.id; + const space: TopicSpace = "workflow"; + const topics = await listTopicsByWorkspaceID(workspaceID); + let record = topics.find( + (item) => + (item.slug === params.topic || item.title === params.topic) && + item.space === space, + ); + if (!record) { + const created = await createTopicByWorkspaceID(workspaceID, params.topic, space); + if (!created.id?.trim()) { + throw new Error(`Topic created without an id: ${params.topic}`); + } + record = { + id: created.id, + workspace_id: workspaceID, + slug: created.name, + title: created.name, + space: created.space, + status: created.status, + summary: created.description, + created_at: created.created_at, + updated_at: created.updated_at, + }; + } + const topicID = record.id; + const data = await writeJson<{ id?: string; message_id?: string; status?: string }>( + `${API_PREFIX}/topics/${encodeURIComponent(topicID)}/messages`, + "POST", + { + workspace_id: workspaceID, + from_role_name: "user", + to_expr: params.to, + type: params.type ?? "chat", + stage: params.stage ?? "plan", + reply_to_message_id: params.reply_to, + body_markdown: params.body.trim(), + }, + ); + + return { + status: data.status ?? "delivered", + file: data.id ?? "", + }; +} + +export interface TopicRecord { + id?: string; + name: string; + space: string; + status: string; + created_at: string; + updated_at: string; + description?: string; +} + +function normalizeTopicRecord(topic: RawTopic): TopicRecord { + return { + id: topic.id, + name: topic.slug, + space: topic.space, + status: topic.status, + created_at: topic.created_at, + updated_at: topic.updated_at, + description: topic.summary, + }; +} +async function createTopicByWorkspaceID( + workspaceID: string, + topic: string, + space: TopicSpace, +): Promise { + const created = await writeJson(`${API_PREFIX}/topics`, "POST", { + workspace_id: workspaceID, + slug: topic, + title: topic, + space, + status: "plan", + summary: "", + }); + return normalizeTopicRecord(created); +} + +export async function createTopic( + workspace: string, + topic: string, + space: TopicSpace, +): Promise { + const workspaceID = await resolveWorkspaceID(workspace); + const existingTopics = await listTopicsByWorkspaceID(workspaceID); + const existing = existingTopics.find( + (item) => (item.slug === topic || item.title === topic) && item.space === space, + ); + if (existing) { + return normalizeTopicRecord(existing); + } + return createTopicByWorkspaceID(workspaceID, topic, space); +} + +export async function fetchTopicRecords( + workspace: string, + space?: TopicSpace, +): Promise { + const q = new URLSearchParams({ workspace }); + if (space) q.set("space", space); + const data = await fetchJson<{ records: TopicRecord[] }>( + `${API_PREFIX}/dashboard/topics/records?${q.toString()}`, + ); + return data.records ?? []; +} + +export async function deleteTopic(workspace: string, topic: string): Promise { + const record = await resolveTopic(workspace, topic); + await sendNoContent( + `${API_PREFIX}/topics/${encodeURIComponent(record.id)}`, + "DELETE", + ); +} + +export async function stopTopic(workspace: string, topic: string): Promise { + const record = await resolveTopic(workspace, topic); + await writeJson( + `${API_PREFIX}/topics/${encodeURIComponent(record.id)}/stop`, + "POST", + ); +} + +export async function confirmTopicPlan(workspace: string, topic: string): Promise { + const record = await resolveTopic(workspace, topic); + await writeJson( + `${API_PREFIX}/topics/${encodeURIComponent(record.id)}/confirm`, + "POST", + ); +} diff --git a/dashboard/src/api/http.ts b/dashboard/src/api/http.ts new file mode 100644 index 0000000..10d5c71 --- /dev/null +++ b/dashboard/src/api/http.ts @@ -0,0 +1,97 @@ +export const API_PREFIX = "/api/v2"; +const REQUEST_TIMEOUT_MS = 15_000; + +function normalizeRequestError(error: unknown): Error { + if (error instanceof DOMException && error.name === "AbortError") { + return new Error("Request timed out. Check the local API and try again."); + } + + if (error instanceof TypeError) { + return new Error( + "Network request failed. Check the local API or your connection.", + ); + } + + return error instanceof Error ? error : new Error("Request failed."); +} + +export async function fetchWithTimeout( + input: RequestInfo | URL, + init?: RequestInit, +): Promise { + const controller = new AbortController(); + const timeoutId = window.setTimeout( + () => controller.abort(), + REQUEST_TIMEOUT_MS, + ); + + try { + return await fetch(input, { ...init, signal: controller.signal }); + } catch (error) { + throw normalizeRequestError(error); + } finally { + window.clearTimeout(timeoutId); + } +} + +export async function readJson(res: Response): Promise { + try { + return (await res.json()) as T; + } catch { + throw new Error("Server returned an unreadable response."); + } +} + +export async function readErrorMessage(res: Response): Promise { + const contentType = res.headers.get("content-type") ?? ""; + + try { + if (contentType.includes("application/json")) { + const data = (await res.json()) as { error?: string; message?: string }; + if (data.error?.trim()) return data.error; + if (data.message?.trim()) return data.message; + } else { + const text = (await res.text()).trim(); + if (text) return text; + } + } catch { + // Fall through to status fallback. + } + + if (res.status === 404) return "Resource not found."; + if (res.status === 429) return "Too many requests. Try again in a moment."; + if (res.status >= 500) return "Server error. Try again in a moment."; + + return `${res.status} ${res.statusText}`.trim() || "Request failed."; +} + +export async function fetchJson(url: string): Promise { + const res = await fetchWithTimeout(url); + if (!res.ok) { + throw new Error(await readErrorMessage(res)); + } + return readJson(res); +} + +export async function writeJson( + url: string, + method: string, + body?: unknown, +): Promise { + const res = await fetchWithTimeout(url, { + method, + headers: { "Content-Type": "application/json" }, + body: body === undefined ? undefined : JSON.stringify(body), + }); + if (!res.ok) { + throw new Error(await readErrorMessage(res)); + } + return readJson(res); +} + +export async function sendNoContent(url: string, method: string): Promise { + const res = await fetchWithTimeout(url, { method }); + if (!res.ok) { + throw new Error(await readErrorMessage(res)); + } +} diff --git a/dashboard/src/api/internal.ts b/dashboard/src/api/internal.ts new file mode 100644 index 0000000..7a5d63a --- /dev/null +++ b/dashboard/src/api/internal.ts @@ -0,0 +1,320 @@ +import type { WorkflowBoardResponse, Workspace } from "../types"; +import { API_PREFIX, fetchJson } from "./http"; + +export interface RawProject { + id: string; + slug: string; + name: string; + root_path: string; +} + +export interface RawWorkspace { + id: string; + project_id: string; + slug: string; + name: string; + root_path: string; + base_branch: string; + worktree_branch: string; + runtime_backend: string; + status: string; + provision_state: string; + provision_error: string; + last_provisioned_at?: string | null; + container_state?: string; + created_at: string; + updated_at: string; +} + +export interface RawTopic { + id: string; + workspace_id: string; + slug: string; + title: string; + space: string; + status: string; + summary?: string; + created_at: string; + updated_at: string; +} + +export type TopicSpace = "workflow"; + +export interface RawRoleDefinition { + name: string; + title: string; + executor_kind?: string; + description: string; + is_enabled: boolean; + is_builtin: boolean; + sort_order: number; +} + +export interface RawRolePrompt { + role_name: string; + workspace_id?: string; + prompt_kind: string; + content_markdown: string; +} + +export interface RawRoleConfig { + role_name?: string; + config_toml?: string; + auth_json?: string; +} + +export interface RawRoleSkillBinding { + workspace_id?: string; + skill_id: string; + is_enabled: boolean; + sort_order: number; + config?: Record; +} + +export interface RawSkill { + id: string; + skill_key: string; + name: string; + description: string; + source_type: string; + content_markdown: string; + status: string; + updated_at?: string; +} + +export interface RawRoleDetailResponse { + role: RawRoleDefinition; + prompts: RawRolePrompt[]; + config: RawRoleConfig; + bindings: RawRoleSkillBinding[]; +} + +export interface RawResolvedRole { + config: RawRoleConfig; + skills: Array<{ + binding: RawRoleSkillBinding; + skill: RawSkill; + }>; +} + +function normalizeStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return value.filter((item): item is string => typeof item === "string"); +} + +export function normalizeWorkflowBoardResponse( + value: WorkflowBoardResponse, +): WorkflowBoardResponse { + const topics = Array.isArray(value.topics) + ? value.topics.map((topic) => ({ + ...topic, + status: topic.status ?? "", + running_roles: normalizeStringArray(topic.running_roles), + waiting_roles: normalizeStringArray(topic.waiting_roles), + })) + : []; + + const board = value.board + ? { + ...value.board, + plan: value.board.plan + ? { + ...value.board.plan, + summary_markdown: value.board.plan.summary_markdown ?? "", + } + : null, + summary: { + ...value.board.summary, + active_roles: normalizeStringArray(value.board.summary?.active_roles), + }, + agents: Array.isArray(value.board.agents) ? value.board.agents : [], + lanes: Array.isArray(value.board.lanes) ? value.board.lanes : [], + tasks: Array.isArray(value.board.tasks) + ? value.board.tasks.map((task) => ({ + ...task, + lane_id: task.lane_id ?? "", + deliverables: Array.isArray(task.deliverables) + ? task.deliverables.filter((item): item is string => typeof item === "string") + : [], + dependencies: Array.isArray(task.dependencies) + ? task.dependencies + : [], + })) + : [], + links: Array.isArray(value.board.links) ? value.board.links : [], + events: Array.isArray(value.board.events) ? value.board.events : [], + pending_human_tasks: Array.isArray(value.board.pending_human_tasks) + ? value.board.pending_human_tasks + : [], + } + : null; + + return { + ...value, + topics, + active_topic: value.active_topic ?? null, + board, + }; +} + +export function normalizeKey(value: string): string { + return value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +export function normalizeWorkspaceRecord( + workspace: RawWorkspace, + project?: RawProject, +): Workspace { + const slug = workspace.slug || workspace.name || ""; + + return { + id: workspace.id, + name: slug, + slug, + project_slug: project?.slug ?? "", + project_dir: project?.root_path ?? "", + path: workspace.root_path ?? "", + base_branch: workspace.base_branch ?? "", + worktree_branch: workspace.worktree_branch ?? "", + runtime_backend: workspace.runtime_backend ?? "", + container_name: "", + status: workspace.status ?? "", + provision_state: workspace.provision_state ?? "pending", + provision_error: workspace.provision_error ?? "", + last_provisioned_at: workspace.last_provisioned_at ?? "", + container_state: workspace.container_state ?? "missing", + created_at: workspace.created_at ?? "", + updated_at: workspace.updated_at ?? "", + }; +} + +export function normalizeWorkspace( + workspace: RawWorkspace, + projectByID: Map, +): Workspace { + return normalizeWorkspaceRecord( + workspace, + projectByID.get(workspace.project_id), + ); +} + +export function pickPrompt(prompts: RawRolePrompt[], workspaceID: string): string { + const systemWorkspace = prompts.find( + (prompt) => + prompt.prompt_kind === "system" && prompt.workspace_id === workspaceID, + ); + if (systemWorkspace) return systemWorkspace.content_markdown; + + const systemGlobal = prompts.find( + (prompt) => prompt.prompt_kind === "system" && !prompt.workspace_id, + ); + if (systemGlobal) return systemGlobal.content_markdown; + + return ""; +} + +export function selectBinding( + bindings: RawRoleSkillBinding[], + workspaceID: string, + skillID: string, +): RawRoleSkillBinding | undefined { + return ( + bindings.find( + (binding) => + binding.skill_id === skillID && binding.workspace_id === workspaceID, + ) ?? + bindings.find( + (binding) => binding.skill_id === skillID && !binding.workspace_id, + ) + ); +} + +export async function fetchProjectsRaw(): Promise { + const data = await fetchJson<{ projects: RawProject[] }>( + `${API_PREFIX}/projects`, + ); + return data.projects ?? []; +} + +export async function fetchWorkspacesRaw(): Promise { + const data = await fetchJson<{ workspaces: RawWorkspace[] }>( + `${API_PREFIX}/workspaces`, + ); + return data.workspaces ?? []; +} + +export async function resolveWorkspace(workspace: string): Promise { + const items = await fetchWorkspacesRaw(); + const resolved = + items.find((item) => item.slug === workspace) ?? + items.find((item) => item.name === workspace); + if (!resolved) { + throw new Error(`Workspace not found: ${workspace}`); + } + return resolved; +} + +export async function resolveWorkspaceID(workspace: string): Promise { + const resolved = await resolveWorkspace(workspace); + return resolved.id; +} + +export async function listTopicsByWorkspaceID(workspaceID: string): Promise { + const data = await fetchJson<{ topics: RawTopic[] }>( + `${API_PREFIX}/topics?workspace_id=${encodeURIComponent(workspaceID)}`, + ); + return data.topics ?? []; +} + +export async function resolveTopic( + workspace: string, + topic: string, + spaces?: string[], +): Promise { + const workspaceID = await resolveWorkspaceID(workspace); + const items = await listTopicsByWorkspaceID(workspaceID); + const resolved = items.find( + (item) => + (item.slug === topic || item.title === topic) && + (!spaces || spaces.includes(item.space)), + ); + if (!resolved) { + throw new Error(`Topic not found: ${topic}`); + } + return resolved; +} + +export async function fetchRoleDetailResponse( + roleName: string, + workspaceID?: string, +): Promise { + const q = workspaceID + ? `?workspace_id=${encodeURIComponent(workspaceID)}` + : ""; + return fetchJson( + `${API_PREFIX}/config/roles/${encodeURIComponent(roleName)}${q}`, + ); +} + +export async function fetchResolvedRole( + roleName: string, + workspaceID?: string, +): Promise { + const q = workspaceID + ? `?workspace_id=${encodeURIComponent(workspaceID)}` + : ""; + return fetchJson( + `${API_PREFIX}/runtime/roles/${encodeURIComponent(roleName)}${q}`, + ); +} + +export async function fetchSkillsRaw(): Promise { + const data = await fetchJson<{ skills: RawSkill[] }>( + `${API_PREFIX}/config/skills`, + ); + return data.skills ?? []; +} diff --git a/dashboard/src/api/roleConfig.ts b/dashboard/src/api/roleConfig.ts new file mode 100644 index 0000000..c78d826 --- /dev/null +++ b/dashboard/src/api/roleConfig.ts @@ -0,0 +1,249 @@ +import type { RoleDetail, RoleSkill, SkillCatalogEntry } from "../types"; +import { extractSkillMetadata } from "../utils/skillMetadata"; +import { + fetchRoleDetailResponse, + fetchSkillsRaw, + normalizeKey, + pickPrompt, + resolveWorkspaceID, + selectBinding, +} from "./internal"; +import { API_PREFIX, sendNoContent, writeJson } from "./http"; + +export async function fetchRoleDetail( + ws: string, + name: string, +): Promise { + const workspaceID = ws ? await resolveWorkspaceID(ws) : ""; + const data = await fetchRoleDetailResponse(name, workspaceID); + return { + name: data.role.name, + description: data.role.description, + prompt: pickPrompt(data.prompts ?? [], workspaceID), + sort_order: data.role.sort_order, + config_toml: data.config?.config_toml ?? "", + auth_json: data.config?.auth_json ?? "{}", + }; +} + +export async function updateRole( + ws: string, + name: string, + updates: { + description?: string; + prompt?: string; + sort_order?: number; + config_toml?: string; + auth_json?: string; + }, +): Promise { + const workspaceID = ws ? await resolveWorkspaceID(ws) : ""; + const current = await fetchRoleDetailResponse(name, workspaceID); + + await writeJson( + `${API_PREFIX}/config/roles/${encodeURIComponent(name)}`, + "PUT", + { + title: current.role.title, + executor_kind: current.role.executor_kind, + description: updates.description ?? current.role.description, + is_enabled: current.role.is_enabled, + is_builtin: current.role.is_builtin, + sort_order: updates.sort_order ?? current.role.sort_order, + updated_by: "dashboard", + }, + ); + + if (updates.prompt !== undefined) { + await writeJson( + `${API_PREFIX}/config/roles/${encodeURIComponent(name)}/prompts/system`, + "PUT", + { + workspace_id: workspaceID || undefined, + content_markdown: updates.prompt, + updated_by: "dashboard", + }, + ); + } + + if (updates.config_toml !== undefined || updates.auth_json !== undefined) { + await writeJson( + `${API_PREFIX}/config/roles/${encodeURIComponent(name)}/config`, + "PUT", + { + config_toml: updates.config_toml ?? current.config?.config_toml ?? "", + auth_json: updates.auth_json ?? current.config?.auth_json ?? "{}", + updated_by: "dashboard", + }, + ); + } +} + +export async function answerHumanTask( + taskID: string, + body: string, +): Promise { + await writeJson( + `${API_PREFIX}/human-tasks/${encodeURIComponent(taskID)}/answer`, + "POST", + { body_markdown: body.trim() }, + ); +} + +export async function fetchRoleSkills( + workspace: string, + roleName: string, +): Promise { + const workspaceID = workspace ? await resolveWorkspaceID(workspace) : ""; + const [detail, skills] = await Promise.all([ + fetchRoleDetailResponse(roleName, workspaceID || undefined), + fetchSkillsRaw(), + ]); + + const skillByID = new Map(skills.map((skill) => [skill.id, skill])); + const results: RoleSkill[] = skills.map((skill) => { + const binding = selectBinding(detail.bindings ?? [], workspaceID, skill.id); + return { + id: skill.skill_key, + name: skill.name, + description: skill.description, + category: + (typeof binding?.config?.category === "string" + ? binding.config.category + : skill.source_type) || "other", + enabled: binding?.is_enabled ?? false, + missing: false, + }; + }); + + for (const binding of detail.bindings ?? []) { + if (skillByID.has(binding.skill_id)) continue; + results.push({ + id: binding.skill_id, + name: binding.skill_id, + description: "", + category: + (typeof binding.config?.category === "string" + ? binding.config.category + : "other"), + enabled: binding.is_enabled, + missing: true, + }); + } + + return results.sort((left, right) => left.name.localeCompare(right.name)); +} + +export interface RoleSkillOverrideUpdate { + id: string; + category: string; + sort_order?: number; +} + +export async function updateRoleSkills( + workspace: string, + roleName: string, + selectedIds: string[], + skillOverrides: RoleSkillOverrideUpdate[] = [], +): Promise { + const workspaceID = workspace ? await resolveWorkspaceID(workspace) : ""; + const [detail, skills] = await Promise.all([ + fetchRoleDetailResponse(roleName, workspaceID || undefined), + fetchSkillsRaw(), + ]); + + const selected = new Set(selectedIds); + const overrides = new Map(skillOverrides.map((item) => [item.id, item])); + + await Promise.all( + skills.map(async (skill) => { + const binding = selectBinding(detail.bindings ?? [], workspaceID, skill.id); + const override = overrides.get(skill.skill_key); + const category = + override?.category?.trim() || + (typeof binding?.config?.category === "string" + ? binding.config.category + : skill.source_type || "other"); + + await writeJson( + `${API_PREFIX}/config/roles/${encodeURIComponent(roleName)}/skills/${encodeURIComponent(skill.skill_key)}`, + "PUT", + { + workspace_id: workspaceID || undefined, + is_enabled: selected.has(skill.skill_key), + sort_order: override?.sort_order ?? binding?.sort_order ?? 1000, + config: { category }, + updated_by: "dashboard", + }, + ); + }), + ); +} + +export async function fetchSkills(): Promise { + const skills = await fetchSkillsRaw(); + return skills.map((skill) => ({ + id: skill.skill_key, + name: skill.name, + description: skill.description, + category: skill.source_type || "other", + sort_order: 1000, + content: skill.content_markdown, + updated_at: skill.updated_at, + })); +} + +export interface SkillCatalogSaveInput { + id?: string; + category: string; + sort_order: number; + content: string; +} + +export async function upsertSkill( + skill: SkillCatalogSaveInput, +): Promise { + const metadata = extractSkillMetadata(skill.content); + const skillKey = + skill.id?.trim() || + normalizeKey(metadata.name) || + normalizeKey(skill.category) || + `skill-${Date.now()}`; + + const saved = await writeJson<{ + skill_key: string; + name: string; + description: string; + source_type: string; + content_markdown: string; + updated_at?: string; + }>( + `${API_PREFIX}/config/skills/${encodeURIComponent(skillKey)}`, + "PUT", + { + name: metadata.name || skillKey, + description: metadata.description || "", + source_type: skill.category.trim() || "other", + content_markdown: skill.content, + status: "active", + updated_by: "dashboard", + }, + ); + + return { + id: saved.skill_key, + name: saved.name, + description: saved.description, + category: saved.source_type || "other", + sort_order: skill.sort_order || 1000, + content: saved.content_markdown, + updated_at: saved.updated_at, + }; +} + +export async function deleteSkill(id: string): Promise { + await sendNoContent( + `${API_PREFIX}/config/skills/${encodeURIComponent(id)}`, + "DELETE", + ); +} diff --git a/dashboard/src/components/ExecutionTable.test.tsx b/dashboard/src/components/ExecutionTable.test.tsx new file mode 100644 index 0000000..4febaa0 --- /dev/null +++ b/dashboard/src/components/ExecutionTable.test.tsx @@ -0,0 +1,203 @@ +import { screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { DispatchLog } from "../types"; +import { usePolling } from "../hooks/usePolling"; +import { renderWithProviders } from "../test/renderWithProviders"; +import ExecutionTable from "./ExecutionTable"; + +vi.mock("../hooks/usePolling", () => ({ + usePolling: vi.fn(), +})); + +const usePollingMock = vi.mocked(usePolling); + +function mockPollingState(overrides: Partial> = {}) { + const refresh = vi.fn(); + usePollingMock.mockReturnValue({ + data: null, + loading: false, + error: null, + refresh, + ...overrides, + } as ReturnType); + + return refresh; +} + +afterEach(() => { + vi.clearAllMocks(); +}); + +function makeLog(overrides: Partial = {}): DispatchLog { + return { + role: "worker", + inbox_file: "20260309T090000Z-old.md", + stage: "execution", + topic: "topic-0", + mode: "exec", + started_at: "2026-03-09T09:00:00Z", + completed_at: "2026-03-09T09:01:30Z", + exit_code: 0, + reply: "", + error_message: "", + running: false, + ...overrides, + }; +} + +describe("ExecutionTable", () => { + it("prompts for a workspace before loading runs", () => { + mockPollingState(); + + renderWithProviders(, { locale: "en" }); + + expect(screen.getByText("Select a project")).toBeInTheDocument(); + expect(screen.getByText(/load run history/i)).toBeInTheDocument(); + }); + + it("prioritizes running and failed runs before completed runs", () => { + const completed = makeLog({ + topic: "completed-topic", + started_at: "2026-03-09T11:15:00Z", + }); + const failed = makeLog({ + topic: "failed-topic", + started_at: "2026-03-09T10:15:00Z", + exit_code: 1, + error_message: "failed", + }); + const running = makeLog({ + topic: "running-topic", + started_at: "2026-03-09T09:15:00Z", + mode: "resume", + completed_at: "", + running: true, + }); + + mockPollingState({ data: [completed, failed, running] }); + + const { container } = renderWithProviders(, { + locale: "en", + }); + const rows = Array.from( + container.querySelectorAll("tbody > tr"), + ) as HTMLTableRowElement[]; + + expect(rows).toHaveLength(3); + expect(within(rows[0]!).getByText("running-topic")).toBeInTheDocument(); + expect(within(rows[0]!).getByText("Continue thread")).toBeInTheDocument(); + expect(within(rows[0]!).getByText("Running")).toBeInTheDocument(); + expect(within(rows[1]!).getByText("failed-topic")).toBeInTheDocument(); + expect(within(rows[2]!).getByText("completed-topic")).toBeInTheDocument(); + }); + + it("paginates desktop rows and loads older runs on mobile", async () => { + const user = userEvent.setup(); + const logs = Array.from({ length: 27 }, (_, index) => makeLog({ + inbox_file: `20260309T09${String(index).padStart(2, "0")}00Z-${index}.md`, + topic: `topic-${index}`, + started_at: `2026-03-09T${String(index % 24).padStart(2, "0")}:00:00Z`, + })); + mockPollingState({ data: logs }); + + const { container } = renderWithProviders(, { + locale: "en", + }); + + expect(screen.getByText("Showing 8 of 27 runs.")).toBeInTheDocument(); + expect(screen.getByText("Page 1 of 2")).toBeInTheDocument(); + expect(container.querySelectorAll("tbody > tr")).toHaveLength(25); + + await user.click(screen.getByRole("button", { name: "Load older runs" })); + + expect(screen.getByText("Showing 16 of 27 runs.")).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "Next" })); + + expect(screen.getByText("Page 2 of 2")).toBeInTheDocument(); + expect(screen.getByText("Showing 26-27 of 27 runs.")).toBeInTheDocument(); + expect(container.querySelectorAll("tbody > tr")).toHaveLength(2); + }); + + it("keeps run notes collapsed until explicitly expanded", async () => { + const user = userEvent.setup(); + mockPollingState({ + data: [ + makeLog({ + topic: "noted-run", + reply: "Long diagnostic note for this run.", + }), + ], + }); + + renderWithProviders(, { locale: "en" }); + + expect(screen.queryByText("Long diagnostic note for this run.")).not.toBeInTheDocument(); + + await user.click(screen.getAllByRole("button", { name: "Show note" })[0]!); + + expect( + screen.getAllByText("Long diagnostic note for this run.").length, + ).toBeGreaterThan(0); + }); + + it("shows a new-runs banner when fresh data arrives while viewing older history", async () => { + const user = userEvent.setup(); + const initialLogs = Array.from({ length: 26 }, (_, index) => makeLog({ + inbox_file: `20260309T09${String(index).padStart(2, "0")}00Z-${index}.md`, + topic: `topic-${index}`, + started_at: `2026-03-09T${String(index % 24).padStart(2, "0")}:00:00Z`, + })); + const refresh = mockPollingState({ data: initialLogs }); + + const { rerender } = renderWithProviders(, { + locale: "en", + }); + + await user.click(screen.getByRole("button", { name: "Next" })); + expect(screen.getByText("Page 2 of 2")).toBeInTheDocument(); + + usePollingMock.mockReturnValue({ + data: [ + makeLog({ + inbox_file: "20260310T120000Z-fresh.md", + topic: "fresh-topic", + started_at: "2026-03-10T12:00:00Z", + running: true, + completed_at: "", + mode: "resume", + }), + ...initialLogs, + ], + loading: false, + error: null, + refresh, + } as ReturnType); + + rerender(); + + expect( + screen.getByText("New executions arrived while you were reviewing older history."), + ).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "Back to latest" })); + + expect(screen.getByText("Page 1 of 2")).toBeInTheDocument(); + expect(screen.queryByText("New executions arrived while you were reviewing older history.")).not.toBeInTheDocument(); + }); + + it("retries after a polling error", async () => { + const refresh = mockPollingState({ + error: "dispatch offline", + }); + const user = userEvent.setup(); + + renderWithProviders(, { locale: "en" }); + + expect(screen.getByText("Couldn't load runs")).toBeInTheDocument(); + await user.click(screen.getByRole("button", { name: "Retry" })); + + expect(refresh).toHaveBeenCalledTimes(1); + }); +}); diff --git a/dashboard/src/components/ExecutionTable.tsx b/dashboard/src/components/ExecutionTable.tsx new file mode 100644 index 0000000..7141516 --- /dev/null +++ b/dashboard/src/components/ExecutionTable.tsx @@ -0,0 +1,481 @@ +import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { usePolling } from "../hooks/usePolling"; +import { fetchDispatch } from "../api/client"; +import type { DispatchLog } from "../types"; +import ExecutionRunNote from "./executions/ExecutionRunNote"; +import ExecutionRunSummary from "./executions/ExecutionRunSummary"; +import AlertBanner from "./ui/AlertBanner"; +import RoleBadge from "./RoleBadge"; +import AsyncPageState from "./ui/AsyncPageState"; +import Button from "./ui/Button"; +import Card from "./ui/Card"; +import PageHero from "./ui/PageHero"; +import PageSectionCard from "./ui/PageSectionCard"; +import StatusBadge from "./ui/StatusBadge"; +import SummaryStat from "./ui/SummaryStat"; +import { useI18n } from "../i18n"; +import { dispatchListChangeKey } from "../utils/pollingKeys"; +import { exitCodeTone } from "../styles/tokens"; + +interface ExecutionTableProps { + workspace: string; +} + +const MOBILE_PAGE_SIZE = 8; +const DESKTOP_PAGE_SIZE = 25; + +function getRunKey(log: DispatchLog): string { + return `${log.role}-${log.inbox_file}-${log.started_at}-${log.completed_at || "running"}`; +} + +function getRunPriority(log: DispatchLog): number { + if (log.running) return 0; + if (log.exit_code !== 0) return 1; + return 2; +} + +function getRunNote(log: DispatchLog): string { + return log.error_message?.trim() || log.reply?.trim() || ""; +} + +function getRunSummaryFields( + log: DispatchLog, + labels: { + topic: string; + phase: string; + thread: string; + duration: string; + started: string; + fallback: string; + }, + formatStageLabel: (value: string) => string, + formatRunModeLabel: (value: string) => string, + formatDuration: (startedAt: string, completedAt: string) => string, + formatTimestampLabel: (value: string) => string, + formatAbsoluteDateTime: (value: string) => string, +) { + return [ + { + label: labels.topic, + value: {log.topic || labels.fallback}, + }, + { + label: labels.phase, + value: formatStageLabel(log.stage), + }, + { + label: labels.thread, + value: formatRunModeLabel(log.mode), + }, + { + label: labels.duration, + value: {formatDuration(log.started_at, log.completed_at)}, + }, + { + label: labels.started, + value: ( + + {formatTimestampLabel(log.started_at)} + + ), + fullWidth: true, + }, + ]; +} + +export default function ExecutionTable({ workspace }: ExecutionTableProps) { + const { + copy, + formatAbsoluteDateTime, + formatDuration, + formatRunModeLabel, + formatStageLabel, + formatTimestampLabel, + } = useI18n(); + const fetcher = useCallback(() => fetchDispatch(workspace), [workspace]); + const { data, loading, error, refresh } = usePolling( + workspace ? fetcher : null, + 5000, + { getChangeKey: dispatchListChangeKey }, + ); + const [mobileVisibleCount, setMobileVisibleCount] = useState(MOBILE_PAGE_SIZE); + const [desktopPage, setDesktopPage] = useState(1); + const [expandedNotes, setExpandedNotes] = useState>(new Set()); + const [hasHiddenNewRuns, setHasHiddenNewRuns] = useState(false); + const latestSeenKeyRef = useRef(null); + const latestAnchorRef = useRef(null); + + const sorted = useMemo(() => { + if (!data) return []; + return [...data].sort((a, b) => { + const priorityDifference = getRunPriority(a) - getRunPriority(b); + if (priorityDifference !== 0) return priorityDifference; + return b.started_at.localeCompare(a.started_at); + }); + }, [data]); + + const totalPages = Math.max(1, Math.ceil(sorted.length / DESKTOP_PAGE_SIZE)); + const currentDesktopPage = Math.min(desktopPage, totalPages); + const desktopStartIndex = (currentDesktopPage - 1) * DESKTOP_PAGE_SIZE; + const desktopVisible = sorted.slice( + desktopStartIndex, + desktopStartIndex + DESKTOP_PAGE_SIZE, + ); + const mobileVisible = sorted.slice(0, mobileVisibleCount); + const mobileWindowEnd = mobileVisible.length; + const desktopWindowStart = sorted.length === 0 ? 0 : desktopStartIndex + 1; + const desktopWindowEnd = desktopStartIndex + desktopVisible.length; + const latestRunKey = sorted[0] ? getRunKey(sorted[0]) : null; + const isViewingLatest = desktopPage === 1 && mobileVisibleCount === MOBILE_PAGE_SIZE; + + useEffect(() => { + setMobileVisibleCount(MOBILE_PAGE_SIZE); + setDesktopPage(1); + setExpandedNotes(new Set()); + setHasHiddenNewRuns(false); + latestSeenKeyRef.current = null; + }, [workspace]); + + useEffect(() => { + setDesktopPage((current) => Math.min(current, totalPages)); + setMobileVisibleCount((current) => { + if (sorted.length === 0) return MOBILE_PAGE_SIZE; + return Math.min(Math.max(current, MOBILE_PAGE_SIZE), sorted.length); + }); + }, [sorted.length, totalPages]); + + useEffect(() => { + if (!latestRunKey) { + latestSeenKeyRef.current = null; + setHasHiddenNewRuns(false); + return; + } + + if (isViewingLatest) { + latestSeenKeyRef.current = latestRunKey; + setHasHiddenNewRuns(false); + return; + } + + if (latestSeenKeyRef.current === null) { + latestSeenKeyRef.current = latestRunKey; + return; + } + + if (latestSeenKeyRef.current !== latestRunKey) { + setHasHiddenNewRuns(true); + } + }, [isViewingLatest, latestRunKey]); + + const toggleNote = useCallback((key: string) => { + setExpandedNotes((current) => { + const next = new Set(current); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }, []); + + const jumpToLatest = useCallback(() => { + setDesktopPage(1); + setMobileVisibleCount(MOBILE_PAGE_SIZE); + setHasHiddenNewRuns(false); + if (latestRunKey) { + latestSeenKeyRef.current = latestRunKey; + } + + window.requestAnimationFrame(() => { + latestAnchorRef.current?.scrollIntoView?.({ + block: "start", + behavior: "smooth", + }); + }); + }, [latestRunKey]); + + const runningCount = sorted.filter((log) => log.running).length; + const failedCount = sorted.filter((log) => !log.running && log.exit_code !== 0).length; + const resumedCount = sorted.filter((log) => log.mode === "resume").length; + + return ( + 0} + error={error} + loadingEyebrow={copy.executions.eyebrow} + loadingTitle={copy.executions.loadingTitle} + errorEyebrow={copy.executions.eyebrow} + errorTitle={copy.executions.errorTitle} + emptyEyebrow={copy.executions.eyebrow} + emptyTitle={copy.executions.emptyTitle} + emptyDetail={copy.executions.emptyDetail} + retryLabel={copy.common.retry} + onRetry={refresh} + > +
+
+ + + + +
+ } + /> + + {hasHiddenNewRuns ? ( + {copy.executions.newRunsDetail}

} + actionLabel={copy.common.backToLatest} + onAction={jumpToLatest} + /> + ) : null} + +
+ {mobileVisible.map((log) => ( + (() => { + const key = getRunKey(log); + const note = getRunNote(log); + const noteOpen = expandedNotes.has(key); + + return ( + +
+
+ + + {log.running ? "…" : log.exit_code} + +
+ + {note && ( +
+ + {noteOpen && ( + + )} +
+ )} + + ); + })() + ))} +
+

+ {copy.executions.mobileShowing(mobileWindowEnd, sorted.length)} +

+
+ {mobileVisibleCount > MOBILE_PAGE_SIZE && ( + + )} + {mobileVisibleCount < sorted.length && ( + + )} +
+
+
+ + +
+ + + + + + + + + + + + + + + {desktopVisible.map((log) => { + const key = getRunKey(log); + const note = getRunNote(log); + const noteOpen = expandedNotes.has(key); + + return ( + + + + + + + + + + + + {note && noteOpen && ( + + + + )} + + ); + })} + +
{copy.executions.role}{copy.executions.topic}{copy.executions.phase}{copy.executions.thread}{copy.executions.duration}{copy.executions.exitCode}{copy.executions.started}{copy.executions.details}
+ + +
+ {log.topic || copy.common.fallback} +
+
{formatStageLabel(log.stage)}{formatRunModeLabel(log.mode)} + {formatDuration(log.started_at, log.completed_at)} + + + {log.running ? "…" : log.exit_code} + + + {formatTimestampLabel(log.started_at)} + + {note ? ( + + ) : ( + {copy.executions.noDetail} + )} +
+ +
+
+
+

+ {copy.executions.desktopShowing(desktopWindowStart, desktopWindowEnd, sorted.length)} +

+
+ + + {copy.executions.pageLabel(currentDesktopPage, totalPages)} + + +
+
+
+
+ + ); +} diff --git a/dashboard/src/components/LocaleSelect.tsx b/dashboard/src/components/LocaleSelect.tsx new file mode 100644 index 0000000..f312fa8 --- /dev/null +++ b/dashboard/src/components/LocaleSelect.tsx @@ -0,0 +1,35 @@ +import { useId } from "react"; +import type { Locale } from "../copy"; +import { useI18n } from "../i18n"; + +export default function LocaleSelect() { + const { copy, locale, setLocale } = useI18n(); + const selectId = useId(); + const options: Array<{ id: Locale; label: string }> = [ + { id: "zh-CN", label: copy.localeToggle.zh }, + { id: "en", label: copy.localeToggle.en }, + ]; + + return ( +
+ + +
+ ); +} diff --git a/dashboard/src/components/RoleBadge.tsx b/dashboard/src/components/RoleBadge.tsx new file mode 100644 index 0000000..313d195 --- /dev/null +++ b/dashboard/src/components/RoleBadge.tsx @@ -0,0 +1,24 @@ +import { roleBadgeTone } from "../styles/tokens"; +import { useI18n } from "../i18n"; +import StatusBadge from "./ui/StatusBadge"; + +interface RoleBadgeProps { + role: string; + size?: "sm" | "md"; +} + +export default function RoleBadge({ role, size = "sm" }: RoleBadgeProps) { + const { formatRoleLabel } = useI18n(); + const colors = roleBadgeTone[role] ?? roleBadgeTone.default; + + return ( + + {formatRoleLabel(role)} + + ); +} diff --git a/dashboard/src/components/RoleEditor.test.tsx b/dashboard/src/components/RoleEditor.test.tsx new file mode 100644 index 0000000..f0df699 --- /dev/null +++ b/dashboard/src/components/RoleEditor.test.tsx @@ -0,0 +1,252 @@ +import type { ComponentProps } from "react"; +import { fireEvent, screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + fetchRoleDetail, + fetchRoleSkills, + updateRole, + updateRoleSkills, +} from "../api/client"; +import { renderWithProviders } from "../test/renderWithProviders"; +import RoleEditor from "./RoleEditor"; + +vi.mock("../api/client", () => ({ + fetchRoleDetail: vi.fn(), + fetchRoleSkills: vi.fn(), + updateRole: vi.fn(), + updateRoleSkills: vi.fn(), +})); + +const fetchRoleDetailMock = vi.mocked(fetchRoleDetail); +const fetchRoleSkillsMock = vi.mocked(fetchRoleSkills); +const updateRoleMock = vi.mocked(updateRole); +const updateRoleSkillsMock = vi.mocked(updateRoleSkills); + +function deferredPromise() { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + +function mockLoadedEditor() { + fetchRoleDetailMock.mockResolvedValue({ + name: "worker", + description: "Handles APIs", + prompt: "You are the worker agent.", + sort_order: 200, + config_toml: "model = \"gpt-5.4\"", + auth_json: "{\"OPENAI_API_KEY\":\"token-1\"}", + }); + fetchRoleSkillsMock.mockResolvedValue([ + { + id: "adapt", + name: "adapt", + description: "Adapt designs.", + category: "adaptation", + enabled: true, + missing: false, + }, + { + id: "animate", + name: "animate", + description: "Animate interfaces.", + category: "visual_design", + enabled: false, + missing: false, + }, + ]); +} + +function renderRoleEditor(props: Partial> = {}) { + const onClose = vi.fn(); + const onSaved = vi.fn(); + + renderWithProviders( + , + { locale: "en" }, + ); + + return { onClose, onSaved }; +} + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe("RoleEditor", () => { + it("loads role data and switches to the skills tab", async () => { + mockLoadedEditor(); + + renderRoleEditor(); + + expect(await screen.findByDisplayValue("Handles APIs")).toBeInTheDocument(); + expect(screen.getByDisplayValue("You are the worker agent.")).toBeInTheDocument(); + expect(screen.getByDisplayValue("200")).toBeInTheDocument(); + expect(screen.getByDisplayValue("model = \"gpt-5.4\"")).toBeInTheDocument(); + expect(screen.getByDisplayValue("{\"OPENAI_API_KEY\":\"token-1\"}")).toBeInTheDocument(); + + await userEvent.setup().click(screen.getByRole("button", { name: "Role Skills" })); + + expect(screen.getByText("Adaptation")).toBeInTheDocument(); + expect(screen.getByText("Visual Design")).toBeInTheDocument(); + }); + + it("shows a load error when role detail fails", async () => { + fetchRoleDetailMock.mockRejectedValue(new Error("Role detail offline")); + fetchRoleSkillsMock.mockResolvedValue([]); + + renderRoleEditor(); + + expect(await screen.findByRole("alert")).toHaveTextContent("Role detail offline"); + expect(screen.getByRole("button", { name: "Save Role" })).toBeInTheDocument(); + }); + + it("shows a load error when role skills loading fails", async () => { + fetchRoleDetailMock.mockResolvedValue({ + name: "worker", + description: "Handles APIs", + prompt: "You are the worker agent.", + sort_order: 200, + config_toml: "model = \"gpt-5.4\"", + auth_json: "{\"OPENAI_API_KEY\":\"token-1\"}", + }); + fetchRoleSkillsMock.mockRejectedValue(new Error("404 page not found")); + + renderRoleEditor(); + + expect(await screen.findByDisplayValue("Handles APIs")).toBeInTheDocument(); + expect(screen.getByRole("alert")).toHaveTextContent("404 page not found"); + }); + + it("saves the default role tab and disables the action while saving", async () => { + mockLoadedEditor(); + const saveRequest = deferredPromise(); + updateRoleMock.mockReturnValue(saveRequest.promise); + const user = userEvent.setup(); + const { onSaved } = renderRoleEditor(); + + const descriptionInput = await screen.findByPlaceholderText( + /short description of what this role does/i, + ); + const sortOrderInput = screen.getByRole("spinbutton"); + const promptInput = screen.getByPlaceholderText( + /the full system prompt for this agent role/i, + ); + const configInput = screen.getByPlaceholderText(/approval_policy = "never"/i); + const authInput = screen.getByPlaceholderText(/OPENAI_API_KEY/i); + + await user.clear(descriptionInput); + await user.type(descriptionInput, "Owns the API surface"); + await user.clear(sortOrderInput); + await user.type(sortOrderInput, "240"); + await user.clear(promptInput); + await user.type(promptInput, "You are the new worker agent."); + await user.clear(configInput); + await user.type(configInput, "model = \"gpt-5.5\""); + await user.clear(authInput); + fireEvent.change(authInput, { target: { value: "{\"OPENAI_API_KEY\":\"token-2\"}" } }); + + await user.click(screen.getByRole("button", { name: "Save Role" })); + + await waitFor(() => { + expect(updateRoleMock).toHaveBeenCalledWith("alpha", "worker", { + description: "Owns the API surface", + prompt: "You are the new worker agent.", + sort_order: 240, + config_toml: "model = \"gpt-5.5\"", + auth_json: "{\"OPENAI_API_KEY\":\"token-2\"}", + }); + }); + + const savingButton = screen.getByRole("button", { name: "Saving..." }); + expect(savingButton).toBeDisabled(); + expect(onSaved).not.toHaveBeenCalled(); + + saveRequest.resolve(); + + await waitFor(() => { + expect(onSaved).toHaveBeenCalledTimes(1); + }); + }); + + it("uses global role data when no workspace is selected", async () => { + mockLoadedEditor(); + const user = userEvent.setup(); + + renderRoleEditor({ workspace: "" }); + + await user.click(await screen.findByRole("button", { name: "Save Role" })); + + await waitFor(() => { + expect(updateRoleMock).toHaveBeenCalledWith("", "worker", { + description: "Handles APIs", + prompt: "You are the worker agent.", + sort_order: 200, + config_toml: "model = \"gpt-5.4\"", + auth_json: "{\"OPENAI_API_KEY\":\"token-1\"}", + }); + }); + }); + + it("saves role skills from the skills tab", async () => { + mockLoadedEditor(); + updateRoleSkillsMock.mockResolvedValue(); + const user = userEvent.setup(); + const { onSaved } = renderRoleEditor(); + + await user.click(await screen.findByRole("button", { name: "Role Skills" })); + + await user.click(screen.getByRole("checkbox", { name: /animate/i })); + await user.clear(screen.getByLabelText("animate group")); + await user.type(screen.getByLabelText("animate group"), "custom_bucket"); + await user.click(screen.getByRole("button", { name: "Save Skills" })); + + await waitFor(() => { + expect(updateRoleSkillsMock).toHaveBeenCalledWith("alpha", "worker", ["adapt", "animate"], [ + { id: "adapt", category: "adaptation" }, + { id: "animate", category: "custom_bucket" }, + ]); + }); + expect(updateRoleMock).not.toHaveBeenCalled(); + expect(onSaved).toHaveBeenCalledTimes(1); + }); + + it("traps tab navigation inside the dialog and closes on escape or backdrop click", async () => { + mockLoadedEditor(); + const user = userEvent.setup(); + const { onClose } = renderRoleEditor(); + + const dialog = await screen.findByRole("dialog", { name: "Edit: worker" }); + const closeButton = within(dialog).getByRole("button", { name: "Close" }); + const saveButton = within(dialog).getByRole("button", { name: "Save Role" }); + + closeButton.focus(); + await user.tab({ shift: true }); + expect(saveButton).toHaveFocus(); + + await user.tab(); + expect(closeButton).toHaveFocus(); + + await user.keyboard("{Escape}"); + expect(onClose).toHaveBeenCalledTimes(1); + + const backdrop = dialog.previousElementSibling; + if (!(backdrop instanceof HTMLElement)) { + throw new Error("dialog backdrop not found"); + } + + await user.click(backdrop); + expect(onClose).toHaveBeenCalledTimes(2); + }); +}); diff --git a/dashboard/src/components/RoleEditor.tsx b/dashboard/src/components/RoleEditor.tsx new file mode 100644 index 0000000..6c3cc49 --- /dev/null +++ b/dashboard/src/components/RoleEditor.tsx @@ -0,0 +1,412 @@ +import { useState, useEffect, useCallback, useId } from "react"; +import { motion } from "framer-motion"; +import { + fetchRoleDetail, + fetchRoleSkills, + updateRole, + updateRoleSkills, +} from "../api/client"; +import Button from "./ui/Button"; +import EditorActionBar from "./ui/EditorActionBar"; +import EditorShell from "./ui/EditorShell"; +import FormField from "./ui/FormField"; +import PanelHeader from "./ui/PanelHeader"; +import Tabs from "./ui/Tabs"; +import { + Dialog, + DialogClose, + DialogContent, + DialogOverlay, + DialogPortal, + DialogTitle, +} from "./ui/Dialog"; +import InsetPanel from "./ui/InsetPanel"; +import TextInput from "./ui/TextInput"; +import TextareaField from "./ui/TextareaField"; +import { useI18n } from "../i18n"; +import type { RoleSkill } from "../types"; + +interface RoleEditorProps { + workspace: string; + roleName: string; + onClose: () => void; + onSaved: () => void; +} + +type Tab = "role" | "roleSkills"; + +interface SkillSection { + category: string; + skills: RoleSkill[]; +} + +function getErrorMessage(error: unknown) { + return error instanceof Error ? error.message : "Request failed."; +} + +function groupRoleSkills(skills: RoleSkill[]): SkillSection[] { + const sections: SkillSection[] = []; + const sectionByCategory = new Map(); + + for (const skill of skills) { + const category = skill.category || "other"; + let section = sectionByCategory.get(category); + if (!section) { + section = { category, skills: [] }; + sectionByCategory.set(category, section); + sections.push(section); + } + section.skills.push(skill); + } + + return sections; +} + +function formatSkillCategoryLabel(category: string) { + return category + .replace(/[_-]+/g, " ") + .replace(/\b\w/g, (match) => match.toUpperCase()); +} + +export default function RoleEditor({ + workspace, + roleName, + onClose, + onSaved, +}: RoleEditorProps) { + const { copy } = useI18n(); + const titleId = useId(); + + const [tab, setTab] = useState("role"); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(""); + + const [name, setName] = useState(""); + const [sortOrder, setSortOrder] = useState(1000); + const [description, setDescription] = useState(""); + const [prompt, setPrompt] = useState(""); + const [configToml, setConfigToml] = useState(""); + const [authJson, setAuthJson] = useState("{}"); + const [roleSkills, setRoleSkills] = useState([]); + const [skillCategoryDrafts, setSkillCategoryDrafts] = useState>({}); + + const syncRoleSkills = useCallback((skills: RoleSkill[]) => { + setRoleSkills(skills); + setSkillCategoryDrafts( + Object.fromEntries(skills.map((skill) => [skill.id, skill.category || "other"])), + ); + }, []); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(""); + + Promise.allSettled([ + fetchRoleDetail(workspace, roleName), + fetchRoleSkills(workspace, roleName), + ]).then(([roleResult, skillsResult]) => { + if (cancelled) return; + + if (roleResult.status === "rejected") { + setError(getErrorMessage(roleResult.reason)); + setLoading(false); + return; + } + + setName(roleResult.value.name); + setSortOrder(roleResult.value.sort_order); + setDescription(roleResult.value.description); + setPrompt(roleResult.value.prompt); + setConfigToml(roleResult.value.config_toml); + setAuthJson(roleResult.value.auth_json); + + if (skillsResult.status === "fulfilled") { + syncRoleSkills(skillsResult.value); + } else { + setError(getErrorMessage(skillsResult.reason)); + } + + setLoading(false); + }); + + return () => { + cancelled = true; + }; + }, [workspace, roleName, syncRoleSkills]); + + const handleSaveRole = useCallback(async () => { + setSaving(true); + setError(""); + try { + await updateRole(workspace, roleName, { + description, + prompt, + sort_order: sortOrder, + config_toml: configToml, + auth_json: authJson, + }); + onSaved(); + } catch (err: unknown) { + setError(getErrorMessage(err)); + } finally { + setSaving(false); + } + }, [workspace, roleName, sortOrder, description, prompt, configToml, authJson, onSaved]); + + const handleSaveRoleSkills = useCallback(async () => { + setSaving(true); + setError(""); + try { + const metadata = roleSkills + .filter((skill) => !skill.missing) + .map((skill) => ({ + id: skill.id, + category: (skillCategoryDrafts[skill.id] ?? skill.category ?? "other").trim(), + })); + await updateRoleSkills( + workspace, + roleName, + roleSkills.filter((skill) => skill.enabled).map((skill) => skill.id), + metadata, + ); + syncRoleSkills( + roleSkills.map((skill) => ({ + ...skill, + category: skill.missing + ? skill.category + : (skillCategoryDrafts[skill.id] ?? skill.category ?? "other").trim() || "other", + })), + ); + onSaved(); + } catch (err: unknown) { + setError(getErrorMessage(err)); + } finally { + setSaving(false); + } + }, [workspace, roleName, roleSkills, skillCategoryDrafts, onSaved, syncRoleSkills]); + + const toggleSkill = useCallback((id: string) => { + setRoleSkills((skills) => + skills.map((skill) => + skill.id === id ? { ...skill, enabled: !skill.enabled } : skill, + ), + ); + }, []); + + const updateSkillCategory = useCallback((id: string, value: string) => { + setSkillCategoryDrafts((drafts) => ({ + ...drafts, + [id]: value, + })); + }, []); + + const groupedRoleSkills = groupRoleSkills(roleSkills); + const skillCategoryLabels = copy.roleEditor.skillCategories as Record; + const tabItems = [ + { id: "role", label: copy.roleEditor.roleTab }, + { id: "roleSkills", label: copy.roleEditor.roleSkillsTab }, + ] as const; + const handleSave = tab === "roleSkills" ? handleSaveRoleSkills : handleSaveRole; + const saveLabel = saving + ? copy.roleEditor.saving + : tab === "roleSkills" + ? copy.roleEditor.saveSkills + : copy.roleEditor.saveRole; + + return ( + { + if (!nextOpen) { + onClose(); + } + }} + > + + + + + + + + + {copy.roleEditor.title(roleName)} + + + + + + )} + /> + + + )} + mainClassName="p-4" + footer={!loading ? ( + + + + + + + ) : undefined} + > + {loading ? ( +
{copy.roleEditor.loading}
+ ) : tab === "role" ? ( +
+ + + {name} + + + + + setDescription(e.target.value)} + placeholder={copy.roleEditor.descriptionPlaceholder} + /> + + + + setSortOrder(Number.parseInt(e.target.value || "0", 10) || 0)} + /> + + + setPrompt(e.target.value)} + placeholder={copy.roleEditor.promptPlaceholder} + textareaClassName="min-h-[320px] font-mono text-xs leading-relaxed" + /> + + setConfigToml(e.target.value)} + placeholder={copy.roleEditor.codexConfigPlaceholder} + textareaClassName="min-h-[240px] font-mono text-xs leading-relaxed" + /> + + setAuthJson(e.target.value)} + placeholder={copy.roleEditor.codexAuthPlaceholder} + textareaClassName="min-h-[180px] font-mono text-xs leading-relaxed" + /> +
+ ) : roleSkills.length === 0 ? ( + + {copy.roleEditor.skillsEmpty} + + ) : ( +
+ + {copy.roleEditor.skillsGroupingScope} + + {groupedRoleSkills.map((section) => ( +
+
+

+ {skillCategoryLabels[section.category] ?? formatSkillCategoryLabel(section.category)} +

+
+
+ {section.skills.map((skill) => ( + + ))} +
+
+ ))} +
+ )} +
+
+
+
+
+ ); +} diff --git a/dashboard/src/components/RoleStatus.test.tsx b/dashboard/src/components/RoleStatus.test.tsx new file mode 100644 index 0000000..abc7b42 --- /dev/null +++ b/dashboard/src/components/RoleStatus.test.tsx @@ -0,0 +1,144 @@ +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { usePolling } from "../hooks/usePolling"; +import { renderWithProviders } from "../test/renderWithProviders"; +import type { RoleInfo } from "../types"; +import RoleStatus from "./RoleStatus"; + +vi.mock("../hooks/usePolling", () => ({ + usePolling: vi.fn(), +})); + +const usePollingMock = vi.mocked(usePolling); + +function mockPollingState(overrides: Partial> = {}) { + const refresh = vi.fn(); + usePollingMock.mockReturnValue({ + data: null, + loading: false, + error: null, + refresh, + ...overrides, + } as ReturnType); + + return refresh; +} + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe("RoleStatus", () => { + it("orders roles by sort_order and renders status labels", () => { + const roles: RoleInfo[] = [ + { + name: "leader", + description: "Leader role", + sort_order: 100, + pending: 0, + session: { + role: "leader", + created_at: "2026-03-10T00:00:00Z", + last_used_at: "2026-03-10T00:30:00Z", + last_message: "Scope locked", + }, + }, + { + name: "worker", + description: "Worker role", + sort_order: 200, + pending: 0, + session: null, + }, + { + name: "worker-burst", + description: "Busy worker role", + sort_order: 300, + pending: 1, + session: { + role: "worker", + created_at: "2026-03-10T01:00:00Z", + last_used_at: "2026-03-10T01:05:00Z", + last_message: "Need review", + }, + }, + ]; + mockPollingState({ data: roles }); + + renderWithProviders(, { locale: "en" }); + + const workflowHeading = screen.getByText("Agents"); + expect(workflowHeading).toBeInTheDocument(); + + const busyWorkerLabel = screen.getByText("Busy worker role"); + const leaderLabel = screen.getByText("Leader", { exact: true }); + const workerLabel = screen.getByText("Worker", { exact: true }); + const busyWorkerRow = busyWorkerLabel.closest("div"); + const leaderRow = leaderLabel.closest("article"); + const workerRow = workerLabel.closest("div"); + + expect(busyWorkerRow).toBeTruthy(); + expect(leaderRow).toBeTruthy(); + expect(workerRow).toBeTruthy(); + expect( + leaderLabel.compareDocumentPosition(workerLabel) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + expect( + workerLabel.compareDocumentPosition(busyWorkerLabel) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + expect(busyWorkerRow).toHaveTextContent("Needs attention"); + expect(busyWorkerRow).toHaveTextContent("1 waiting"); + + expect(leaderRow).toHaveTextContent("Active"); + + expect(workerRow).toHaveTextContent("Not started"); + + expect(screen.getByText("Active agents")).toBeInTheDocument(); + expect(screen.getAllByText("Needs attention").length).toBeGreaterThan(0); + }); + + it("renders the empty state when there are no roles", () => { + mockPollingState({ data: [] }); + + renderWithProviders(, { locale: "en" }); + + expect(screen.getByText("No agents yet")).toBeInTheDocument(); + expect( + screen.getByText( + "Agent status appears here after this workspace starts running leader and worker threads.", + ), + ).toBeInTheDocument(); + }); + + it("retries after a polling error", async () => { + const refresh = mockPollingState({ error: "roles unavailable" }); + const user = userEvent.setup(); + + renderWithProviders(, { locale: "en" }); + + expect(screen.getByText("Couldn't load agent status")).toBeInTheDocument(); + await user.click(screen.getByRole("button", { name: "Retry" })); + + expect(refresh).toHaveBeenCalledTimes(1); + }); + + it("renders the workspace required state when no workspace is selected", () => { + mockPollingState({ + data: [{ + name: "leader", + description: "Leader role", + sort_order: 100, + pending: 0, + session: null, + }], + }); + + renderWithProviders(, { locale: "en" }); + + expect(screen.getByText("Select a project")).toBeInTheDocument(); + expect( + screen.getByText("Choose a project from the picker in the top-right corner to load agent status."), + ).toBeInTheDocument(); + }); +}); diff --git a/dashboard/src/components/RoleStatus.tsx b/dashboard/src/components/RoleStatus.tsx new file mode 100644 index 0000000..5f8a459 --- /dev/null +++ b/dashboard/src/components/RoleStatus.tsx @@ -0,0 +1,228 @@ +import { useCallback, useMemo, useState } from "react"; +import { usePolling } from "../hooks/usePolling"; +import { fetchRoles } from "../api/client"; +import type { RoleInfo } from "../types"; +import AsyncPageState from "./ui/AsyncPageState"; +import Button from "./ui/Button"; +import Chip from "./ui/Chip"; +import RoleBadge from "./RoleBadge"; +import PageHero from "./ui/PageHero"; +import PageSectionCard from "./ui/PageSectionCard"; +import SummaryStat from "./ui/SummaryStat"; +import StatusDot from "./ui/StatusDot"; +import RoleEditor from "./RoleEditor"; +import { useI18n } from "../i18n"; +import { roleListChangeKey } from "../utils/pollingKeys"; + +interface RoleStatusProps { + workspace: string; +} + +function getRoleState(role: RoleInfo, copy: ReturnType["copy"]) { + if (role.pending > 0) { + return { + label: copy.roleStatus.states.attention, + dotClassName: "bg-[color:var(--app-accent-warm)]", + textClassName: "text-[color:var(--app-accent-warm)]", + }; + } + + if (role.session) { + return { + label: copy.roleStatus.states.active, + dotClassName: "app-dot-success", + textClassName: "app-text-success", + }; + } + + return { + label: copy.roleStatus.states.idle, + dotClassName: "app-dot-idle", + textClassName: "app-text-idle", + }; +} + +function getRolePriority(role: RoleInfo) { + return (role.pending > 0 ? 2 : 0) + (role.session ? 1 : 0); +} + +export default function RoleStatus({ workspace }: RoleStatusProps) { + const { copy, formatAbsoluteDateTime, formatRelativeTime } = useI18n(); + const fetcher = useCallback(() => fetchRoles(workspace), [workspace]); + const { data, loading, error, refresh } = usePolling( + fetcher, + 5000, + { getChangeKey: roleListChangeKey }, + ); + + // Editor state: string = editing role name, undefined = closed + const [editorTarget, setEditorTarget] = useState(); + + const sortedRoles = useMemo(() => { + if (!data) return []; + return [...data].sort((left, right) => { + if (left.sort_order !== right.sort_order) { + return left.sort_order - right.sort_order; + } + const priorityDiff = getRolePriority(right) - getRolePriority(left); + if (priorityDiff !== 0) { + return priorityDiff; + } + return left.name.localeCompare(right.name); + }); + }, [data]); + + const activeSessions = data?.filter((role) => role.session).length ?? 0; + const attentionCount = data?.filter((role) => role.pending > 0).length ?? 0; + const idleCount = data ? data.length - activeSessions : 0; + const waitingMessages = data?.reduce((sum, role) => sum + role.pending, 0) ?? 0; + + return ( + 0)} + error={error} + loadingEyebrow={copy.roleStatus.eyebrow} + loadingTitle={copy.roleStatus.loadingTitle} + errorEyebrow={copy.roleStatus.eyebrow} + errorTitle={copy.roleStatus.errorTitle} + emptyEyebrow={copy.roleStatus.eyebrow} + emptyTitle={copy.roleStatus.emptyTitle} + emptyDetail={copy.roleStatus.emptyDetail} + retryLabel={copy.common.retry} + onRetry={refresh} + > +
+ + + + + +
+ } + /> + + {sortedRoles.length}} + className="rounded-[26px]" + headerClassName="px-4 py-4 sm:px-5" + bodyClassName="grid gap-3 p-4 sm:p-5 md:grid-cols-2" + > + {sortedRoles.map((role) => { + const state = getRoleState(role, copy); + + return ( +
+
+
+
+ + + + {state.label} + + {role.pending > 0 && ( + + {copy.roleStatus.states.waiting(role.pending)} + + )} +
+ +

+ {copy.roleStatus.conciseDescription[role.name as keyof typeof copy.roleStatus.conciseDescription] ?? role.description} +

+
+ + +
+ + {role.session?.last_message ? ( +

+ {role.session.last_message} +

+ ) : null} + +
+
+
{copy.roleStatus.lastActive}
+ {role.session ? ( +
+ {formatRelativeTime(role.session.last_used_at)} +
+ ) : ( +
{copy.common.notStarted}
+ )} +
+ +
+ #{role.sort_order} +
+
+
+ ); + })} +
+ + {/* Role Editor slide-out panel */} + {editorTarget !== undefined && ( + setEditorTarget(undefined)} + onSaved={() => { + setEditorTarget(undefined); + refresh(); + }} + /> + )} +
+
+ ); +} diff --git a/dashboard/src/components/SkillCatalogManager.test.tsx b/dashboard/src/components/SkillCatalogManager.test.tsx new file mode 100644 index 0000000..885b1cc --- /dev/null +++ b/dashboard/src/components/SkillCatalogManager.test.tsx @@ -0,0 +1,141 @@ +import { screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { deleteSkill, fetchSkills, upsertSkill } from "../api/client"; +import { renderWithProviders } from "../test/renderWithProviders"; +import SkillCatalogManager from "./SkillCatalogManager"; + +vi.mock("../api/client", () => ({ + fetchSkills: vi.fn(), + upsertSkill: vi.fn(), + deleteSkill: vi.fn(), +})); + +const fetchSkillsMock = vi.mocked(fetchSkills); +const upsertSkillMock = vi.mocked(upsertSkill); +const deleteSkillMock = vi.mocked(deleteSkill); + +function mockCatalogLoaded() { + fetchSkillsMock.mockResolvedValue([ + { + id: "adapt", + name: "adapt", + description: "Adapt designs.", + category: "adaptation", + sort_order: 10, + content: "---\nname: adapt\ndescription: Adapt designs.\n---\n", + }, + { + id: "animate", + name: "animate", + description: "Animate interfaces.", + category: "visual_design", + sort_order: 20, + content: "---\nname: animate\ndescription: Animate interfaces.\n---\n", + }, + ]); +} + +function renderSkillCatalog() { + renderWithProviders(, { locale: "en" }); +} + +afterEach(() => { + vi.resetAllMocks(); +}); + +describe("SkillCatalogManager", () => { + it("loads and saves the global skill catalog", async () => { + mockCatalogLoaded(); + upsertSkillMock.mockImplementation(async (skill) => ({ + id: skill.id ?? "adapt", + name: "adapt-plus", + description: "Adapt designs across breakpoints.", + category: skill.category, + sort_order: skill.sort_order, + content: skill.content, + })); + const user = userEvent.setup(); + + renderSkillCatalog(); + + expect((await screen.findAllByText("adapt")).length).toBeGreaterThan(0); + expect(screen.getByText("Skills in catalog")).toBeInTheDocument(); + + const contentInput = screen.getByLabelText("Skill Content"); + await user.clear(contentInput); + await user.type(contentInput, "---\nname: adapt-plus\ndescription: Adapt designs across breakpoints.\n---\n\nDo it."); + await user.click(screen.getByRole("button", { name: "Save Skill" })); + + await waitFor(() => { + expect(upsertSkillMock).toHaveBeenCalledWith({ + id: "adapt", + category: "adaptation", + sort_order: 10, + content: "---\nname: adapt-plus\ndescription: Adapt designs across breakpoints.\n---\n\nDo it.", + }); + }); + }); + + it("can start creating a new skill from the page", async () => { + mockCatalogLoaded(); + const user = userEvent.setup(); + + renderSkillCatalog(); + + await screen.findByLabelText("Skill Content"); + await user.click(screen.getByRole("button", { name: "New Skill" })); + + expect(screen.queryByLabelText("Path")).not.toBeInTheDocument(); + expect(screen.getByLabelText("Skill Content")).toHaveValue( + "---\nname: my-skill\ndescription: Brief summary shown in role assignment.\n---\n\nWrite the skill instructions here.", + ); + }); + + it("deletes an existing skill from the page", async () => { + fetchSkillsMock + .mockResolvedValueOnce([ + { + id: "adapt", + name: "adapt", + description: "Adapt designs.", + category: "adaptation", + sort_order: 10, + content: "---\nname: adapt\ndescription: Adapt designs.\n---\n", + }, + ]) + .mockResolvedValueOnce([]); + deleteSkillMock.mockResolvedValue(); + const user = userEvent.setup(); + + renderSkillCatalog(); + + await screen.findByLabelText("Skill Content"); + const deleteButton = await screen.findByRole("button", { name: "Delete Skill" }); + expect(deleteButton).toBeEnabled(); + + await user.click(deleteButton); + + await waitFor(() => { + expect(deleteSkillMock).toHaveBeenCalledWith("adapt"); + }); + }); + + it("shows an error state when the skills API is unavailable", async () => { + fetchSkillsMock.mockRejectedValue(new Error("404 page not found")); + + renderSkillCatalog(); + + expect(await screen.findByText("Couldn't load skills")).toBeInTheDocument(); + expect(screen.getByText("404 page not found")).toBeInTheDocument(); + }); + + it("loads the global skill catalog without any workspace context", async () => { + mockCatalogLoaded(); + + renderSkillCatalog(); + + expect((await screen.findAllByText("adapt")).length).toBeGreaterThan(0); + expect(screen.queryByText("Choose a workspace to view skill catalog.")).not.toBeInTheDocument(); + }); +}); diff --git a/dashboard/src/components/SkillCatalogManager.tsx b/dashboard/src/components/SkillCatalogManager.tsx new file mode 100644 index 0000000..b13bd66 --- /dev/null +++ b/dashboard/src/components/SkillCatalogManager.tsx @@ -0,0 +1,354 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { deleteSkill, fetchSkills, upsertSkill, type SkillCatalogSaveInput } from "../api/client"; +import { useI18n } from "../i18n"; +import type { SkillCatalogEntry } from "../types"; +import { extractSkillMetadata } from "../utils/skillMetadata"; +import AsyncPageState from "./ui/AsyncPageState"; +import Button from "./ui/Button"; +import CatalogSidebar from "./ui/CatalogSidebar"; +import Card from "./ui/Card"; +import EditorActionBar from "./ui/EditorActionBar"; +import EditorShell from "./ui/EditorShell"; +import FormField from "./ui/FormField"; +import InsetPanel from "./ui/InsetPanel"; +import PageHero from "./ui/PageHero"; +import SummaryStat from "./ui/SummaryStat"; +import TextInput from "./ui/TextInput"; +import TextareaField from "./ui/TextareaField"; + +const newSkillSelection = "__new_skill__"; + +function getErrorMessage(error: unknown) { + return error instanceof Error ? error.message : "Request failed."; +} + +function formatSkillCategoryLabel(category: string) { + return category + .replace(/[_-]+/g, " ") + .replace(/\b\w/g, (match) => match.toUpperCase()); +} + +function withParsedSkillMetadata(skill: SkillCatalogEntry): SkillCatalogEntry { + const parsed = extractSkillMetadata(skill.content); + return { + ...skill, + name: skill.content.trim() ? parsed.name : skill.name.trim(), + description: skill.content.trim() ? parsed.description : skill.description.trim(), + }; +} + +function makeEmptySkillDraft(template: string): SkillCatalogEntry { + return { + id: "", + name: "", + description: "", + category: "other", + sort_order: 1000, + content: template, + }; +} + +function normalizeSkillDraft(skill: SkillCatalogEntry): SkillCatalogSaveInput { + return { + id: skill.id.trim() || undefined, + category: skill.category.trim() || "other", + sort_order: skill.sort_order > 0 ? skill.sort_order : 1000, + content: skill.content, + }; +} + +export default function SkillCatalogManager() { + const { copy } = useI18n(); + const emptySkillDraft = useMemo( + () => withParsedSkillMetadata(makeEmptySkillDraft(copy.skillsCatalog.skillContentPlaceholder)), + [copy.skillsCatalog.skillContentPlaceholder], + ); + const [catalogSkills, setCatalogSkills] = useState([]); + const [catalogLoaded, setCatalogLoaded] = useState(false); + const [catalogLoading, setCatalogLoading] = useState(false); + const [selectedSkillId, setSelectedSkillId] = useState(newSkillSelection); + const [skillDraft, setSkillDraft] = useState(emptySkillDraft); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(""); + + const applyCatalogSelection = useCallback((id: string, skills: SkillCatalogEntry[]) => { + if (id === newSkillSelection) { + setSelectedSkillId(newSkillSelection); + setSkillDraft(emptySkillDraft); + return; + } + + const selected = skills.find((skill) => skill.id === id); + if (!selected) { + if (skills.length > 0) { + setSelectedSkillId(skills[0].id); + setSkillDraft(skills[0]); + return; + } + setSelectedSkillId(newSkillSelection); + setSkillDraft(emptySkillDraft); + return; + } + + setSelectedSkillId(selected.id); + setSkillDraft(selected); + }, [emptySkillDraft]); + + const loadSkillCatalog = useCallback(async (preferredSelection?: string) => { + setCatalogLoading(true); + setError(""); + try { + const skills = (await fetchSkills()).map(withParsedSkillMetadata); + setCatalogSkills(skills); + setCatalogLoaded(true); + const nextSelection = + preferredSelection && skills.some((skill) => skill.id === preferredSelection) + ? preferredSelection + : skills[0]?.id ?? newSkillSelection; + applyCatalogSelection(nextSelection, skills); + } catch (err) { + setCatalogLoaded(false); + setError(getErrorMessage(err)); + } finally { + setCatalogLoading(false); + } + }, [applyCatalogSelection]); + + useEffect(() => { + void loadSkillCatalog(); + }, [loadSkillCatalog]); + + const selectCatalogSkill = useCallback((id: string) => { + applyCatalogSelection(id, catalogSkills); + }, [applyCatalogSelection, catalogSkills]); + + const updateCatalogField = useCallback( + (field: keyof SkillCatalogEntry, value: SkillCatalogEntry[keyof SkillCatalogEntry]) => { + setSkillDraft((draft) => ({ + ...(field === "content" + ? withParsedSkillMetadata({ + ...draft, + content: String(value), + }) + : { + ...draft, + [field]: value, + }), + })); + }, + [], + ); + + const handleSaveCatalogSkill = useCallback(async () => { + setSaving(true); + setError(""); + try { + const saved = await upsertSkill(normalizeSkillDraft(skillDraft)); + await loadSkillCatalog(saved.id); + } catch (err) { + setError(getErrorMessage(err)); + } finally { + setSaving(false); + } + }, [loadSkillCatalog, skillDraft]); + + const handleDeleteCatalogSkill = useCallback(async () => { + if (selectedSkillId === newSkillSelection || !skillDraft.id.trim()) { + return; + } + setSaving(true); + setError(""); + try { + await deleteSkill(skillDraft.id.trim()); + await loadSkillCatalog(); + } catch (err) { + setError(getErrorMessage(err)); + } finally { + setSaving(false); + } + }, [loadSkillCatalog, selectedSkillId, skillDraft.id]); + + const handleResetDraft = useCallback(() => { + applyCatalogSelection(selectedSkillId, catalogSkills); + }, [applyCatalogSelection, catalogSkills, selectedSkillId]); + + const categoryCount = useMemo( + () => new Set(catalogSkills.map((skill) => skill.category.trim() || "other")).size, + [catalogSkills], + ); + const selectedExistingSkill = selectedSkillId !== newSkillSelection; + const skillCategoryLabels = copy.skillsCatalog.skillCategories as Record; + const fieldLabelClassName = "text-xs font-medium normal-case tracking-normal"; + const catalogItems = catalogSkills.map((skill) => ({ + id: skill.id, + title: skill.name, + description: ( + <> +

+ {skillCategoryLabels[skill.category] ?? formatSkillCategoryLabel(skill.category)} +

+

{skill.description}

+ + ), + })); + + return ( + void loadSkillCatalog()} + > +
+ + + + +
+ } + /> + + + selectCatalogSkill(newSkillSelection)} + empty={copy.skillsCatalog.catalogEmpty} + items={catalogItems} + value={selectedSkillId} + onChange={selectCatalogSkill} + /> + )} + footer={( + + {copy.skillsCatalog.deleteSkill} + + ) : undefined} + > + + + + )} + mainClassName="p-4 sm:p-5" + > +
+ + {copy.skillsCatalog.catalogScope} + + +
+ + +
+

+ {copy.skillsCatalog.skillNameLabel} +

+

+ {skillDraft.name || copy.skillsCatalog.skillNameMissing} +

+
+
+

+ {copy.skillsCatalog.skillDescriptionLabel} +

+

+ {skillDraft.description || copy.skillsCatalog.skillDescriptionMissing} +

+
+
+
+ + + + updateCatalogField("sort_order", Number.parseInt(e.target.value || "0", 10) || 0) + } + /> + +
+ + + updateCatalogField("category", e.target.value)} + placeholder={copy.skillsCatalog.skillGroupPlaceholder} + /> + + + updateCatalogField("content", e.target.value)} + placeholder={copy.skillsCatalog.skillContentPlaceholder} + spellCheck={false} + /> +
+
+
+ +
+ ); +} diff --git a/dashboard/src/components/ThemeSelector.test.tsx b/dashboard/src/components/ThemeSelector.test.tsx new file mode 100644 index 0000000..b9e69bb --- /dev/null +++ b/dashboard/src/components/ThemeSelector.test.tsx @@ -0,0 +1,33 @@ +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it } from "vitest"; +import ThemeSelector from "./ThemeSelector"; +import { renderWithProviders } from "../test/renderWithProviders"; + +describe("ThemeSelector", () => { + it("renders a labeled theme select and updates the active theme", async () => { + const user = userEvent.setup(); + + renderWithProviders(, { locale: "en" }); + + const select = screen.getByRole("combobox", { name: /theme/i }); + expect(select).toHaveValue("atelier-copper"); + + await user.selectOptions(select, "mist-blue"); + + expect(select).toHaveValue("mist-blue"); + }); + + it("shows all static themes in the picker", async () => { + renderWithProviders(, { locale: "en" }); + + const select = screen.getByRole("combobox", { name: /theme/i }); + expect(screen.getByRole("option", { name: "Atelier Copper" })).toBeInTheDocument(); + expect(screen.getByRole("option", { name: "Graphite Aqua" })).toBeInTheDocument(); + expect(screen.getByRole("option", { name: "Midnight Plum" })).toBeInTheDocument(); + expect(screen.getByRole("option", { name: "Ivory Brass" })).toBeInTheDocument(); + expect(screen.getByRole("option", { name: "Mist Blue" })).toBeInTheDocument(); + expect(screen.getByRole("option", { name: "Sage Paper" })).toBeInTheDocument(); + expect(select).toBeInTheDocument(); + }); +}); diff --git a/dashboard/src/components/ThemeSelector.tsx b/dashboard/src/components/ThemeSelector.tsx new file mode 100644 index 0000000..e4e4c63 --- /dev/null +++ b/dashboard/src/components/ThemeSelector.tsx @@ -0,0 +1,41 @@ +import { useId } from "react"; +import { themesByAppearance, useTheme } from "../theme"; +import { useI18n } from "../i18n"; + +export default function ThemeSelector() { + const { theme, setTheme } = useTheme(); + const { copy } = useI18n(); + const selectId = useId(); + + return ( +
+ + +
+ ); +} diff --git a/dashboard/src/components/WorkflowPrototype.test.tsx b/dashboard/src/components/WorkflowPrototype.test.tsx new file mode 100644 index 0000000..2d58bc8 --- /dev/null +++ b/dashboard/src/components/WorkflowPrototype.test.tsx @@ -0,0 +1,398 @@ +import { screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + answerHumanTask, + confirmTopicPlan, + createTopic, + deleteTopic, + fetchWorkflowBoard, + sendMessage, + stopTopic, +} from "../api/client"; +import type { WorkflowBoardResponse } from "../types"; +import { renderWithProviders } from "../test/renderWithProviders"; +import WorkflowPrototype from "./WorkflowPrototype"; + +vi.mock("../api/client", async () => { + const actual = await vi.importActual("../api/client"); + return { + ...actual, + fetchWorkflowBoard: vi.fn(), + createTopic: vi.fn(), + deleteTopic: vi.fn(), + sendMessage: vi.fn(), + answerHumanTask: vi.fn(), + confirmTopicPlan: vi.fn(), + stopTopic: vi.fn(), + }; +}); + +const fetchWorkflowBoardMock = vi.mocked(fetchWorkflowBoard); +const createTopicMock = vi.mocked(createTopic); +const deleteTopicMock = vi.mocked(deleteTopic); +const sendMessageMock = vi.mocked(sendMessage); +const answerHumanTaskMock = vi.mocked(answerHumanTask); +const confirmTopicPlanMock = vi.mocked(confirmTopicPlan); +const stopTopicMock = vi.mocked(stopTopic); + +function makeBoard(): WorkflowBoardResponse { + return { + topics: [ + { + name: "launch-dashboard", + status: "execution", + message_count: 3, + latest_stage: "execution", + latest_time: "2026-03-16T09:00:00Z", + running_roles: [], + waiting_roles: [], + }, + ], + active_topic: "launch-dashboard", + board: { + topic: { + name: "launch-dashboard", + latest_stage: "execution", + message_count: 3, + created_at: "2026-03-16T08:00:00Z", + updated_at: "2026-03-16T09:00:00Z", + status: "execution", + }, + plan: { + version: 2, + status: "active", + summary_markdown: "Build the operator-facing React console.\n\nThen verify it.", + created_at: "2026-03-16T08:05:00Z", + confirmed_at: "2026-03-16T08:08:00Z", + created_by_role_name: "leader", + }, + summary: { + running_count: 1, + waiting_count: 1, + active_roles: ["leader"], + last_event_at: "2026-03-16T09:00:00Z", + }, + agents: [ + { + name: "leader", + category: "workflow", + sort_order: 100, + description: "Leader", + pending_global: 0, + session_last_used_at: "2026-03-16T09:00:00Z", + state: "running", + latest_inbound_at: "2026-03-16T08:59:00Z", + latest_outbound_at: "2026-03-16T09:00:00Z", + latest_inbound_preview: "Need ship readiness.", + latest_outbound_preview: "Creating execution lanes.", + current_dispatch: null, + }, + ], + lanes: [ + { + id: "chain_1", + name: "UI Chain", + slug: "ui-chain", + purpose: "Build the operator-facing React console.", + status: "running", + branch_name: "chain/main/ui-chain", + worktree_path: "/tmp/ui-chain", + container_name: "chain-main-ui-chain", + runtime_endpoint: "http://127.0.0.1:40123", + started_at: "2026-03-16T08:10:00Z", + }, + ], + tasks: [ + { + id: "task_1", + lane_id: "chain_1", + title: "Build UI", + kind: "execution", + deliverables: ["apps/web/src", "apps/web/package.json"], + batch_key: "ui-core", + status: "running", + priority: 10, + task_order: 1, + dependencies: [], + }, + { + id: "task_2", + lane_id: "chain_1", + title: "Verify UI", + kind: "verification", + deliverables: ["reports/ui-check.md"], + batch_key: "ui-verify", + status: "draft", + priority: 5, + task_order: 2, + dependencies: [{ depends_on_task_id: "task_1" }], + }, + ], + links: [], + events: [ + { + kind: "message", + id: "msg_1", + timestamp: "2026-03-16T08:59:00Z", + from: "user", + to: "leader", + stage: "plan", + type: "chat", + body: "Need a clean leader console.", + }, + { + kind: "message", + id: "msg_2", + timestamp: "2026-03-16T09:00:00Z", + from: "worker", + to: "leader", + stage: "execution", + type: "summary", + body: "UI chain is in progress.", + }, + { + kind: "dispatch", + id: "run_1", + timestamp: "2026-03-16T09:00:00Z", + role: "worker", + stage: "execution", + mode: "task", + running: true, + started_at: "2026-03-16T08:10:00Z", + completed_at: "", + exit_code: 0, + reply: "", + error_message: "", + }, + ], + pending_human_tasks: [], + }, + }; +} + +afterEach(() => { + vi.resetAllMocks(); +}); + +describe("WorkflowPrototype", () => { + it("renders the leader console with chains and tasks", async () => { + fetchWorkflowBoardMock.mockResolvedValue(makeBoard()); + const user = userEvent.setup(); + + renderWithProviders(, { + route: "/workspaces/alpha/workflow/launch-dashboard", + locale: "en", + }); + + expect(await screen.findByText("Leader Console")).toBeInTheDocument(); + expect(screen.getAllByText("UI Chain")).toHaveLength(1); + expect(screen.getAllByText("Execution map").length).toBeGreaterThan(0); + expect(screen.getByText(/Build the operator-facing React console\./)).toBeInTheDocument(); + expect(screen.getByText("Build UI")).toBeInTheDocument(); + expect(screen.getByText("Verify UI")).toBeInTheDocument(); + expect(screen.getByText("apps/web/src")).toBeInTheDocument(); + expect(screen.getByText(/worktree .*ui-chain/)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Open timeline (3)" })).toBeInTheDocument(); + expect(screen.queryByText("Need a clean leader console.")).not.toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "Open timeline (3)" })); + + expect(await screen.findByText("Need a clean leader console.")).toBeInTheDocument(); + expect(screen.getAllByText("UI chain is in progress.").length).toBeGreaterThan(0); + }); + + it("creates a workflow topic from the leader console", async () => { + fetchWorkflowBoardMock + .mockResolvedValueOnce({ topics: [], active_topic: null, board: null }) + .mockResolvedValue(makeBoard()); + createTopicMock.mockResolvedValue({ + id: "topic_1", + name: "release-prep-2026", + space: "workflow", + status: "execution", + created_at: "2026-03-16T09:10:00Z", + updated_at: "2026-03-16T09:10:00Z", + }); + const user = userEvent.setup(); + renderWithProviders(, { + route: "/workspaces/alpha/workflow", + locale: "en", + }); + + await user.click(await screen.findByRole("button", { name: /start first topic/i })); + await user.type(screen.getByLabelText("Topic name"), "Release Prep 2026"); + await user.keyboard("{Enter}"); + + await waitFor(() => { + expect(createTopicMock).toHaveBeenCalledWith("alpha", "release-prep-2026", "workflow"); + }); + }); + + it("sends a new message to leader", async () => { + fetchWorkflowBoardMock.mockResolvedValue(makeBoard()); + sendMessageMock.mockResolvedValue({ status: "delivered", file: "msg_3" }); + + const user = userEvent.setup(); + renderWithProviders(, { + route: "/workspaces/alpha/workflow/launch-dashboard", + locale: "en", + }); + + await user.type(await screen.findByPlaceholderText("Outline the next decision, handoff, or build request..."), "Ship the final UI pass."); + await user.click(screen.getByRole("button", { name: "Send update" })); + + await waitFor(() => { + expect(sendMessageMock).toHaveBeenCalledWith({ + workspace: "alpha", + to: "leader", + topic: "launch-dashboard", + body: "Ship the final UI pass.", + stage: "plan", + }); + }); + }); + + it("submits a pending human task", async () => { + const board = makeBoard(); + board.board!.pending_human_tasks = [ + { + id: "human-task-1", + role_name: "user", + status: "pending", + prompt_message_id: "msg_99", + prompt_from: "leader", + prompt_stage: "plan", + prompt_body: "Which deploy window should this chain target?", + created_at: "2026-03-16T09:00:00Z", + updated_at: "2026-03-16T09:00:00Z", + }, + ]; + fetchWorkflowBoardMock.mockResolvedValue(board); + answerHumanTaskMock.mockResolvedValue(); + + const user = userEvent.setup(); + renderWithProviders(, { + route: "/workspaces/alpha/workflow/launch-dashboard", + locale: "en", + }); + + const [humanTaskInput] = await screen.findAllByRole("textbox"); + await user.type(humanTaskInput, "Tonight after 10 PM."); + await user.click(screen.getByRole("button", { name: "Send answer" })); + + await waitFor(() => { + expect(answerHumanTaskMock).toHaveBeenCalledWith("human-task-1", "Tonight after 10 PM."); + }); + }); + + it("deletes a topic after confirmation", async () => { + fetchWorkflowBoardMock.mockResolvedValue(makeBoard()); + deleteTopicMock.mockResolvedValue(); + + const user = userEvent.setup(); + renderWithProviders(, { + route: "/workspaces/alpha/workflow/launch-dashboard", + locale: "en", + }); + + await user.click(await screen.findByRole("button", { name: "Delete topic" })); + expect(await screen.findByText("Delete this topic?")).toBeInTheDocument(); + await user.click(screen.getByRole("button", { name: "Confirm delete" })); + + await waitFor(() => { + expect(deleteTopicMock).toHaveBeenCalledWith("alpha", "launch-dashboard"); + }); + }); + + it("stops a topic from the detail header", async () => { + fetchWorkflowBoardMock.mockResolvedValue(makeBoard()); + stopTopicMock.mockResolvedValue(); + + const user = userEvent.setup(); + renderWithProviders(, { + route: "/workspaces/alpha/workflow/launch-dashboard", + locale: "en", + }); + + await user.click(await screen.findByRole("button", { name: "Stop topic" })); + expect(await screen.findByText("Stop this topic?")).toBeInTheDocument(); + await user.click(screen.getByRole("button", { name: "Confirm stop" })); + + await waitFor(() => { + expect(stopTopicMock).toHaveBeenCalledWith("alpha", "launch-dashboard"); + }); + }); + + it("disables compose for a cancelled topic", async () => { + const cancelledBoard = makeBoard(); + cancelledBoard.board!.topic.status = "cancelled"; + fetchWorkflowBoardMock.mockResolvedValue(cancelledBoard); + renderWithProviders(, { + route: "/workspaces/alpha/workflow/launch-dashboard", + locale: "en", + }); + + expect(await screen.findByRole("button", { name: "Stopped" })).toBeDisabled(); + expect(screen.getByPlaceholderText("Name the topic first...")).toBeDisabled(); + }); + + it("shows a stopped marker in the topic rail", async () => { + const board = makeBoard(); + board.topics[0].status = "cancelled"; + fetchWorkflowBoardMock.mockResolvedValue(board); + + renderWithProviders(, { + route: "/workspaces/alpha/workflow/launch-dashboard", + locale: "en", + }); + + expect(await screen.findAllByText("Stopped")).toHaveLength(1); + }); + + it("confirms an awaiting plan from the execution map", async () => { + const board = makeBoard(); + board.topics = [ + { + ...board.topics[0], + status: "awaiting_confirmation", + }, + { + name: "approval-topic", + status: "awaiting_confirmation", + message_count: 1, + latest_stage: "plan", + latest_time: "2026-03-16T10:00:00Z", + running_roles: [], + waiting_roles: ["leader"], + }, + ]; + board.board!.topic.status = "awaiting_confirmation"; + board.board!.plan = { + version: 3, + status: "draft", + summary_markdown: "Freeze the plan before execution.\n\n1. Create the lanes.\n2. Confirm and start.", + created_at: "2026-03-16T09:40:00Z", + created_by_role_name: "leader", + }; + fetchWorkflowBoardMock.mockResolvedValue(board); + confirmTopicPlanMock.mockResolvedValue(); + + const user = userEvent.setup(); + renderWithProviders(, { + route: "/workspaces/alpha/workflow/launch-dashboard", + locale: "en", + }); + + expect(await screen.findAllByText("Execution map")).toHaveLength(2); + expect(screen.getByText("Plan v3")).toBeInTheDocument(); + expect(screen.getByText("Prepared by leader")).toBeInTheDocument(); + expect(screen.queryByText("Review the frozen plan before execution starts")).not.toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "Confirm plan" })); + + await waitFor(() => { + expect(confirmTopicPlanMock).toHaveBeenCalledWith("alpha", "launch-dashboard"); + }); + }); +}); diff --git a/dashboard/src/components/WorkflowPrototype.tsx b/dashboard/src/components/WorkflowPrototype.tsx new file mode 100644 index 0000000..c5ed2b4 --- /dev/null +++ b/dashboard/src/components/WorkflowPrototype.tsx @@ -0,0 +1,412 @@ +import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useLocation, useNavigate } from "react-router"; +import { + confirmTopicPlan, + createTopic, + deleteTopic, + fetchWorkflowBoard, + stopTopic, +} from "../api/client"; +import type { + WorkflowBoardResponse, +} from "../types"; +import { usePolling } from "../hooks/usePolling"; +import AsyncPageState from "./ui/AsyncPageState"; +import AlertBanner from "./ui/AlertBanner"; +import Button from "./ui/Button"; +import ConfirmDialog from "./ui/ConfirmDialog"; +import InlineComposer from "./ui/InlineComposer"; +import StatusBadge from "./ui/StatusBadge"; +import ViewState from "./ui/ViewState"; +import PageHero from "./ui/PageHero"; +import { useI18n } from "../i18n"; +import { workflowBoardChangeKey } from "../utils/pollingKeys"; +import { + getErrorMessage, + isExistingTopicError, + slugifyTopicName, +} from "../utils/topics"; +import { buildWorkflowHref, getTopicFromPathname } from "../routes"; +import { getConsoleCopy } from "./workflow-prototype/copy"; +import { + taskStats, +} from "./workflow-prototype/helpers"; +import { + ComposeBar, + ExecutionMapSection, + HumanTaskCard, + TimelineDrawer, + TopicItem, +} from "./workflow-prototype/sections"; + +interface WorkflowPrototypeProps { + workspace: string; +} + +export default function WorkflowPrototype({ workspace }: WorkflowPrototypeProps) { + const { copy, locale } = useI18n(); + const consoleCopy = useMemo(() => getConsoleCopy(locale), [locale]); + const location = useLocation(); + const navigate = useNavigate(); + const topicParam = getTopicFromPathname(location.pathname); + const [creatingNew, setCreatingNew] = useState(false); + const [newTopicName, setNewTopicName] = useState(""); + const [createTopicError, setCreateTopicError] = useState(""); + const [topicActionError, setTopicActionError] = useState(""); + const [deleteDialogTopic, setDeleteDialogTopic] = useState(null); + const [deletingTopic, setDeletingTopic] = useState(false); + const [confirmingPlan, setConfirmingPlan] = useState(false); + const [stoppingTopic, setStoppingTopic] = useState(false); + const [timelineOpen, setTimelineOpen] = useState(false); + const [stopDialogOpen, setStopDialogOpen] = useState(false); + const newTopicInputRef = useRef(null); + + const boardFetcher = useCallback( + () => fetchWorkflowBoard(workspace, topicParam || undefined), + [workspace, topicParam], + ); + const { data, loading, error, refresh } = usePolling( + workspace ? boardFetcher : null, + 5000, + { getChangeKey: workflowBoardChangeKey }, + ); + + const topics = data?.topics ?? []; + const board = data?.board ?? null; + const activeTopic = creatingNew ? slugifyTopicName(newTopicName) : data?.active_topic ?? null; + const primaryHumanTask = board?.pending_human_tasks?.[0] ?? null; + + useEffect(() => { + if (!creatingNew && !topicParam && data?.active_topic) { + startTransition(() => navigate(buildWorkflowHref({ + workspace, + topic: data.active_topic ?? undefined, + }), { replace: true })); + } + }, [creatingNew, data?.active_topic, navigate, topicParam, workspace]); + + useEffect(() => { + setTimelineOpen(false); + }, [activeTopic]); + + const handleSelectTopic = (topic: string) => { + setCreatingNew(false); + setNewTopicName(""); + setCreateTopicError(""); + setTopicActionError(""); + setDeleteDialogTopic(null); + setStopDialogOpen(false); + startTransition(() => navigate(buildWorkflowHref({ workspace, topic }))); + }; + + const handleNewTopic = () => { + setCreatingNew(true); + setNewTopicName(""); + setCreateTopicError(""); + setTopicActionError(""); + setDeleteDialogTopic(null); + setStopDialogOpen(false); + window.setTimeout(() => newTopicInputRef.current?.focus(), 50); + }; + + const handleRequestDeleteTopic = (topic: string) => { + if (deletingTopic) return; + setTopicActionError(""); + setDeleteDialogTopic(topic); + }; + + const handleConfirmNewTopic = async () => { + const slug = slugifyTopicName(newTopicName); + if (!slug || !workspace) return; + setCreateTopicError(""); + try { + await createTopic(workspace, slug, "workflow"); + setCreatingNew(false); + setNewTopicName(""); + handleSelectTopic(slug); + refresh(); + } catch (error) { + if (isExistingTopicError(error)) { + setCreatingNew(false); + setNewTopicName(""); + handleSelectTopic(slug); + refresh(); + return; + } + setCreateTopicError(getErrorMessage(error, copy.workflow.createTopicFailed)); + } + }; + + const handleDeleteTopic = async () => { + if (!workspace || deletingTopic || !deleteDialogTopic) return; + setDeletingTopic(true); + setTopicActionError(""); + try { + await deleteTopic(workspace, deleteDialogTopic); + setDeleteDialogTopic(null); + startTransition(() => navigate(buildWorkflowHref({ workspace }), { replace: true })); + refresh(); + } catch (error) { + setTopicActionError(getErrorMessage(error, copy.workflow.deleteTopicFailed)); + } finally { + setDeletingTopic(false); + } + }; + + const handleStopTopic = async (topic: string) => { + if (!workspace || stoppingTopic) return; + setStoppingTopic(true); + setTopicActionError(""); + try { + await stopTopic(workspace, topic); + setStopDialogOpen(false); + refresh(); + } catch (error) { + setTopicActionError(getErrorMessage(error, copy.workflow.stopTopicFailed)); + } finally { + setStoppingTopic(false); + } + }; + + const handleConfirmPlan = async (topic: string) => { + if (!workspace || confirmingPlan) return; + setConfirmingPlan(true); + setTopicActionError(""); + try { + await confirmTopicPlan(workspace, topic); + refresh(); + } catch (error) { + setTopicActionError(getErrorMessage(error, copy.workflow.confirmPlanFailed)); + } finally { + setConfirmingPlan(false); + } + }; + + const lanes = board?.lanes ?? []; + const tasks = board?.tasks ?? []; + const activityItems = (board?.events ?? []).slice().sort((left, right) => + right.timestamp.localeCompare(left.timestamp), + ); + const stats = taskStats(tasks); + const topicStopped = board?.topic.status === "cancelled"; + const topicAwaitingConfirmation = board?.topic.status === "awaiting_confirmation"; + + return ( + +
+ + +
+ {!activeTopic ? ( + {consoleCopy.startTopic}} + align="start" + surface + size="lg" + className="flex-1" + /> + ) : ( +
+ + {copy.workflow.topicStatus(board.topic.status)} + + ) : null + } + headerActions={ +
+ + +
+ } + footer={ +
+
+

{consoleCopy.summaryLanes}

+

{lanes.length}

+
+
+

{consoleCopy.summaryTasks}

+

{tasks.length}

+
+
+

{consoleCopy.summaryRunning}

+

{stats.running}

+
+
+

{consoleCopy.summaryWaiting}

+

{stats.waiting}

+
+
+ } + /> + + {primaryHumanTask ? : null} + +
+ + {board?.plan?.version ? ( + + {copy.workflow.planReview.versionLabel(board.plan.version)} + + ) : null} + {board?.plan?.created_by_role_name ? ( + + {copy.workflow.planReview.createdByLabel(board.plan.created_by_role_name)} + + ) : null} + +
+ ) : undefined} + /> + +
+
+ )} + + + { + if (!open) { + setDeleteDialogTopic(null); + } + }} + title={copy.workflow.deleteTopicDialogTitle} + description={copy.workflow.deleteTopicDialogDetail} + confirmLabel={deletingTopic ? copy.workflow.deletingTopic : copy.workflow.confirmDeleteTopic} + cancelLabel={copy.common.cancel} + confirmTone="danger" + busy={deletingTopic} + onConfirm={() => void handleDeleteTopic()} + /> + {activeTopic ? ( + void handleStopTopic(activeTopic)} + /> + ) : null} + +
+ ); +} diff --git a/dashboard/src/components/WorkspaceSelector.test.tsx b/dashboard/src/components/WorkspaceSelector.test.tsx new file mode 100644 index 0000000..b9df261 --- /dev/null +++ b/dashboard/src/components/WorkspaceSelector.test.tsx @@ -0,0 +1,167 @@ +import { screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import { renderWithProviders } from "../test/renderWithProviders"; +import WorkspaceSelector from "./WorkspaceSelector"; + +function makeWorkspace(name: string, overrides: Record = {}) { + return { + id: `ws-${name}`, + name, + slug: name, + path: `/workspaces/${name}`, + runtime_backend: "container", + container_name: `codex-${name}`, + status: "active", + provision_state: "ready", + provision_error: "", + last_provisioned_at: "", + container_state: "running", + ...overrides, + }; +} + +describe("WorkspaceSelector", () => { + it("shows a loading label while workspaces are being resolved", () => { + renderWithProviders( + , + { locale: "en" }, + ); + + expect(screen.getByRole("button", { name: /loading workspaces/i })).toBeInTheDocument(); + }); + + it("lets the user switch to another existing workspace", async () => { + const onChange = vi.fn(); + const user = userEvent.setup(); + + renderWithProviders( + , + { locale: "en" }, + ); + + await user.click(await screen.findByRole("button", { name: /alpha/i })); + + const betaOption = (await screen.findByText("beta")).closest("button"); + if (!betaOption) { + throw new Error("beta workspace option not found"); + } + + await user.click(betaOption); + + expect(onChange).toHaveBeenCalledWith("beta"); + }); + + it("updates the trigger label when the active workspace prop changes", async () => { + const { rerender } = renderWithProviders( + , + { locale: "en" }, + ); + + expect(await screen.findByRole("button", { name: /alpha/i })).toBeInTheDocument(); + + rerender( + , + ); + + expect(await screen.findByRole("button", { name: /beta/i })).toBeInTheDocument(); + }); + + it("opens a labeled dialog, focuses the active project, and restores focus on escape", async () => { + const user = userEvent.setup(); + + renderWithProviders( + , + { locale: "en" }, + ); + + const trigger = await screen.findByRole("button", { name: /alpha/i }); + expect(trigger).toHaveAttribute("aria-expanded", "false"); + + await user.click(trigger); + expect(trigger).toHaveAttribute("aria-expanded", "true"); + + const dialog = await screen.findByRole("dialog", { name: "Workspaces" }); + await waitFor(() => { + expect(within(dialog).getByRole("button", { name: /alpha/i })).toHaveFocus(); + }); + + for (let i = 0; i < 4; i += 1) { + await user.tab(); + expect(dialog.contains(document.activeElement)).toBe(true); + } + + await user.keyboard("{Escape}"); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + await waitFor(() => { + expect(trigger).toHaveFocus(); + }); + expect(trigger).toHaveAttribute("aria-expanded", "false"); + }); + + it("keeps the current workspace selected when the next workspace has no id", async () => { + const onChange = vi.fn(); + const user = userEvent.setup(); + + renderWithProviders( + , + { locale: "en" }, + ); + + await user.click(await screen.findByRole("button", { name: /alpha/i })); + + const betaOption = (await screen.findByText("beta")).closest("button"); + if (!betaOption) { + throw new Error("beta workspace option not found"); + } + + await user.click(betaOption); + + expect(await screen.findByText("Couldn't prepare the workspace runtime.")).toBeInTheDocument(); + expect(onChange).not.toHaveBeenCalled(); + }); + + it("shows a header-managed empty state when no workspaces exist", async () => { + const user = userEvent.setup(); + + renderWithProviders( + , + { locale: "en" }, + ); + + await user.click(await screen.findByRole("button", { name: /no workspaces/i })); + + expect(await screen.findByText("No workspaces yet")).toBeInTheDocument(); + expect(screen.getByText("Workspaces must be created outside the header picker.")).toBeInTheDocument(); + }); +}); diff --git a/dashboard/src/components/WorkspaceSelector.tsx b/dashboard/src/components/WorkspaceSelector.tsx new file mode 100644 index 0000000..6a219ae --- /dev/null +++ b/dashboard/src/components/WorkspaceSelector.tsx @@ -0,0 +1,276 @@ +import { useCallback, useEffect, useId, useRef, useState } from "react"; +import { AnimatePresence, motion, useReducedMotion } from "framer-motion"; +import type { Workspace } from "../types"; +import Button from "./ui/Button"; +import PanelHeader from "./ui/PanelHeader"; +import { + Popover, + PopoverContent, + PopoverPortal, + PopoverTrigger, +} from "./ui/Popover"; +import StatusDot from "./ui/StatusDot"; +import { useI18n } from "../i18n"; +import { + fadeScale, + fadeUp, + motionDuration, + motionEase, + motionTransition, +} from "../utils/motion"; +import { summarizeWorkspacePath } from "../utils/workspace"; +import { + workspaceStatusClassName, + workspaceStatusLabel, +} from "./workspace-selector/helpers"; + +interface WorkspaceSelectorProps { + value: string; + workspaces: Workspace[]; + loading?: boolean; + error?: string | null; + onChange: (ws: string) => void; +} + +export default function WorkspaceSelector({ + value, + workspaces, + loading = false, + error = null, + onChange, +}: WorkspaceSelectorProps) { + const { copy } = useI18n(); + const [open, setOpen] = useState(false); + const [selectionErr, setSelectionErr] = useState(""); + const triggerRef = useRef(null); + const panelRef = useRef(null); + const prefersReducedMotion = useReducedMotion() ?? false; + const panelId = useId(); + const titleId = useId(); + + const focusPanelTarget = useCallback(() => { + const panel = panelRef.current; + if (!panel) return; + + const target = + panel.querySelector("[data-active-workspace='true']") + ?? panel.querySelector("[data-workspace-option='true']"); + + (target ?? panel).focus(); + }, []); + + useEffect(() => { + if (!open) return; + + const focusHandle = window.setTimeout(() => { + focusPanelTarget(); + }, 0); + + return () => window.clearTimeout(focusHandle); + }, [focusPanelTarget, open, value, workspaces.length]); + + const current = workspaces.find((workspace) => workspace.name === value) ?? null; + const handleWorkspaceSelect = useCallback((nextWorkspace: Workspace) => { + if (nextWorkspace.name === value) { + setOpen(false); + return; + } + if (!nextWorkspace.id) { + setSelectionErr(copy.workspaceSelector.ensureWorkspaceError); + return; + } + + setSelectionErr(""); + onChange(nextWorkspace.name); + setOpen(false); + }, [copy.workspaceSelector.ensureWorkspaceError, onChange, value]); + + const currentPathSummary = summarizeWorkspacePath( + current?.path, + 4, + copy.workspaceSelector.noProjectPath, + ); + const currentLabel = current?.name + || (loading + ? copy.workspaceSelector.loadingProjects + : value + ? copy.workspaceSelector.missingProject(value) + : workspaces.length === 0 + ? copy.workspaceSelector.noProjects + : copy.workspaceSelector.chooseProject); + + if (error) { + return {copy.workspaceSelector.unavailable}; + } + + return ( + +
+ + + + + + + {open ? ( + + { + event.preventDefault(); + }} + onCloseAutoFocus={(event) => { + event.preventDefault(); + window.setTimeout(() => triggerRef.current?.focus(), 0); + }} + > + + setOpen(false)} + > + {copy.common.close} + + )} + /> + {selectionErr ? ( +
+
+ {selectionErr} +
+
+ ) : null} +
+ {workspaces.map((workspace, index) => { + const active = workspace.name === value; + return ( + handleWorkspaceSelect(workspace)} + initial={fadeUp(prefersReducedMotion, 6)} + animate={{ opacity: 1, y: 0 }} + transition={ + prefersReducedMotion + ? { duration: 0 } + : { + duration: motionDuration.fast, + ease: motionEase.smooth, + delay: Math.min(index * 0.03, 0.12), + } + } + className={`flex w-full items-start gap-2.5 px-3 py-2.5 text-left text-sm transition-colors ${ + active + ? "bg-[color:var(--app-surface-muted)] app-text-primary" + : "app-text-muted hover:bg-[color:var(--app-surface-muted)] hover:text-[color:var(--app-text)]" + }`} + > + +
+
+
{workspace.name}
+ + {workspaceStatusLabel(workspace, copy)} + +
+
+ {summarizeWorkspacePath(workspace.path, 4, copy.workspaceSelector.noProjectPath)} +
+
+ {active ? ( + + + + ) : null} +
+ ); + })} + {workspaces.length === 0 ? ( +
+
+

{copy.workspaceSelector.noProjectsYet}

+

+ Workspaces must be created outside the header picker. +

+
+
+ ) : null} +
+
+
+
+ ) : null} +
+
+
+ ); +} diff --git a/dashboard/src/components/executions/ExecutionRunNote.tsx b/dashboard/src/components/executions/ExecutionRunNote.tsx new file mode 100644 index 0000000..45391b9 --- /dev/null +++ b/dashboard/src/components/executions/ExecutionRunNote.tsx @@ -0,0 +1,35 @@ +import InsetPanel from "../ui/InsetPanel"; + +interface ExecutionRunNoteProps { + note: string; + isError?: boolean; + id?: string; + className?: string; + runNoteLabel: string; + replyLabel: string; +} + +export default function ExecutionRunNote({ + note, + isError = false, + id, + className, + runNoteLabel, + replyLabel, +}: ExecutionRunNoteProps) { + return ( + +

+ {isError ? runNoteLabel : replyLabel} +

+

+ {note} +

+
+ ); +} diff --git a/dashboard/src/components/executions/ExecutionRunSummary.tsx b/dashboard/src/components/executions/ExecutionRunSummary.tsx new file mode 100644 index 0000000..79861c1 --- /dev/null +++ b/dashboard/src/components/executions/ExecutionRunSummary.tsx @@ -0,0 +1,28 @@ +import type { ReactNode } from "react"; + +interface ExecutionRunSummaryField { + label: ReactNode; + value: ReactNode; + fullWidth?: boolean; +} + +interface ExecutionRunSummaryProps { + fields: ExecutionRunSummaryField[]; + className?: string; +} + +export default function ExecutionRunSummary({ + fields, + className, +}: ExecutionRunSummaryProps) { + return ( +
+ {fields.map((field, index) => ( +
+
{field.label}
+
{field.value}
+
+ ))} +
+ ); +} diff --git a/dashboard/src/components/ui/AlertBanner.tsx b/dashboard/src/components/ui/AlertBanner.tsx new file mode 100644 index 0000000..89dd90b --- /dev/null +++ b/dashboard/src/components/ui/AlertBanner.tsx @@ -0,0 +1,69 @@ +import type { ReactNode } from "react"; +import Button from "./Button"; +import { cx } from "./cx"; + +type AlertBannerTone = "neutral" | "danger" | "attention"; + +const toneClasses: Record = { + neutral: + "border-[color:var(--app-divider-soft)] bg-[color:var(--app-surface-muted)]", + danger: + "border-[color:var(--app-danger-border)] bg-[color:var(--app-danger-background)] text-[color:var(--app-danger-text)]", + attention: + "border-[color:var(--app-attention-border)] bg-[color:var(--app-attention-background)] text-[color:var(--app-attention-text)]", +}; + +interface AlertBannerProps { + title?: ReactNode; + detail: ReactNode; + actionLabel?: ReactNode; + onAction?: () => void; + action?: ReactNode; + tone?: AlertBannerTone; + className?: string; + titleClassName?: string; + detailClassName?: string; +} + +export default function AlertBanner({ + title, + detail, + actionLabel, + onAction, + action, + tone = "danger", + className, + titleClassName, + detailClassName, +}: AlertBannerProps) { + const actionNode = action ?? ( + actionLabel && onAction + ? ( + + ) + : null + ); + + return ( +
+
+
+ {title ? ( +

{title}

+ ) : null} +
{detail}
+
+ {actionNode} +
+
+ ); +} diff --git a/dashboard/src/components/ui/AsyncDisclosureList.tsx b/dashboard/src/components/ui/AsyncDisclosureList.tsx new file mode 100644 index 0000000..181cd0a --- /dev/null +++ b/dashboard/src/components/ui/AsyncDisclosureList.tsx @@ -0,0 +1,72 @@ +import type { ReactNode } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import Button from "./Button"; +import AlertBanner from "./AlertBanner"; +import { cx } from "./cx"; + +interface AsyncDisclosureListProps { + trigger: ReactNode; + expanded: boolean; + loading?: boolean; + error?: ReactNode; + retryLabel?: ReactNode; + onToggle: () => void; + onRetry?: () => void; + children: ReactNode; + className?: string; +} + +export default function AsyncDisclosureList({ + trigger, + expanded, + loading = false, + error, + retryLabel, + onToggle, + onRetry, + children, + className, +}: AsyncDisclosureListProps) { + return ( +
+ + {error ? ( + + ) : null} + + {expanded ? ( + + {children} + + ) : null} + +
+ ); +} diff --git a/dashboard/src/components/ui/AsyncPageState.test.tsx b/dashboard/src/components/ui/AsyncPageState.test.tsx new file mode 100644 index 0000000..fa82cf8 --- /dev/null +++ b/dashboard/src/components/ui/AsyncPageState.test.tsx @@ -0,0 +1,55 @@ +import { screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { renderWithProviders } from "../../test/renderWithProviders"; +import AsyncPageState from "./AsyncPageState"; + +describe("AsyncPageState", () => { + it("renders workspace required state when the subject is provided without a workspace", () => { + renderWithProviders( + +
ready
+
, + { locale: "en" }, + ); + + expect(screen.getByText("Select a project")).toBeInTheDocument(); + }); + + it("renders the error state with a retry action", () => { + const onRetry = vi.fn(); + + renderWithProviders( + +
ready
+
, + ); + + expect(screen.getByText("Couldn't load runs")).toBeInTheDocument(); + screen.getByRole("button", { name: "Retry" }).click(); + expect(onRetry).toHaveBeenCalledTimes(1); + }); + + it("renders children when the async state is resolved", () => { + renderWithProviders( + +
ready
+
, + ); + + expect(screen.getByText("ready")).toBeInTheDocument(); + }); +}); diff --git a/dashboard/src/components/ui/AsyncPageState.tsx b/dashboard/src/components/ui/AsyncPageState.tsx new file mode 100644 index 0000000..b0773ff --- /dev/null +++ b/dashboard/src/components/ui/AsyncPageState.tsx @@ -0,0 +1,96 @@ +import type { ReactNode } from "react"; +import Button from "./Button"; +import ViewState from "./ViewState"; +import WorkspaceRequiredState from "./WorkspaceRequiredState"; + +interface AsyncPageStateProps { + children: ReactNode; + workspace?: string; + workspaceSubject?: string; + loading: boolean; + loadingMode?: "always" | "initial"; + hasData: boolean; + error?: ReactNode; + loadingEyebrow?: ReactNode; + loadingTitle?: ReactNode; + errorEyebrow?: ReactNode; + errorTitle?: ReactNode; + emptyEyebrow?: ReactNode; + emptyTitle?: ReactNode; + emptyDetail?: ReactNode; + emptyAction?: ReactNode; + retryLabel?: ReactNode; + onRetry?: () => void; +} + +export default function AsyncPageState({ + children, + workspace, + workspaceSubject, + loading, + loadingMode = "always", + hasData, + error, + loadingEyebrow, + loadingTitle, + errorEyebrow, + errorTitle, + emptyEyebrow, + emptyTitle, + emptyDetail, + emptyAction, + retryLabel, + onRetry, +}: AsyncPageStateProps) { + if (workspaceSubject && !workspace) { + return ; + } + + const showLoading = loading && (loadingMode === "always" || !hasData); + if (showLoading) { + return ( + + ); + } + + if (error) { + return ( + + {retryLabel} + + ) : undefined + } + /> + ); + } + + if (!hasData && emptyTitle) { + return ( + + ); + } + + return <>{children}; +} diff --git a/dashboard/src/components/ui/Button.tsx b/dashboard/src/components/ui/Button.tsx new file mode 100644 index 0000000..efcb272 --- /dev/null +++ b/dashboard/src/components/ui/Button.tsx @@ -0,0 +1,79 @@ +import type { ButtonHTMLAttributes, ReactNode } from "react"; +import { cx } from "./cx"; + +type ButtonVariant = "solid" | "soft" | "ghost"; +type ButtonTone = "neutral" | "brand" | "success" | "danger"; +type ButtonSize = "xs" | "sm" | "md" | "icon-sm" | "icon"; + +interface ButtonProps extends ButtonHTMLAttributes { + children: ReactNode; + variant?: ButtonVariant; + tone?: ButtonTone; + size?: ButtonSize; +} + +const sizeClasses: Record = { + xs: "min-h-11 rounded-lg px-3 py-2 text-sm md:rounded-md md:px-2.5 md:py-1.5 md:text-[0.75rem] md:leading-[1.4]", + sm: "min-h-11 rounded-lg px-3 py-2 text-sm md:rounded-md md:px-3 md:py-2 md:text-xs", + md: "min-h-11 rounded-lg px-4 py-2.5 text-sm md:py-2", + "icon-sm": "h-11 w-11 rounded-lg p-0 md:rounded-md", + icon: "h-12 w-12 rounded-xl p-0 md:h-10 md:w-10", +}; + +const toneClasses: Record> = { + solid: { + neutral: + "bg-[color:var(--app-text-muted)] text-[color:var(--app-text-inverse)] hover:bg-[color:var(--app-text)]", + brand: + "bg-[color:var(--app-accent-warm)] text-[color:var(--app-text-inverse)] hover:bg-[color:var(--app-accent)]", + success: + "bg-[color:var(--app-success-strong)] text-[color:var(--app-text-inverse)] hover:bg-[color:var(--app-success-text)]", + danger: + "bg-[color:var(--app-danger-strong)] text-[color:var(--app-text-inverse)] hover:bg-[color:var(--app-danger-text)]", + }, + soft: { + neutral: + "border border-[color:var(--app-divider)] bg-[color:var(--app-surface-muted)] text-[color:var(--app-text)] hover:bg-[color:var(--app-surface-elevated)]", + brand: + "border border-[color:var(--app-attention-border)] bg-[color:var(--app-attention-background)] text-[color:var(--app-attention-text)] hover:bg-[color:var(--app-attention-border)]", + success: + "border border-[color:var(--app-success-border)] bg-[color:var(--app-success-background)] text-[color:var(--app-success-text)] hover:bg-[color:var(--app-success-border)]", + danger: + "border border-[color:var(--app-danger-border)] bg-[color:var(--app-danger-background)] text-[color:var(--app-danger-text)] hover:bg-[color:var(--app-danger-border)]", + }, + ghost: { + neutral: + "text-[color:var(--app-text-muted)] hover:bg-[color:var(--app-surface-muted)] hover:text-[color:var(--app-text)]", + brand: + "text-[color:var(--app-attention-text)] hover:bg-[color:var(--app-attention-background)] hover:text-[color:var(--app-text)]", + success: + "text-[color:var(--app-success-text)] hover:bg-[color:var(--app-success-background)] hover:text-[color:var(--app-text)]", + danger: + "text-[color:var(--app-danger-text)] hover:bg-[color:var(--app-danger-background)] hover:text-[color:var(--app-text)]", + }, +}; + +export default function Button({ + children, + className, + variant = "ghost", + tone = "neutral", + size = "sm", + type = "button", + ...props +}: ButtonProps) { + return ( + + ); +} diff --git a/dashboard/src/components/ui/Card.tsx b/dashboard/src/components/ui/Card.tsx new file mode 100644 index 0000000..173c8fb --- /dev/null +++ b/dashboard/src/components/ui/Card.tsx @@ -0,0 +1,26 @@ +import type { ComponentPropsWithoutRef, ElementType } from "react"; +import { cx } from "./cx"; + +type CardProps = { + as?: T; + variant?: "default" | "hero"; +} & ComponentPropsWithoutRef; + +export default function Card({ + as, + variant = "default", + className, + ...props +}: CardProps) { + const Component = as ?? "div"; + + return ( + + ); +} diff --git a/dashboard/src/components/ui/CatalogSidebar.tsx b/dashboard/src/components/ui/CatalogSidebar.tsx new file mode 100644 index 0000000..e134082 --- /dev/null +++ b/dashboard/src/components/ui/CatalogSidebar.tsx @@ -0,0 +1,47 @@ +import type { ReactNode } from "react"; +import Button from "./Button"; +import InsetPanel from "./InsetPanel"; +import SelectableList, { type SelectableListItem } from "./SelectableList"; + +interface CatalogSidebarProps { + title: ReactNode; + actionLabel?: ReactNode; + onAction?: () => void; + empty: ReactNode; + items: ReadonlyArray>; + value: T; + onChange: (value: T) => void; +} + +export default function CatalogSidebar({ + title, + actionLabel, + onAction, + empty, + items, + value, + onChange, +}: CatalogSidebarProps) { + return ( +
+
+

+ {title} +

+ {actionLabel && onAction ? ( + + ) : null} +
+ + {items.length === 0 ? ( + + {empty} + + ) : ( + + )} +
+ ); +} diff --git a/dashboard/src/components/ui/Chip.tsx b/dashboard/src/components/ui/Chip.tsx new file mode 100644 index 0000000..5a41f6e --- /dev/null +++ b/dashboard/src/components/ui/Chip.tsx @@ -0,0 +1,75 @@ +import type { ComponentPropsWithoutRef } from "react"; +import { cx } from "./cx"; + +type ChipTone = + | "neutral" + | "brand" + | "attention" + | "danger" + | "success" + | "running" + | "info" + | "muted"; + +interface ChipProps extends ComponentPropsWithoutRef<"span"> { + tone?: ChipTone; + toneClassName?: string; + size?: "xs" | "sm" | "md"; + shape?: "pill" | "rounded"; + outlined?: boolean; + uppercase?: boolean; + nowrap?: boolean; +} + +const toneClasses: Record = { + neutral: "app-chip-subtle", + brand: "app-chip-brand", + attention: "app-chip-attention", + danger: "app-chip-danger", + success: "app-chip-success", + running: "app-chip-running", + info: + "border-[color:var(--app-info-border)] bg-[color:var(--app-info-background)] text-[color:var(--app-info-text)]", + muted: + "border-[color:var(--app-divider)] bg-[color:var(--app-surface-muted)] text-[color:var(--app-text-muted)]", +}; + +const sizeClasses = { + xs: "min-h-6 px-2 py-0.5 text-[0.75rem] leading-none", + sm: "min-h-7 px-2.5 py-1 text-xs leading-none", + md: "min-h-8 px-3 py-1.5 text-sm leading-none", +}; + +const shapeClasses = { + pill: "rounded-full", + rounded: "rounded-md", +}; + +export default function Chip({ + tone = "neutral", + toneClassName, + size = "xs", + shape = "pill", + outlined = false, + uppercase = false, + nowrap = true, + className, + ...props +}: ChipProps) { + return ( + + ); +} diff --git a/dashboard/src/components/ui/ConfirmDialog.test.tsx b/dashboard/src/components/ui/ConfirmDialog.test.tsx new file mode 100644 index 0000000..5104c2d --- /dev/null +++ b/dashboard/src/components/ui/ConfirmDialog.test.tsx @@ -0,0 +1,56 @@ +import { screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import { renderWithProviders } from "../../test/renderWithProviders"; +import ConfirmDialog from "./ConfirmDialog"; + +describe("ConfirmDialog", () => { + it("binds the dialog role to the panel instead of a full-screen wrapper", async () => { + renderWithProviders( + , + { locale: "en" }, + ); + + const dialog = await screen.findByRole("dialog", { name: "Stop this topic?" }); + expect(dialog).toHaveClass("max-w-md"); + expect(dialog).not.toHaveClass("inset-0"); + expect(within(dialog).getByRole("button", { name: "Confirm stop" })).toBeInTheDocument(); + }); + + it("closes on backdrop click and escape", async () => { + const onOpenChange = vi.fn(); + const user = userEvent.setup(); + + renderWithProviders( + , + { locale: "en" }, + ); + + const dialog = await screen.findByRole("dialog", { name: "Stop this topic?" }); + const backdrop = dialog.parentElement?.previousElementSibling; + if (!(backdrop instanceof HTMLElement)) { + throw new Error("dialog backdrop not found"); + } + + await user.click(backdrop); + await user.keyboard("{Escape}"); + + expect(onOpenChange).toHaveBeenCalledWith(false); + expect(onOpenChange).toHaveBeenCalledTimes(2); + }); +}); diff --git a/dashboard/src/components/ui/ConfirmDialog.tsx b/dashboard/src/components/ui/ConfirmDialog.tsx new file mode 100644 index 0000000..9ec675c --- /dev/null +++ b/dashboard/src/components/ui/ConfirmDialog.tsx @@ -0,0 +1,92 @@ +import { motion } from "framer-motion"; +import type { ReactNode } from "react"; +import Button from "./Button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogOverlay, + DialogPortal, + DialogTitle, +} from "./Dialog"; + +interface ConfirmDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title: ReactNode; + description?: ReactNode; + confirmLabel: ReactNode; + cancelLabel: ReactNode; + onConfirm: () => void | Promise; + confirmTone?: "neutral" | "brand" | "success" | "danger"; + busy?: boolean; +} + +export default function ConfirmDialog({ + open, + onOpenChange, + title, + description, + confirmLabel, + cancelLabel, + onConfirm, + confirmTone = "danger", + busy = false, +}: ConfirmDialogProps) { + return ( + + + + + +
+ + +
+
+
+ + {title} + + {description ? ( + + {description} + + ) : null} +
+
+ + + + +
+
+ + +
+ +
+ ); +} diff --git a/dashboard/src/components/ui/Dialog.tsx b/dashboard/src/components/ui/Dialog.tsx new file mode 100644 index 0000000..1b4dc7b --- /dev/null +++ b/dashboard/src/components/ui/Dialog.tsx @@ -0,0 +1,10 @@ +export { + Root as Dialog, + Trigger as DialogTrigger, + Portal as DialogPortal, + Overlay as DialogOverlay, + Content as DialogContent, + Close as DialogClose, + Title as DialogTitle, + Description as DialogDescription, +} from "@radix-ui/react-dialog"; diff --git a/dashboard/src/components/ui/DiffViewer.tsx b/dashboard/src/components/ui/DiffViewer.tsx new file mode 100644 index 0000000..2ff24d9 --- /dev/null +++ b/dashboard/src/components/ui/DiffViewer.tsx @@ -0,0 +1,35 @@ +import InsetPanel from "./InsetPanel"; + +interface DiffViewerProps { + diff: string; + className?: string; +} + +export default function DiffViewer({ diff, className }: DiffViewerProps) { + if (!diff) return null; + + return ( + + {diff.split("\n").map((line, index) => { + let lineClassName = "app-text-faint px-3"; + if (line.startsWith("+") && !line.startsWith("+++")) { + lineClassName = "app-code-line-added px-3"; + } else if (line.startsWith("-") && !line.startsWith("---")) { + lineClassName = "app-code-line-removed px-3"; + } else if (line.startsWith("@@")) { + lineClassName = "app-code-line-meta px-3"; + } + + return ( +
+ {line} +
+ ); + })} +
+ ); +} diff --git a/dashboard/src/components/ui/EditorActionBar.tsx b/dashboard/src/components/ui/EditorActionBar.tsx new file mode 100644 index 0000000..0f853f0 --- /dev/null +++ b/dashboard/src/components/ui/EditorActionBar.tsx @@ -0,0 +1,33 @@ +import type { ReactNode } from "react"; +import { cx } from "./cx"; + +interface EditorActionBarProps { + children: ReactNode; + error?: ReactNode; + leading?: ReactNode; + className?: string; +} + +export default function EditorActionBar({ + children, + error, + leading, + className, +}: EditorActionBarProps) { + return ( +
+ {leading} + {error ? ( +
+ {error} +
+ ) : ( +
+ )} +
{children}
+
+ ); +} diff --git a/dashboard/src/components/ui/EditorShell.tsx b/dashboard/src/components/ui/EditorShell.tsx new file mode 100644 index 0000000..305b4d2 --- /dev/null +++ b/dashboard/src/components/ui/EditorShell.tsx @@ -0,0 +1,45 @@ +import type { ReactNode } from "react"; +import { cx } from "./cx"; + +interface EditorShellProps { + header?: ReactNode; + sidebar?: ReactNode; + footer?: ReactNode; + children: ReactNode; + className?: string; + contentClassName?: string; + bodyClassName?: string; + sidebarClassName?: string; + mainClassName?: string; +} + +export default function EditorShell({ + header, + sidebar, + footer, + children, + className, + contentClassName, + bodyClassName, + sidebarClassName, + mainClassName, +}: EditorShellProps) { + return ( +
+ {header} +
+
+ {sidebar ? ( + + ) : null} +
+ {children} +
+
+
+ {footer} +
+ ); +} diff --git a/dashboard/src/components/ui/FormField.tsx b/dashboard/src/components/ui/FormField.tsx new file mode 100644 index 0000000..87d2984 --- /dev/null +++ b/dashboard/src/components/ui/FormField.tsx @@ -0,0 +1,50 @@ +import type { ReactNode } from "react"; +import { cx } from "./cx"; + +interface FormFieldProps { + children: ReactNode; + label?: ReactNode; + htmlFor?: string; + hint?: ReactNode; + error?: ReactNode; + className?: string; + contentClassName?: string; + labelClassName?: string; + hintClassName?: string; + errorClassName?: string; +} + +export default function FormField({ + children, + label, + htmlFor, + hint, + error, + className, + contentClassName, + labelClassName, + hintClassName, + errorClassName, +}: FormFieldProps) { + return ( +
+ {label ? ( + + ) : null} +
{children}
+ {hint ? ( +
{hint}
+ ) : null} + {error ? ( +
+ {error} +
+ ) : null} +
+ ); +} diff --git a/dashboard/src/components/ui/InlineComposer.tsx b/dashboard/src/components/ui/InlineComposer.tsx new file mode 100644 index 0000000..277ab44 --- /dev/null +++ b/dashboard/src/components/ui/InlineComposer.tsx @@ -0,0 +1,71 @@ +import type { FormEvent, ReactNode, Ref } from "react"; +import Button from "./Button"; +import InsetPanel from "./InsetPanel"; +import TextInput from "./TextInput"; +import { cx } from "./cx"; + +interface InlineComposerProps { + label: ReactNode; + value: string; + onChange: (value: string) => void; + onSubmit: () => void; + placeholder?: string; + hint?: ReactNode; + error?: ReactNode; + submitLabel: ReactNode; + disabled?: boolean; + inputId: string; + inputRef?: Ref; + className?: string; + action?: ReactNode; +} + +export default function InlineComposer({ + label, + value, + onChange, + onSubmit, + placeholder, + hint, + error, + submitLabel, + disabled = false, + inputId, + inputRef, + className, + action, +}: InlineComposerProps) { + return ( + { + event.preventDefault(); + onSubmit(); + }} + > + +
+ onChange(event.target.value)} + placeholder={placeholder} + disabled={disabled} + className="flex-1" + /> + {action ?? ( + + )} +
+ {hint ?
{hint}
: null} + {error ? ( +
+ {error} +
+ ) : null} +
+ ); +} diff --git a/dashboard/src/components/ui/InsetPanel.tsx b/dashboard/src/components/ui/InsetPanel.tsx new file mode 100644 index 0000000..c3cb480 --- /dev/null +++ b/dashboard/src/components/ui/InsetPanel.tsx @@ -0,0 +1,44 @@ +import type { ComponentPropsWithoutRef, ElementType } from "react"; +import { cx } from "./cx"; + +type InsetPanelProps = { + as?: T; + tone?: "neutral" | "danger" | "code"; + padding?: "sm" | "md"; +} & ComponentPropsWithoutRef; + +const toneClasses = { + neutral: + "border border-[color:var(--app-divider)] bg-[color:var(--app-surface-muted)]", + danger: + "border border-[color:var(--app-danger-border)] bg-[color:var(--app-danger-background)]", + code: + "border border-[color:var(--app-divider-soft)] bg-[color:var(--app-surface-code)]", +}; + +const paddingClasses = { + sm: "px-3 py-2", + md: "p-4", +}; + +export default function InsetPanel({ + as, + tone = "neutral", + padding = "md", + className, + ...props +}: InsetPanelProps) { + const Component = as ?? "div"; + + return ( + + ); +} diff --git a/dashboard/src/components/ui/PageHero.tsx b/dashboard/src/components/ui/PageHero.tsx new file mode 100644 index 0000000..c29b3d7 --- /dev/null +++ b/dashboard/src/components/ui/PageHero.tsx @@ -0,0 +1,89 @@ +import type { ReactNode } from "react"; +import Card from "./Card"; +import { cx } from "./cx"; + +interface PageHeroProps { + eyebrow?: ReactNode; + title: ReactNode; + description?: ReactNode; + contentFooter?: ReactNode; + headerActions?: ReactNode; + stats?: ReactNode; + footer?: ReactNode; + layout?: "split" | "stack"; + align?: "start" | "end"; + className?: string; + contentClassName?: string; + titleClassName?: string; + descriptionClassName?: string; + statsClassName?: string; +} + +export default function PageHero({ + eyebrow, + title, + description, + contentFooter, + headerActions, + stats, + footer, + layout = "split", + align = "end", + className, + contentClassName, + titleClassName, + descriptionClassName, + statsClassName, +}: PageHeroProps) { + return ( + +
+
+
+
+ {eyebrow ?

{eyebrow}

: null} +

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} + {contentFooter ?
{contentFooter}
: null} +
+ {headerActions ?
{headerActions}
: null} +
+
+ {stats ?
{stats}
: null} +
+ {footer ?
{footer}
: null} +
+ ); +} diff --git a/dashboard/src/components/ui/PageSectionCard.tsx b/dashboard/src/components/ui/PageSectionCard.tsx new file mode 100644 index 0000000..6080443 --- /dev/null +++ b/dashboard/src/components/ui/PageSectionCard.tsx @@ -0,0 +1,40 @@ +import type { ReactNode } from "react"; +import Card from "./Card"; +import PanelHeader from "./PanelHeader"; +import { cx } from "./cx"; + +interface PageSectionCardProps { + title: ReactNode; + children: ReactNode; + eyebrow?: ReactNode; + detail?: ReactNode; + action?: ReactNode; + className?: string; + headerClassName?: string; + bodyClassName?: string; +} + +export default function PageSectionCard({ + title, + children, + eyebrow, + detail, + action, + className, + headerClassName, + bodyClassName, +}: PageSectionCardProps) { + return ( + + +
{children}
+
+ ); +} diff --git a/dashboard/src/components/ui/PanelHeader.tsx b/dashboard/src/components/ui/PanelHeader.tsx new file mode 100644 index 0000000..3ba4c46 --- /dev/null +++ b/dashboard/src/components/ui/PanelHeader.tsx @@ -0,0 +1,84 @@ +import type { ReactNode } from "react"; +import { cx } from "./cx"; + +interface PanelHeaderProps { + title: ReactNode; + titleAs?: "h2" | "h3" | "div" | "span"; + eyebrow?: ReactNode; + detail?: ReactNode; + leading?: ReactNode; + trailing?: ReactNode; + action?: ReactNode; + variant?: "compact" | "section"; + className?: string; + contentClassName?: string; + titleClassName?: string; + detailClassName?: string; + actionClassName?: string; + titleId?: string; +} + +export default function PanelHeader({ + title, + titleAs, + eyebrow, + detail, + leading, + trailing, + action, + variant = "compact", + className, + contentClassName, + titleClassName, + detailClassName, + actionClassName, + titleId, +}: PanelHeaderProps) { + const TitleElement = titleAs ?? (titleId ? "h2" : "span"); + const trailingNode = action ?? trailing; + + return ( +
+ {variant === "section" ? ( +
+
+ {eyebrow ?

{eyebrow}

: null} + + {title} + + {detail ? ( +

+ {detail} +

+ ) : null} +
+ {trailingNode ?
{trailingNode}
: null} +
+ ) : ( + <> + {leading} + + {title} + + {trailingNode &&
{trailingNode}
} + + )} +
+ ); +} diff --git a/dashboard/src/components/ui/Popover.tsx b/dashboard/src/components/ui/Popover.tsx new file mode 100644 index 0000000..0dda4ec --- /dev/null +++ b/dashboard/src/components/ui/Popover.tsx @@ -0,0 +1,8 @@ +export { + Root as Popover, + Trigger as PopoverTrigger, + Portal as PopoverPortal, + Content as PopoverContent, + Anchor as PopoverAnchor, + Close as PopoverClose, +} from "@radix-ui/react-popover"; diff --git a/dashboard/src/components/ui/ResponseComposer.tsx b/dashboard/src/components/ui/ResponseComposer.tsx new file mode 100644 index 0000000..b996c5a --- /dev/null +++ b/dashboard/src/components/ui/ResponseComposer.tsx @@ -0,0 +1,95 @@ +import type { ChangeEventHandler, ReactNode } from "react"; +import Button from "./Button"; +import PageSectionCard from "./PageSectionCard"; +import TextareaField from "./TextareaField"; + +interface ResponseComposerProps { + eyebrow?: ReactNode; + title: ReactNode; + detail?: ReactNode; + prompt?: ReactNode; + promptTone?: "neutral" | "danger"; + topSlot?: ReactNode; + textareaId?: string; + textareaLabel?: string; + value: string; + onChange: ChangeEventHandler; + placeholder?: string; + error?: ReactNode; + actionLabel: ReactNode; + sendingLabel: ReactNode; + sending?: boolean; + disabled?: boolean; + onSubmit: () => void; + headerAction?: ReactNode; + className?: string; + bodyClassName?: string; +} + +export default function ResponseComposer({ + eyebrow, + title, + detail, + prompt, + promptTone = "neutral", + topSlot, + textareaId, + textareaLabel, + value, + onChange, + placeholder, + error, + actionLabel, + sendingLabel, + sending = false, + disabled = false, + onSubmit, + headerAction, + className, + bodyClassName = "space-y-3 px-5 py-5 sm:px-6", +}: ResponseComposerProps) { + return ( + + {topSlot} + {prompt ? ( +
+ {prompt} +
+ ) : null} + + {error && typeof error !== "string" ?
{error}
: null} +
+ +
+
+ ); +} diff --git a/dashboard/src/components/ui/SelectableList.tsx b/dashboard/src/components/ui/SelectableList.tsx new file mode 100644 index 0000000..af9e232 --- /dev/null +++ b/dashboard/src/components/ui/SelectableList.tsx @@ -0,0 +1,60 @@ +import type { ReactNode } from "react"; +import { cx } from "./cx"; + +export interface SelectableListItem { + id: T; + title: ReactNode; + description?: ReactNode; + meta?: ReactNode; +} + +interface SelectableListProps { + items: ReadonlyArray>; + value: T; + onChange: (value: T) => void; + className?: string; + itemClassName?: string; + activeItemClassName?: string; +} + +export default function SelectableList({ + items, + value, + onChange, + className, + itemClassName, + activeItemClassName, +}: SelectableListProps) { + return ( +
+ {items.map((item) => { + const active = item.id === value; + return ( + + ); + })} +
+ ); +} diff --git a/dashboard/src/components/ui/StageBadge.tsx b/dashboard/src/components/ui/StageBadge.tsx new file mode 100644 index 0000000..035fa70 --- /dev/null +++ b/dashboard/src/components/ui/StageBadge.tsx @@ -0,0 +1,40 @@ +import type { ComponentPropsWithoutRef } from "react"; +import { stageTone } from "../../styles/tokens"; +import { useI18n } from "../../i18n"; +import StatusBadge from "./StatusBadge"; + +interface StageBadgeProps extends ComponentPropsWithoutRef<"span"> { + stage: string; + label?: string; + size?: "xs" | "sm"; +} + +const sizeClasses = { + xs: "xs", + sm: "sm", +} as const; + +export default function StageBadge({ + stage, + label, + size = "xs", + className, + ...props +}: StageBadgeProps) { + const { formatStageLabel } = useI18n(); + const tone = stageTone[stage]; + + return ( + + {label ?? formatStageLabel(stage) ?? tone?.label ?? stage} + + ); +} diff --git a/dashboard/src/components/ui/StatusBadge.tsx b/dashboard/src/components/ui/StatusBadge.tsx new file mode 100644 index 0000000..b40f56f --- /dev/null +++ b/dashboard/src/components/ui/StatusBadge.tsx @@ -0,0 +1,39 @@ +import type { ComponentPropsWithoutRef } from "react"; +import Chip from "./Chip"; + +interface StatusBadgeProps extends ComponentPropsWithoutRef<"span"> { + tone?: + | "neutral" + | "brand" + | "attention" + | "danger" + | "success" + | "running" + | "info" + | "muted"; + toneClassName?: string; + size?: "xs" | "sm" | "md"; + uppercase?: boolean; + nowrap?: boolean; +} + +export default function StatusBadge({ + tone = "neutral", + toneClassName, + size = "xs", + uppercase = false, + nowrap = true, + ...props +}: StatusBadgeProps) { + return ( + + ); +} diff --git a/dashboard/src/components/ui/StatusDot.tsx b/dashboard/src/components/ui/StatusDot.tsx new file mode 100644 index 0000000..76c1360 --- /dev/null +++ b/dashboard/src/components/ui/StatusDot.tsx @@ -0,0 +1,8 @@ +import type { ComponentPropsWithoutRef } from "react"; +import { cx } from "./cx"; + +type StatusDotProps = ComponentPropsWithoutRef<"span">; + +export default function StatusDot({ className, ...props }: StatusDotProps) { + return ; +} diff --git a/dashboard/src/components/ui/SummaryStat.tsx b/dashboard/src/components/ui/SummaryStat.tsx new file mode 100644 index 0000000..8cb5b0c --- /dev/null +++ b/dashboard/src/components/ui/SummaryStat.tsx @@ -0,0 +1,44 @@ +import type { ReactNode } from "react"; +import { cx } from "./cx"; + +type SummaryStatTone = "default" | "attention" | "muted"; + +const valueToneClasses: Record = { + default: "app-text-primary", + attention: "app-text-attention", + muted: "app-text-muted", +}; + +export default function SummaryStat({ + label, + value, + detail, + tone = "default", + className, +}: { + label: ReactNode; + value: ReactNode; + detail: ReactNode; + tone?: SummaryStatTone; + className?: string; +}) { + return ( +
+

{label}

+

+ {value} +

+

{detail}

+
+ ); +} diff --git a/dashboard/src/components/ui/Tabs.tsx b/dashboard/src/components/ui/Tabs.tsx new file mode 100644 index 0000000..2006003 --- /dev/null +++ b/dashboard/src/components/ui/Tabs.tsx @@ -0,0 +1,54 @@ +import type { ReactNode } from "react"; +import { cx } from "./cx"; + +export interface TabItem { + id: T; + label: ReactNode; + disabled?: boolean; +} + +interface TabsProps { + items: ReadonlyArray>; + value: T; + onChange: (value: T) => void; + className?: string; + listClassName?: string; + triggerClassName?: string; +} + +export default function Tabs({ + items, + value, + onChange, + className, + listClassName, + triggerClassName, +}: TabsProps) { + return ( +
+
+ {items.map((item) => { + const active = item.id === value; + return ( + + ); + })} +
+
+ ); +} diff --git a/dashboard/src/components/ui/TextInput.tsx b/dashboard/src/components/ui/TextInput.tsx new file mode 100644 index 0000000..2840007 --- /dev/null +++ b/dashboard/src/components/ui/TextInput.tsx @@ -0,0 +1,31 @@ +import { + forwardRef, + type ComponentPropsWithoutRef, +} from "react"; +import { cx } from "./cx"; + +export const textInputBaseClassName = + "w-full rounded-lg border border-[color:var(--app-divider)] bg-[color:var(--app-surface-muted)] px-3 py-2 text-sm app-text-primary placeholder:app-text-faint focus:outline-none focus:ring-1 focus:ring-[color:var(--app-accent)] disabled:cursor-not-allowed disabled:opacity-60"; + +interface TextInputProps extends ComponentPropsWithoutRef<"input"> { + invalid?: boolean; +} + +const TextInput = forwardRef(function TextInput( + { className, invalid = false, ...props }, + ref, +) { + return ( + + ); +}); + +export default TextInput; diff --git a/dashboard/src/components/ui/TextareaField.tsx b/dashboard/src/components/ui/TextareaField.tsx new file mode 100644 index 0000000..1455f27 --- /dev/null +++ b/dashboard/src/components/ui/TextareaField.tsx @@ -0,0 +1,65 @@ +import { + forwardRef, + type ComponentPropsWithoutRef, +} from "react"; +import FormField from "./FormField"; +import { cx } from "./cx"; +import { textInputBaseClassName } from "./TextInput"; + +interface TextareaFieldProps extends ComponentPropsWithoutRef<"textarea"> { + label?: string; + hint?: string; + error?: string; + invalid?: boolean; + fieldClassName?: string; + textareaClassName?: string; + labelClassName?: string; + hintClassName?: string; + errorClassName?: string; +} + +const TextareaField = forwardRef(function TextareaField( + { + label, + hint, + error, + invalid = false, + fieldClassName, + textareaClassName, + labelClassName, + hintClassName, + errorClassName, + id, + className, + ...props + }, + ref, +) { + return ( + +