diff --git a/.github/workflows/docs-validation.yml b/.github/workflows/docs-validation.yml index a54e333..3794a58 100644 --- a/.github/workflows/docs-validation.yml +++ b/.github/workflows/docs-validation.yml @@ -21,6 +21,22 @@ jobs: run: python -m unittest discover -s tests -v - name: Check relative Markdown links run: python scripts/check_docs.py + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + cache-dependency-path: dashboard/package-lock.json + - name: Install dashboard dependencies + run: npm ci --prefix dashboard + - name: Typecheck dashboard + run: npm --prefix dashboard run typecheck + - name: Lint dashboard + run: npm --prefix dashboard run lint + - name: Test dashboard + run: npm --prefix dashboard test + - name: Build dashboard + run: npm --prefix dashboard run build # ORIGINAL (preserved, not deleted — disabled by comment): # - name: Validate Markdown documents # run: | diff --git a/.gitignore b/.gitignore index 6e08527..1fdbeef 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ coverage/ # Local runtime data *.log data/ +!dashboard/src/data/ +!dashboard/src/data/** diff --git a/AGENTS.md b/AGENTS.md index d88fd9e..7cc0001 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Mission and current scope -OpenCluster is planned as a self-hosted control plane for cluster nodes, infrastructure, AI agents, and distributed workloads: **One dashboard. Every node. Total control.** The repository is currently in Phase 0. The dashboard, API, node agent, and deployment stack are not implemented. Do not build product features until a maintainer selects an implementation milestone. +OpenCluster is a cluster operations platform: **One dashboard. Every node. Total control.** Phase 0 is complete. Phase 1 builds the permanent standalone React dashboard against a validated, persistent local data provider; seeded hardware telemetry remains clearly identified as sample data until backend services exist. Do not add a backend, authentication service, node agent, remote command execution, live software installation, or cloud dependency in Phase 1. ## Before changing anything @@ -14,7 +14,8 @@ OpenCluster is planned as a self-hosted control plane for cluster nodes, infrast ## Repository organization -- `frontend/` — planned React and TypeScript dashboard. +- `frontend/` — existing Phase 0 placeholder; do not implement the Phase 1 dashboard here. +- `dashboard/` — standalone production frontend in Phase 1, using IndexedDB for structured local records and localStorage for non-sensitive preferences; backend integration remains future work. - `backend/` — planned FastAPI control plane. - `node-agent/` — planned lightweight Linux node service. - `infrastructure/` — deployment and operations assets. @@ -25,7 +26,7 @@ OpenCluster is planned as a self-hosted control plane for cluster nodes, infrast ## Architecture and security rules -- Follow `docs/ARCHITECTURE.md`, `docs/SECURITY_ARCHITECTURE.md`, `docs/API_DESIGN.md`, and `docs/DATABASE_DESIGN.md`. They describe proposals until implementation exists. +- Follow `docs/ARCHITECTURE.md`, `docs/SECURITY_ARCHITECTURE.md`, `docs/API_DESIGN.md`, and `docs/DATABASE_DESIGN.md`. They describe backend proposals until those services exist. The Phase 1 dashboard stores validated structured records in IndexedDB, non-sensitive preferences in localStorage, and clearly labeled sample telemetry in the local provider. - Keep module boundaries explicit. Frontend code must not access the database directly; the node agent must not trust control-plane input without authentication and authorization. - Default to least privilege. Privileged node actions require authenticated identity, authorization, audit records, bounded operations, and explicit confirmation where destructive. - Never expose unrestricted remote shells or arbitrary cross-node command execution. Do not add secrets to code, logs, examples, or fixtures. @@ -51,7 +52,7 @@ Reviewers should check correctness, regressions, architecture and interface cons ## Tests and validation - Run focused checks for changed behavior, then relevant broader checks when they exist. Do not claim commands passed unless they were run. -- During Phase 0, CI validates documentation links and repository configuration only. Application build workflows must wait until application manifests and build tooling exist. +- CI validates documentation and the dashboard's checked-in typecheck, lint, test, and build scripts. Add workflows only for artifacts and tools that exist. - Add regression tests for non-trivial behavior, failure paths, and security-sensitive logic. Keep performance benchmarks reproducible and do not invent targets or results. ## Documentation requirements diff --git a/README.md b/README.md index da5d5ea..b252d58 100644 --- a/README.md +++ b/README.md @@ -3,57 +3,57 @@ **One dashboard. Every node. Total control.** [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE) -[![Status: Phase 0](https://img.shields.io/badge/status-Phase%200%20%7C%20planning-lightgrey)](docs/ROADMAP.md) +[![Status: Phase 1](https://img.shields.io/badge/status-Phase%201%20%7C%20dashboard%20development-blue)](docs/ROADMAP.md) -OpenCluster is a planned free, open-source, self-hosted platform for monitoring and managing computing clusters, infrastructure nodes, AI agents, and distributed workloads through a unified web dashboard. It is maintained by **ZYRIX DEVS**. +OpenCluster is an open-source project by **ZYRIX DEVS**. Its long-term goal is a self-hosted platform for understanding and managing clusters, infrastructure nodes, AI agents, and distributed workloads. -> **Project status:** repository foundation and design phase. The dashboard, backend, node agent, and deployment stack are not implemented. Feature descriptions below are plans, not available functionality. +## Current project status -## Planned capabilities +Phase 0 repository foundation is complete. Phase 1 builds the permanent standalone dashboard in `dashboard/`, with validated browser-local records and a replaceable data-provider boundary. The application is still under development; the required pages and interactions are not claimed as complete until implemented and verified. -- Cluster and node health, resource telemetry, alerts, logs, and history. -- Secure node enrollment, labels, groups, and permission-controlled operations. -- Distributed task scheduling and recovery policies. -- Extensible AI agent adapters, including Codex and Hermes where supported. -- GitHub and developer-operations visibility with auditable administrative controls. +The Phase 1 dashboard is a **production frontend operating in local mode**. It supports persistent local cluster, node, graph, and workflow records. It does not connect to physical clusters, execute tasks, install software, or provide live telemetry until backend services and an authorized node agent exist. It does not require cloud services. Seeded hardware metrics are visibly identified as sample data. -The initial release is expected to focus on secure node enrollment, read-only cluster and node visibility, and a small operator-managed deployment. Scope remains subject to maintainer review; see the [PRD](docs/PRD.md) and [roadmap](docs/ROADMAP.md). +## Dashboard scope -## Proposed architecture +The dashboard includes Home, Mission Control, Clusters, Nodes, Agents, Automation, AI Software, Tasks, Monitoring, Activity, and Settings views. Cluster/node management and canvas editing operate on saved local records; hardware metrics are sample data, and installations or workload execution remain unavailable. The implementation requirements are in the [dashboard PRD](docs/DASHBOARD_PRD.md), [UI/UX specification](docs/DASHBOARD_UI_UX_SPEC.md), and [architecture and component map](docs/DASHBOARD_ARCHITECTURE_AND_COMPONENT_MAP.md). -The current proposal uses a React, TypeScript, and Vite frontend; a Python/FastAPI control plane; PostgreSQL for durable state; and a lightweight Linux node agent. Docker Compose is the planned local deployment path. These choices are not implemented or locked; trade-offs are recorded in [architecture](docs/ARCHITECTURE.md) and [technology stack](docs/TECH_STACK.md). +## Future direction -```text -frontend/ dashboard (planned) -backend/ API and control plane (planned) -node-agent/ Linux metrics and secure communication (planned) -infrastructure/ local and future deployment assets (planned) -docs/ requirements and design decisions -``` +Backend services, secure node enrollment, real telemetry, authentication and authorization, audit logging, AI-agent integrations, and workload orchestration belong to later phases. They are not current capabilities. The [roadmap](docs/ROADMAP.md) and [architecture proposal](docs/ARCHITECTURE.md) distinguish future designs from implemented software. + +## Development -## Development status +The dashboard requires Node.js 20.19+ or 22.12+ and npm (Vite's supported Node versions). From the repository root, install dependencies and start the local development server: -Phase 0 work is limited to repository structure, documentation, Codex guidance, and docs validation. There are no application prerequisites or runnable app commands yet. +```sh +npm --prefix dashboard install +npm --prefix dashboard run dev +``` -To work on the repository foundation: +Vite prints the local URL when the server starts. The dashboard uses persistent browser-local records and sample hardware telemetry; no backend or cloud service is needed. Run the available checks and production build with: + +```sh +npm --prefix dashboard run typecheck +npm --prefix dashboard run lint +npm --prefix dashboard test +npm --prefix dashboard run build +``` -1. Clone this repository and create a topic branch. -2. Read [AGENTS.md](AGENTS.md), [development guide](docs/DEVELOPMENT.md), and the relevant design documents. -3. Edit documentation or repository configuration and run the checks documented in `docs/DEVELOPMENT.md`. -4. Open a pull request against `main` using the [contribution guide](CONTRIBUTING.md). +For repository contribution setup, read [AGENTS.md](AGENTS.md), the [development guide](docs/DEVELOPMENT.md), and [CONTRIBUTING.md](CONTRIBUTING.md). Review the [testing strategy](docs/TESTING_STRATEGY.md) for current repository checks. Open a pull request against `main` following the [GitHub workflow](docs/GITHUB_WORKFLOW.md). ## Documentation -- [Product requirements](docs/PRD.md) · [Architecture](docs/ARCHITECTURE.md) · [Roadmap](docs/ROADMAP.md) -- [Technology stack](docs/TECH_STACK.md) · [API design](docs/API_DESIGN.md) · [Database design](docs/DATABASE_DESIGN.md) -- [Node protocol](docs/NODE_PROTOCOL.md) · [Security architecture](docs/SECURITY_ARCHITECTURE.md) -- [Design system](docs/DESIGN_SYSTEM.md) · [Agent orchestration](docs/AGENT_ORCHESTRATION.md) -- [Development](docs/DEVELOPMENT.md) · [Deployment](docs/DEPLOYMENT.md) · [Testing strategy](docs/TESTING_STRATEGY.md) -- [GitHub workflow](docs/GITHUB_WORKFLOW.md) · [Decisions](docs/DECISIONS.md) +- [Product requirements](docs/PRD.md) · [Roadmap](docs/ROADMAP.md) · [Architecture proposal](docs/ARCHITECTURE.md) +- [Technology stack](docs/TECH_STACK.md) · [Design system](docs/DESIGN_SYSTEM.md) +- [Dashboard motion and performance](docs/DASHBOARD_PERFORMANCE.md) +- [Dashboard product requirements](docs/DASHBOARD_PRD.md) · [UI/UX specification](docs/DASHBOARD_UI_UX_SPEC.md) · [Architecture and component map](docs/DASHBOARD_ARCHITECTURE_AND_COMPONENT_MAP.md) +- [API design proposal](docs/API_DESIGN.md) · [Database design proposal](docs/DATABASE_DESIGN.md) · [Node protocol proposal](docs/NODE_PROTOCOL.md) +- [Security architecture proposal](docs/SECURITY_ARCHITECTURE.md) · [Development](docs/DEVELOPMENT.md) · [Testing strategy](docs/TESTING_STRATEGY.md) +- [Deployment proposal](docs/DEPLOYMENT.md) · [GitHub workflow](docs/GITHUB_WORKFLOW.md) · [Decisions](docs/DECISIONS.md) -## Contributing +## Contributing and security -Contributions are welcome after reading [CONTRIBUTING.md](CONTRIBUTING.md). Please open an issue for substantial proposals before implementation. Security issues should follow [SECURITY.md](SECURITY.md), not a public issue. +Contributions are welcome under [CONTRIBUTING.md](CONTRIBUTING.md). Please report security concerns through [SECURITY.md](SECURITY.md), not a public issue. The dashboard does not yet implement backend authentication, authorization, node enrollment, or remote-action protections because those services are not in Phase 1. ## License and attribution diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 0000000..c29b916 --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,29 @@ +# OpenCluster Dashboard + +The dashboard is OpenCluster's permanent standalone frontend, currently operating in local mode. Cluster, node, workflow, and canvas records persist in IndexedDB; lightweight preferences use localStorage. Seeded telemetry is sample data. The app does not connect to physical nodes, dispatch tasks, install software, or require a cloud service while backend services are unavailable. + +## Run locally + +From the repository root: + +```sh +npm --prefix dashboard install +npm --prefix dashboard run dev +``` + +Vite prints the local development URL. Node.js 20.19+ or 22.12+ is required. + +## Checks + +```sh +npm --prefix dashboard run typecheck +npm --prefix dashboard run lint +npm --prefix dashboard test +npm --prefix dashboard run build +``` + +The local provider stores clusters, nodes, canvas layouts, workflows, and non-sensitive preferences in this browser. Structured records use IndexedDB. Do not enter credentials or secrets. + +## Views + +The Home page summarizes local clusters, nodes, resource use, alerts, activity, and agent records. Mission Control offers topology, orbit, and an optional 3D globe view. Agents and Automation provide editable React Flow canvases with saved local state. Software configuration is saved locally; installation and workflow execution remain unavailable until secure backend services exist. Read the [dashboard PRD](../docs/DASHBOARD_PRD.md), [UI/UX specification](../docs/DASHBOARD_UI_UX_SPEC.md), and [architecture and component map](../docs/DASHBOARD_ARCHITECTURE_AND_COMPONENT_MAP.md) for requirements. See [the dashboard performance notes](../docs/DASHBOARD_PERFORMANCE.md) for animation, reduced-motion, and rendering behavior. diff --git a/dashboard/biome.json b/dashboard/biome.json new file mode 100644 index 0000000..6474c9b --- /dev/null +++ b/dashboard/biome.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.6.2/schema.json", + "files": { "ignoreUnknown": true }, + "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended", + "a11y": { "preset": "recommended" }, + "correctness": { "preset": "recommended" }, + "style": { "useImportType": "error" } + } + }, + "javascript": { + "formatter": { "quoteStyle": "double", "semicolons": "always" } + } +} diff --git a/dashboard/components.json b/dashboard/components.json new file mode 100644 index 0000000..cccf3be --- /dev/null +++ b/dashboard/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 0000000..950469a --- /dev/null +++ b/dashboard/index.html @@ -0,0 +1,17 @@ + + + + + + + + OpenCluster — Mission Control + + +
+ + + diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json new file mode 100644 index 0000000..a406585 --- /dev/null +++ b/dashboard/package-lock.json @@ -0,0 +1,5150 @@ +{ + "name": "@opencluster/dashboard", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@opencluster/dashboard", + "version": "0.1.0", + "dependencies": { + "@fontsource-variable/inter": "^5.3.0", + "@fontsource-variable/sora": "^5.3.0", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-query": "^5.104.5", + "@xyflow/react": "^12.12.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "framer-motion": "^14.6.0", + "gsap": "^3.15.0", + "idb": "^8.0.4", + "lenis": "^1.3.27", + "lucide-react": "^1.61.0", + "react": "^19.3.1", + "react-dom": "^19.3.1", + "react-router-dom": "^7.18.4", + "recharts": "^3.10.1", + "tailwind-merge": "^3.6.0", + "three": "^0.186.1", + "zod": "^4.6.7", + "zustand": "^5.0.16" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.5", + "@tailwindcss/vite": "^4.3.3", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^26.6.5", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@types/three": "^0.186.0", + "@vitejs/plugin-react": "^6.1.2", + "fake-indexeddb": "^6.2.6", + "jsdom": "^30.1.3", + "typescript": "^7.0.2", + "vite": "^8.3.5", + "vitest": "^5.0.5" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.7.tgz", + "integrity": "sha512-Q7ioeear07HwixdIitIn9KS1LZi97CEDFCSjPNm9v+y8KNbsu9q4Lrd4zUQW4faED+E7qxvfeVnbNRW46wFKuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.4.3", + "@csstools/css-color-parser": "^4.2.6", + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-10.0.4.tgz", + "integrity": "sha512-A6f1nKy99I6tyoK82U6kBHvQQZmdHmTmPzJYPY/0eaOW0r8/qPmEOwjGHs+hL/vCgOwQTupu4LT1XtXNUuDruA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.1.0", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.6.2.tgz", + "integrity": "sha512-JLTFAFF9vfJcWwLhwuL4ZHflLg+XfTEC4IUnAlJRX1cMRt57++Xd1Ysb9Fdmlh9YUrvyCXfPEHjjEIqKJGdPKw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-android-arm64": "2.6.2", + "@biomejs/cli-android-x64": "2.6.2", + "@biomejs/cli-darwin-arm64": "2.6.2", + "@biomejs/cli-darwin-x64": "2.6.2", + "@biomejs/cli-linux-arm64": "2.6.2", + "@biomejs/cli-linux-arm64-musl": "2.6.2", + "@biomejs/cli-linux-x64": "2.6.2", + "@biomejs/cli-linux-x64-musl": "2.6.2", + "@biomejs/cli-win32-arm64": "2.6.2", + "@biomejs/cli-win32-x64": "2.6.2" + } + }, + "node_modules/@biomejs/cli-android-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-android-arm64/-/cli-android-arm64-2.6.2.tgz", + "integrity": "sha512-dMpdpH7zFEL5FxuIA9LJW+RoNyC3oeDB7PJyjqVVwtMNoKB5uo8OZYmrVSBikn71txzYwy/3xga7UME6bJQZ6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-android-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-android-x64/-/cli-android-x64-2.6.2.tgz", + "integrity": "sha512-XCwEhPBGYzuRA502dRkCcQ+5cefQwuxQR7f9wa4do92dvIcuBL6VMLMFB/fL9hlpZ4dGSfHdpGFSIDlp52mg0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.6.2.tgz", + "integrity": "sha512-rpXtabRgyZNz3aCjd/cRoSZGYi92R9eAXdUUe1sigIsRDyfMwgThaa98Yxt1fiZ7mxquqkAyIHIo08N/e1ToBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.6.2.tgz", + "integrity": "sha512-7GGzlm1GxUKoQ9CNV8tQZ+XXad4sz+v+uzNsWlhaGjsnZqUOVBw1OmAMpG3uNV/Mr0UrCpRJvSK0qvfuqSQOjg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.6.2.tgz", + "integrity": "sha512-Qs4CofCE8MdkjZFfEjswh2NAcbyQ7IVYtGSzQBvLqFlla/CSXroGbktUFndWG/kVSTywadauzAspYWtSfqAc2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.6.2.tgz", + "integrity": "sha512-I+FYCLVSmR3cYfKcQ+KNeLOZ5w0wRd9fQFlw3JbSvy7bkegvTqdHaBRRcJzHQB/1ldldTLIcG21ka6X2cTWmJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.6.2.tgz", + "integrity": "sha512-FJKsb95418139xDdbv9XnNJsIIwvsgR21Of0aE9B9U+nQlOtRq5gJl2ps0jZUmYBxTaOlP9BKo8N+Ff8+Rui1g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.6.2.tgz", + "integrity": "sha512-wnjEXAK2pDd2j8r9gKI3L+44sIxjkYzS5aHmD1+K57RNJ3sFyT4n22NH6QFPqqeVdRBxN54H3kckUUCrFKCcJw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.6.2.tgz", + "integrity": "sha512-/PEOo6evlgrQOfKhMNtC/4Gk8ai3hySXXEtjUzCqZe/4iAbMdqelpp7qvZEDyLxlZS+BjOMQFpq2gXI/BPaAqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.6.2.tgz", + "integrity": "sha512-FVlWD+mDDz8nN5LWKRWUhhb7wKIK0VE3oXQvQBX7Fwwj00CvqMVhMyiz1iz8O5aZGmnkhFlKSwJPrUvMh5prQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.3.tgz", + "integrity": "sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.2.1" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz", + "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.3.tgz", + "integrity": "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.6.tgz", + "integrity": "sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.2", + "@csstools/css-calc": "^3.4.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz", + "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz", + "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz", + "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@exodus/bytes": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz", + "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@fontsource-variable/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/sora": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/sora/-/sora-5.3.0.tgz", + "integrity": "sha512-h8kHta4Z8SkfUGeI82TUy1n3rbrKpBRGwuk4t4qCwto5oEHxlbOFD2yGP+Df5fbmTXwKGn+o667vYp0E4K8Whw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "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.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "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/@oxc-project/types": { + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", + "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.2.0.tgz", + "integrity": "sha512-KABY+VMIRPJL+Vyzv/335FEhXXD/xDv8BMb1kl48/aXJZMH0LtHCTx5ga5RnxaEP3nZHtmTLf1P9vanmFX46bw==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.17.tgz", + "integrity": "sha512-67jjcVvxjXCCStnTW3Z+M7jmaFMuMR7M2JgpDzH4w7JSRrikmzhjfYgP/09ECb8n7whnsgh1ifJ+Ie96qEj+pQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "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-collection": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.2.0.tgz", + "integrity": "sha512-+yiQzqcc7Sy7/5s0jrMCjpPRFB3538VNrXPWdTLZhE/SCfgEws4aI6lHWN/vvcnyhHndROWBzqmqB4QBrITZ9Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "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.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.3.0.tgz", + "integrity": "sha512-v5AYEocMISkbJ+1fTXzZpiFv7NRr3D2WiYbLIKf+FkmJ8VkP6d503eV2n34MFussHXY1pcoq3yfJiQH77dc4Mg==", + "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.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.3.1.tgz", + "integrity": "sha512-+0E4qvVTwjAWra9sEhJ+9BTsvHE00zkUuNw1VGh7S+Gc13dfYk+S1PVQdchWDj+XYHhAai7Z7IpzXVDCGsDplg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "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-direction": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.2.0.tgz", + "integrity": "sha512-FtRA52a/r+LKStFT+neLUm+8vSe7wbzkbQGGeU8ixFiwC8NU14Evoq9ATECqL3eGbcUzJR4fyfA4KkK/q6dfqg==", + "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-dismissable-layer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.2.1.tgz", + "integrity": "sha512-KIfDcAsx+oVylFZ4xryVKusJUrLLhTQmfnJW4kwk4Km5myvhHCrj04hnWWUC166+xD4DHdH8tQGylwYX1h6+Pg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "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-dropdown-menu": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.2.1.tgz", + "integrity": "sha512-XsHcmYG2AcqnvTqrOXXWcMrhquEIlqucizuDH1MV/qMRW+DndpBCMvNeTRXOxqmG1H5j89M/+I0/34Zdd+zDcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.2.1", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.2.0.tgz", + "integrity": "sha512-MPm5R0iRGUDrDnPNxmLBkv+X7pN4XKzt+MbWJEuJQsH3NFH4omrbpXpucyYCCKTSt+na/ZxGhEWQrWTZ0285cA==", + "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.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.3.1.tgz", + "integrity": "sha512-ugeMhbXt4y5S2T1GnIqbWDKEN962PDwnX1fN0eKOOgf5f63Dh2mn8F1FUySlgW0iF6GiVsRF32DzuDIWTUyQJg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5" + }, + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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-menu": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.2.1.tgz", + "integrity": "sha512-G34BsU90SBj5foYpz8pQqq/Q0Dlfyyl6KitWEfRwXC0QvfwhWNAwS49DWP2+rRJ0iwv20Z2Qx7Sk+T03bGo5JA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "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-popover": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.3.1.tgz", + "integrity": "sha512-ZNxvXRx5W2Wos4fXCXb5lpdqN0Z5chf+mStOUOTr4ik7d+62MRvoe9rzCA/eGNg36lxXqSrKEKmwuZEcUrIstw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "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.4.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.4.0.tgz", + "integrity": "sha512-yCWRvLeGv9Pz3+efwHqGPp70mZ56r3kfIgQVF56Oj2aAa4uP/Tb+IE59wNcc+758rctqbCKx4LbK8II/r7Bn0Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.17", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.5", + "@radix-ui/rect": "1.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-portal": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.2.0.tgz", + "integrity": "sha512-StX7xXnaRU41VvFtMm9zlhVGz8XF6GmWWTXftkQaY+tqVkbs/R9UmAp5mLzHHapk5hM9RMZQvO/3usyIP/g42g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.11.tgz", + "integrity": "sha512-Z7/atlkab36Z8hm+V8zYc79r0W+tiOB1o0tun1By94JyadkekLQmtEAnBSP9XKXBNQsStoStS4+gX10JpS5akA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.2.0.tgz", + "integrity": "sha512-HCTSPMNqv55Qk0IX4geJbpyXazmqulcimwuyjxTkKi29v+QZbG2Zho9BnbBq4YS2PF+KlNb2CXoLz66uUmblkg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.5.0" + }, + "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-roving-focus": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.2.1.tgz", + "integrity": "sha512-UwraORzVUeEPMclhO1p0vz0qI+TbdDLO9i+FiOqhJyHKwdpCRCck9yVEscUnAMIzCiuNoQa8D4A9bIpOPXGt6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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-select": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.4.1.tgz", + "integrity": "sha512-888ZwW0Md6GyptuqYABRX9nLKgoYfcooCyBBvVaX5Jp3e+8Jp2i4Zo/Uy50Hm+Ysn2JfloQXstW4U36w8teVtg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-collection": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-guards": "1.2.0", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-callback-ref": "1.1.5", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.13", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "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.5.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.5.0.tgz", + "integrity": "sha512-tyYd9ddPXAysAbdbD9eYxgwpCSv6f3PncqVSNKQFSR5lcJVLz3cb/pANDoYrLiHBpXv7skortyUgGUWsYuPPFQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "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-tabs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.2.1.tgz", + "integrity": "sha512-hRoJlode+hg5ZNlR+3tkfkUliIybdTpD5lkAvHQ9yaXseGf7XVO9Ib2GPEIOuaJJbo7YWYbNpDDRfo8Dhf6/xA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-direction": "1.2.0", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-roving-focus": "1.2.1", + "@radix-ui/react-use-controllable-state": "1.3.0" + }, + "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-tooltip": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.4.1.tgz", + "integrity": "sha512-NxLFGSFtZ78zVfcoOhTwR1xwqo5qx1Jh8Pw6FMZ9McCaqfRzz7/gIRahs2Oy5XlPgQ+8QkUcKbN5B0U44J/kCg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.3.0", + "@radix-ui/react-dismissable-layer": "1.2.1", + "@radix-ui/react-focus-scope": "1.3.1", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.4.0", + "@radix-ui/react-portal": "1.2.0", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.2.0", + "@radix-ui/react-slot": "1.5.0", + "@radix-ui/react-use-controllable-state": "1.3.0", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.13" + }, + "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-use-callback-ref": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.5.tgz", + "integrity": "sha512-Y5GBf4NKeADoXI1geiMlMuMoURT3k9MizzzKhGb/m55H94aGigZ01PIpkvsO6lML13BA6XYAXSJ6gza7vTn5nw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.3.0.tgz", + "integrity": "sha512-TAcBI7bw2ljYCbv4+zuTSd6zICu8pRt6SDN2vJ/Z06K2MXh6I9WYFe/A9ovlFDtR5HVHGm5iDxDoABm/++dbKg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.2.0", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "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-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "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.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.5.tgz", + "integrity": "sha512-V1McDQkMGzgivwqdu7u96APs7oi0Jvj9LTzgq3WWrDZqxWdAqU9yZnZQqk5OizGFR8badUQwZsrlao7MOlWYOA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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-visually-hidden": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.13.tgz", + "integrity": "sha512-feijiN1oPk4rck4u9Dm9tsU4mT6Wp9oeEEhaz1WUEs1VuqLf8xzsFG+Qk+uWq6TnMH9O+WRECRg0NftyP9E4Fw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.2.0" + }, + "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/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.13.0.tgz", + "integrity": "sha512-f6jwVRfwW30+tnllz4YcsHHpPYYOE7abcHkyUCovD+P7DyhdAI5prz2Fh/8TLhakjpN+7aIEion1s5L/tvlYlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", + "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz", + "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz", + "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz", + "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz", + "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz", + "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz", + "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz", + "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz", + "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz", + "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz", + "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz", + "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz", + "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz", + "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz", + "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.104.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.104.5.tgz", + "integrity": "sha512-Q94gI8Opd+CbdVNOdj8ZCyV8WqLYitrDq1t96Xc8WZT6lY0r5iRnJLXYDxMex7EEf2v+mXvn4FcjaBKSxXZQ9A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.104.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.104.5.tgz", + "integrity": "sha512-RORgFe052U+93weaNuqg7qHq7vNVNzXN/VrXqD4bbAuQCWUqOl2lV5Ui9flWk5CVvMyL/Bh27QWhzwH0aaOx9g==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.104.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@types/react": "^18 || ^19", + "react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "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.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "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.8", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.8.tgz", + "integrity": "sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "dev": true, + "license": "MIT" + }, + "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", + "peer": true + }, + "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-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "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-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "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-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz", + "integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "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.9", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.9.tgz", + "integrity": "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==", + "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.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.186.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.186.0.tgz", + "integrity": "sha512-mxYSBpDC+D0pLfSP6sW4WZTcT+nrtmZcimMqnVmy36Hte3XpeYSrvgg4TRdaM1GemGog1AWzI5qL2VoIfMXbJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.3", + "meshoptimizer": "~1.1.1" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz", + "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.152.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz", + "integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.5", + "estree-walker": "^3.0.3", + "magic-string": "^1.4.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz", + "integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xyflow/react": { + "version": "12.12.0", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.12.0.tgz", + "integrity": "sha512-74oXI0Rgm1Eu33cHQAd74J6oVgCH9mbFyTVQbsGgp2kmWwh0HdtBr2VP04z8mbPD+M/xO69nqG5dDEt7/EN2rQ==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.83", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/react/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 + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.83", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.83.tgz", + "integrity": "sha512-a//eDjez3WZF47aaPTfVUDD0OINOq+WVwBZCRTqG+m0a5euX8EwlJ4jMY1Wxz0Ocj4xOeekJ70V22Pq0ll8RJw==", + "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/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", + "peer": true, + "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", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "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/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "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/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "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/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-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "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-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "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-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "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", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "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": "8.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-8.0.0.tgz", + "integrity": "sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.2" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "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/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "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-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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/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", + "peer": true + }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", + "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, + "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/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fake-indexeddb": { + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.6.tgz", + "integrity": "sha512-ihhmr0KoET51jT8R4YhbUYuLLvr1qqO6INISygtN16i8loR8kAxtAUr4+EKHTSMjL+lggaAn4OC7DRqzrNgnuQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "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/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/framer-motion": { + "version": "14.6.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.6.0.tgz", + "integrity": "sha512-MzBgZxGgoHPfSLEXmo4o5XlZvb+nVRFdHtGjVhm6LMIEJKTt/8hNO023euL5mYyzc8llV/hQTfjnN68tNRHalw==", + "license": "MIT", + "dependencies": { + "motion-dom": "14.6.0", + "motion-utils": "14.6.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "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/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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/gsap": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", + "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, + "node_modules/html-encoding-sniffer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-7.0.0.tgz", + "integrity": "sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/idb": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.4.tgz", + "integrity": "sha512-jd0ITCrN+Zah4J7R/Z2jnR3W7A4BXDAjmo62MwWuSjxCiueUAf7X5rbodEdidWzeknwXHmLpdZkoiiCppNi30Q==", + "license": "ISC" + }, + "node_modules/immer": { + "version": "11.2.9", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.2.9.tgz", + "integrity": "sha512-IQ4UEtXjZjyefyWjtLEDXUSrF6T2hwjKPi6BIJl3bY/2XXW24d6cNGdXJSBOA/+KNnuiXkZY6DR0C3efolhitA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "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/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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", + "peer": true + }, + "node_modules/jsdom": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.3.tgz", + "integrity": "sha512-hQMUPvRgN7KxsfIytDQji7bimuJeRQa6Qo5+iuiZQk5CafeRxLIA10/cG5YCBFNpD9RO3oqIiN4CO1Y44kxqXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^7.1.3", + "@asamuzakjp/dom-selector": "^10.0.3", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.15", + "@exodus/bytes": "^1.16.0", + "css-tree": "^3.2.1", + "data-urls": "^8.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^7.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.3", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "tough-cookie": "^6.0.2", + "undici": "^8.12.1", + "w3c-xmlserializer": "^6.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.2.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lenis": { + "version": "1.3.27", + "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.27.tgz", + "integrity": "sha512-QIHNH4LIfWyLfgH/5K0aTGZgyFJJ7GfHNcaH/AIgyKjb8gstLh5siNGvrVqz1w5ClLXzOvllBVk7vby8JzUVPQ==", + "license": "MIT", + "workspaces": [ + "packages/*", + "playground", + "playground/*" + ], + "funding": { + "type": "github", + "url": "https://github.com/sponsors/darkroomengineering" + }, + "peerDependencies": { + "@nuxt/kit": ">=3.0.0", + "react": ">=17.0.0", + "vue": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "react": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lucide-react": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.61.0.tgz", + "integrity": "sha512-v2SQPPPNjnr+BlhL9Spuzmo5yR+hLkftfuE4E3jaxdNPgSH0hJKPcLJBP7yAta87jQNPsSERS59PJjCJ/CorBA==", + "license": "ISC", + "peerDependencies": { + "@types/react": "*", + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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", + "peer": true, + "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/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/meshoptimizer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", + "dev": true, + "license": "MIT" + }, + "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/motion-dom": { + "version": "14.6.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.6.0.tgz", + "integrity": "sha512-lIojTl9aYSPIkeOOSzBliy1nNvpdO3JtVzW2WgTXQlOP4ZMuJhtWO3fP7OD4TVFwlRucdvxlDSMkL+fEQnQ52A==", + "license": "MIT", + "dependencies": { + "motion-utils": "14.6.0" + } + }, + "node_modules/motion-utils": { + "version": "14.6.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-14.6.0.tgz", + "integrity": "sha512-bZJo2AmQ2aOFkaato6VXrTbkPG0XhMzivWUWSaI59pxyllj8QMQUFZ84MYRQnPkK+7dr4KldyJwF2+RM0sdq/w==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "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/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "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": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "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", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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", + "peer": true, + "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/pretty-format/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", + "peer": true + }, + "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/react": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.1" + } + }, + "node_modules/react-is": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.1.tgz", + "integrity": "sha512-RRieF6IyxQBkcK6pP8xOHprLTPW6QjRkbGdImtqNme+/hYVwCZkLQLdlZGOhUqXwkS9IEynaGvPJtv6XB/cz0Q==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "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.18.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", + "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", + "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-router-dom": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz", + "integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "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/recharts": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", + "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.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/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz", + "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.155.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.15", + "@rolldown/binding-android-arm64": "1.2.15", + "@rolldown/binding-darwin-arm64": "1.2.15", + "@rolldown/binding-darwin-x64": "1.2.15", + "@rolldown/binding-freebsd-x64": "1.2.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", + "@rolldown/binding-linux-arm64-gnu": "1.2.15", + "@rolldown/binding-linux-arm64-musl": "1.2.15", + "@rolldown/binding-linux-ppc64-gnu": "1.2.15", + "@rolldown/binding-linux-s390x-gnu": "1.2.15", + "@rolldown/binding-linux-x64-gnu": "1.2.15", + "@rolldown/binding-linux-x64-musl": "1.2.15", + "@rolldown/binding-openharmony-arm64": "1.2.15", + "@rolldown/binding-win32-arm64-msvc": "1.2.15", + "@rolldown/binding-win32-x64-msvc": "1.2.15" + } + }, + "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.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "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/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "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/tailwind-merge": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.7.0.tgz", + "integrity": "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/three": { + "version": "0.186.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.186.1.tgz", + "integrity": "sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "7.4.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.19.tgz", + "integrity": "sha512-48dH6aSpQnpbzDUc4vIQZ1VjB8tkK+e8c1yx05/SZK+rkssgjEOlxDd/yU8/a4vGYQ9vRgJr9dwMq9TJ0b3X2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.19.tgz", + "integrity": "sha512-goNXV6Ho9TIOfpJZanTd6CKG5DyYgYAL+k+22T0SLh8l6+KHIfBUPFuePuSZv05UttIaaLmI9tb7TmGIdK5AkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-7.0.0.tgz", + "integrity": "sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.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/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici": { + "version": "8.12.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz", + "integrity": "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "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.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", + "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.30", + "rolldown": "~1.2.14", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vitest": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz", + "integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.5", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.5", + "@vitest/browser-preview": "5.0.5", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.5", + "@vitest/coverage-v8": "5.0.5", + "@vitest/ui": "5.0.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-6.0.0.tgz", + "integrity": "sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.3.0.tgz", + "integrity": "sha512-KwOAJrpf+GTgBvcBQJcP38sMF0DsQKdwtG+TxM2pW44P6E5k0hDbWrDmyuJDIF0Uef0aQq0ZoJs+5oI+k8ddhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.16.0", + "tr46": "^7.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "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/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.16", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.16.tgz", + "integrity": "sha512-Uv7e327ihlCfayfja3hDc0M64Upev1LWBKpRobdK4NyovDDPeeX4imWXc/wzC1yc6fkYPoAY/ncKVPAs2jrtQw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 0000000..53566e7 --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,62 @@ +{ + "name": "@opencluster/dashboard", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -p tsconfig.json && vite build", + "preview": "vite preview", + "typecheck": "tsc -p tsconfig.json", + "lint": "biome check src tests", + "format": "biome format --write src tests", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@fontsource-variable/inter": "^5.3.0", + "@fontsource-variable/sora": "^5.3.0", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-query": "^5.104.5", + "@xyflow/react": "^12.12.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "framer-motion": "^14.6.0", + "gsap": "^3.15.0", + "idb": "^8.0.4", + "lenis": "^1.3.27", + "lucide-react": "^1.61.0", + "react": "^19.3.1", + "react-dom": "^19.3.1", + "react-router-dom": "^7.18.4", + "recharts": "^3.10.1", + "tailwind-merge": "^3.6.0", + "three": "^0.186.1", + "zod": "^4.6.7", + "zustand": "^5.0.16" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.5", + "@tailwindcss/vite": "^4.3.3", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^26.6.5", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@types/three": "^0.186.0", + "@vitejs/plugin-react": "^6.1.2", + "fake-indexeddb": "^6.2.6", + "jsdom": "^30.1.3", + "typescript": "^7.0.2", + "vite": "^8.3.5", + "vitest": "^5.0.5" + } +} diff --git a/dashboard/src/components/AppErrorBoundary.tsx b/dashboard/src/components/AppErrorBoundary.tsx new file mode 100644 index 0000000..dfdd779 --- /dev/null +++ b/dashboard/src/components/AppErrorBoundary.tsx @@ -0,0 +1,34 @@ +import { Component, type ErrorInfo, type ReactNode } from "react"; + +type Props = { children: ReactNode }; +type State = { error: Error | null }; + +export class AppErrorBoundary extends Component { + state: State = { error: null }; + + static getDerivedStateFromError(error: Error): State { + return { error }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + console.error("OpenCluster dashboard render failure", { + error, + componentStack: info.componentStack, + }); + } + + render() { + if (this.state.error) { + return ( +
+

OpenCluster could not render this view

+

{this.state.error.message}

+ +
+ ); + } + return this.props.children; + } +} diff --git a/dashboard/src/components/AppShell.tsx b/dashboard/src/components/AppShell.tsx new file mode 100644 index 0000000..069ec7a --- /dev/null +++ b/dashboard/src/components/AppShell.tsx @@ -0,0 +1,432 @@ +import { motion, useReducedMotion } from "framer-motion"; +import Lenis from "lenis"; +import { + Activity, + Bell, + BrainCircuit, + ChevronDown, + CircleHelp, + Command, + Cpu, + Gauge, + Grid2X2, + Layers3, + Menu, + Search, + Settings, + Sparkles, + TerminalSquare, + Workflow, + X, +} from "lucide-react"; +import type { KeyboardEvent as ReactKeyboardEvent } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; +import { useDashboardStore } from "../state/useDashboardStore"; +import { BranchedMenu } from "./BranchedMenu"; +import { Dock } from "./Dock"; +import { Button } from "./ui"; + +const navItems = [ + { label: "Home", to: "/", icon: Grid2X2 }, + { label: "Mission Control", to: "/mission-control", icon: Sparkles }, + { label: "Clusters", to: "/clusters", icon: Layers3 }, + { label: "Nodes", to: "/nodes", icon: Cpu }, + { label: "Agents", to: "/agents", icon: BrainCircuit }, + { label: "Automation", to: "/automation", icon: Workflow }, + { label: "AI Software", to: "/software", icon: TerminalSquare }, + { label: "Tasks", to: "/tasks", icon: Activity }, + { label: "Monitoring", to: "/monitoring", icon: Gauge }, + { label: "Activity", to: "/activity", icon: Bell }, +]; +const routeNames: Record = { + "/": "Home", + "/mission-control": "Mission Control", + "/clusters": "Clusters", + "/nodes": "Nodes", + "/agents": "Agents", + "/automation": "Automation", + "/software": "AI Software", + "/tasks": "Tasks", + "/monitoring": "Monitoring", + "/activity": "Activity", + "/settings": "Settings", +}; +const branchedItems = [ + { label: "Overview", value: "/", icon: }, + { + label: "Operations", + children: [ + { + value: "/mission-control", + label: "Mission Control", + icon: , + }, + { value: "/clusters", label: "Clusters", icon: }, + { value: "/nodes", label: "Nodes", icon: }, + { value: "/monitoring", label: "Monitoring", icon: }, + { value: "/activity", label: "Activity", icon: }, + ], + }, + { + label: "Orchestration", + children: [ + { value: "/agents", label: "Agents", icon: }, + { + value: "/automation", + label: "Automation", + icon: , + }, + { + value: "/software", + label: "AI Software", + icon: , + }, + { value: "/tasks", label: "Tasks", icon: }, + ], + }, +]; +function keepFocusInDialog(event: ReactKeyboardEvent) { + if (event.key !== "Tab") return; + const focusable = Array.from( + event.currentTarget.querySelectorAll( + 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', + ), + ); + const first = focusable[0]; + const last = focusable.at(-1); + if (event.shiftKey && document.activeElement === first) { + event.preventDefault(); + last?.focus(); + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first?.focus(); + } +} + +export function AppShell() { + const location = useLocation(); + const navigate = useNavigate(); + const clusters = useDashboardStore((s) => s.clusters); + const selectedClusterId = useDashboardStore((s) => s.selectedClusterId); + const selectCluster = useDashboardStore((s) => s.selectCluster); + const collapsed = useDashboardStore((s) => s.sidebarCollapsed); + const toggleSidebar = useDashboardStore((s) => s.toggleSidebar); + const theme = useDashboardStore((s) => s.theme); + const demoMotion = useDashboardStore((s) => s.demoMotion); + const compactDashboard = useDashboardStore((s) => s.compactDashboard); + const performanceMode = useDashboardStore((s) => s.performanceMode); + const cinematicMotion = useDashboardStore((s) => s.cinematicMotion); + const displayName = useDashboardStore((s) => s.displayName); + const persistenceError = useDashboardStore((s) => s.persistenceError); + const clearPersistenceError = useDashboardStore( + (s) => s.clearPersistenceError, + ); + const prefersReducedMotion = useReducedMotion(); + const [searchOpen, setSearchOpen] = useState(false); + const [query, setQuery] = useState(""); + const searchInput = useRef(null); + useEffect(() => { + const media = window.matchMedia("(prefers-color-scheme: light)"); + const updateTheme = () => { + document.documentElement.dataset.theme = + theme === "system" ? (media.matches ? "light" : "dark") : theme; + }; + updateTheme(); + media.addEventListener("change", updateTheme); + document.documentElement.dataset.motion = demoMotion ? "full" : "reduced"; + document.documentElement.dataset.performance = performanceMode + ? "on" + : "off"; + return () => media.removeEventListener("change", updateTheme); + }, [theme, demoMotion, performanceMode]); + useEffect(() => { + if (!demoMotion || performanceMode || prefersReducedMotion) return; + const lenis = new Lenis({ + autoRaf: true, + anchors: true, + allowNestedScroll: true, + stopInertiaOnNavigate: true, + }); + const handleVisibility = () => { + if (document.hidden) lenis.stop(); + else lenis.start(); + }; + document.addEventListener("visibilitychange", handleVisibility); + return () => { + document.removeEventListener("visibilitychange", handleVisibility); + lenis.destroy(); + }; + }, [demoMotion, performanceMode, prefersReducedMotion]); + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { + event.preventDefault(); + setSearchOpen((open) => !open); + } + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, []); + useEffect(() => { + if (searchOpen) searchInput.current?.focus(); + }, [searchOpen]); + const selected = + clusters.find((cluster) => cluster.id === selectedClusterId) ?? clusters[0]; + const commands = useMemo( + () => + [ + ...navItems.map((item) => ({ label: item.label, to: item.to })), + { label: "Settings", to: "/settings" }, + ].filter((item) => + item.label.toLowerCase().includes(query.toLowerCase()), + ), + [query], + ); + const openSearch = () => setSearchOpen(true); + return ( +
+ +
+
+
+ + OpenCluster + / + {routeNames[location.pathname] ?? "Dashboard"} +
+
+ + + +
+ OC +
+
+
+
+
+ {persistenceError && ( +
+ + Local changes could not be saved: {persistenceError} + + +
+ )} + + + +
+
+
+ + OpenCluster LOCAL MODE + + + Hardware telemetry uses seed data · Backend services are not + connected + +
+
+ + {searchOpen && ( +
+ {/* ORIGINAL (preserved, not deleted — disabled by comment): onMouseDown={(e) => { if (e.target === e.currentTarget) setSearchOpen(false); }} */} + +
+
PAGES
+ {commands.length ? ( + commands.map((item) => ( + + )) + ) : ( +

No pages match “{query}”

+ )} +
+ ↑↓ navigate + ↵ open + ESC close +
+ +
+ )} + + ); +} diff --git a/dashboard/src/components/BranchedMenu.css b/dashboard/src/components/BranchedMenu.css new file mode 100644 index 0000000..6fc79b3 --- /dev/null +++ b/dashboard/src/components/BranchedMenu.css @@ -0,0 +1,131 @@ +.branched-menu { + --bm-line: #1c2a34; + --bm-accent: #62d3a5; + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + padding-left: 13px; +} +.branched-menu::before, +.branched-menu__marker { + position: absolute; + left: 2px; + width: 1px; + border-radius: 1px; + content: ""; +} +.branched-menu::before { + top: 10px; + bottom: 0; + background: linear-gradient(var(--bm-line) 0 85%, transparent); +} +.branched-menu__marker { + top: 0; + z-index: 1; + height: 16px; + background: var(--bm-accent); + opacity: 0; + transition: + transform 220ms cubic-bezier(0.23, 1, 0.32, 1), + opacity 150ms ease; +} +.branched-menu__marker[data-on] { + opacity: 1; +} +.branched-menu__head, +.branched-menu__item { + display: flex; + align-items: center; + width: 100%; + min-width: 0; + border: 0; + background: transparent; + color: #81909b; + text-align: left; + transition: color 160ms ease; +} +.branched-menu__head { + min-height: 35px; + padding: 8px 4px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; +} +.branched-menu__section[data-open] > .branched-menu__head, +.branched-menu__head[aria-current="page"], +.branched-menu__item[aria-current="page"] { + color: #a5e4ca; +} +.branched-menu__body { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 240ms cubic-bezier(0.23, 1, 0.32, 1); +} +.branched-menu__section[data-open] .branched-menu__body { + grid-template-rows: 1fr; +} +.branched-menu__fold { + min-height: 0; + overflow: hidden; +} +.branched-menu__tree { + position: relative; + padding: 6px 0; + box-sizing: border-box; +} +.branched-menu__lines { + position: absolute; + top: 0; + left: 0; + overflow: visible; + pointer-events: none; +} +.branched-menu__base, +.branched-menu__reach { + fill: none; + stroke-width: 1px; + stroke-linecap: round; + stroke-linejoin: round; +} +.branched-menu__base { + stroke: var(--bm-line); +} +.branched-menu__reach { + stroke: var(--bm-accent); + transition: stroke-dashoffset 400ms cubic-bezier(0.23, 1, 0.32, 1); +} +.branched-menu__item { + height: 35px; + gap: 10px; + padding: 0 4px 0 30px; + border-radius: 6px; + font-size: 12px; + cursor: pointer; +} +.branched-menu__item:hover, +.branched-menu__head:hover { + color: #dce5eb; +} +.branched-menu__item[aria-current="page"] { + background: #152a28; + box-shadow: inset 2px 0 #5dd3a3; +} +.branched-menu__icon { + display: inline-flex; + flex: 0 0 16px; + color: #758592; +} +.branched-menu__item[aria-current="page"] .branched-menu__icon { + color: #6ed9ad; +} +@media (prefers-reduced-motion: reduce) { + .branched-menu__marker, + .branched-menu__body { + transition: none; + } + .branched-menu__reach { + transition: none; + } +} diff --git a/dashboard/src/components/BranchedMenu.tsx b/dashboard/src/components/BranchedMenu.tsx new file mode 100644 index 0000000..588d06c --- /dev/null +++ b/dashboard/src/components/BranchedMenu.tsx @@ -0,0 +1,168 @@ +import type { ReactNode } from "react"; +import { useLayoutEffect, useRef, useState } from "react"; +import "./BranchedMenu.css"; + +export type BranchedMenuEntry = { + value: string; + label: string; + icon?: ReactNode; +}; +export type BranchedMenuSection = { + label: string; + children?: BranchedMenuEntry[]; + value?: string; +}; + +export function BranchedMenu({ + items, + active, + onSelect, +}: { + items: BranchedMenuSection[]; + active: string; + onSelect: (value: string) => void; +}) { + const [open, setOpen] = useState( + () => new Set(items.map((_, index) => index)), + ); + const marker = useRef(null); + const heads = useRef>([]); + const sections = useRef(null); + const sectionIndex = items.findIndex((section) => + section.children?.some((entry) => entry.value === active), + ); + useLayoutEffect(() => { + const place = () => { + const selected = marker.current; + if (!selected) return; + const head = heads.current[sectionIndex]; + const sectionOffset = head?.parentElement?.offsetTop ?? 0; + selected.style.transform = head + ? `translateY(${sectionOffset + head.offsetTop + (head.offsetHeight - 16) / 2}px)` + : "translateY(-20px)"; + selected.toggleAttribute( + "data-on", + Boolean(head && open.has(sectionIndex)), + ); + }; + place(); + if (!sections.current || typeof ResizeObserver === "undefined") return; + const observer = new ResizeObserver(place); + observer.observe(sections.current); + return () => observer.disconnect(); + }, [sectionIndex, open]); + const select = (value: string) => onSelect(value); + + return ( + + ); +} diff --git a/dashboard/src/components/Charts.tsx b/dashboard/src/components/Charts.tsx new file mode 100644 index 0000000..b90f809 --- /dev/null +++ b/dashboard/src/components/Charts.tsx @@ -0,0 +1,137 @@ +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import type { TelemetryPoint } from "../data/models"; +import { useLocalCollection } from "../data/useDashboardData"; + +export function ResourceChart({ + metric = "cpu", + compact = false, + range = "24h", + compareMemory = false, +}: { + metric?: "cpu" | "memory" | "network"; + compact?: boolean; + range?: "15m" | "1h" | "24h" | "7d"; + compareMemory?: boolean; +}) { + const { data: telemetry = [] } = + useLocalCollection("telemetry"); + const series = + metric === "network" + ? [ + { key: "networkIn", name: "Inbound", color: "#6cb9ff" }, + { key: "networkOut", name: "Outbound", color: "#a78bfa" }, + ] + : [ + { + key: metric, + name: metric === "cpu" ? "CPU" : "Memory", + color: metric === "cpu" ? "#62d3a5" : "#65b9ff", + }, + ]; + if (metric === "cpu" && compareMemory) { + series.push({ key: "memory", name: "Memory", color: "#65b9ff" }); + } + const visiblePoints = + range === "15m" + ? telemetry.slice(-4) + : range === "1h" + ? telemetry.slice(-6) + : telemetry; + const points = + range === "7d" + ? Array.from( + { length: 14 }, + (_, index) => telemetry[Math.floor((index * telemetry.length) / 14)], + ) + : visiblePoints; + const data = points.map((point, index) => ({ + ...point, + time: + range === "7d" + ? `Day ${Math.floor(index / 2) + 1}` + : range === "1h" + ? `${index * 10} min` + : range === "15m" + ? `${index * 5} min` + : point.time, + })); + if (!data.length) + return ( +
+ No telemetry samples are stored locally. +
+ ); + return ( +
+ + + + {series.map((s) => ( + + + + + ))} + + + + + + {series.map((s) => ( + + ))} + + +
+ ); +} diff --git a/dashboard/src/components/Counter.css b/dashboard/src/components/Counter.css new file mode 100644 index 0000000..453db7d --- /dev/null +++ b/dashboard/src/components/Counter.css @@ -0,0 +1,9 @@ +.counter { + display: inline-flex; + align-items: baseline; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} +.counter > span { + display: inline-block; +} diff --git a/dashboard/src/components/Counter.tsx b/dashboard/src/components/Counter.tsx new file mode 100644 index 0000000..77241c0 --- /dev/null +++ b/dashboard/src/components/Counter.tsx @@ -0,0 +1,133 @@ +import type { CSSProperties, ReactNode } from "react"; +import { useEffect, useMemo, useRef } from "react"; +import "./Counter.css"; + +export type CounterProps = { + value?: number; + locale?: string; + format?: Intl.NumberFormatOptions; + decimals?: number; + minDigits?: number; + prefix?: ReactNode; + suffix?: ReactNode; + duration?: number; + fontSize?: number | string; + fontWeight?: number | string; + textColor?: string; + className?: string; + style?: CSSProperties; +}; + +export function Counter({ + value = 0, + locale = "en-US", + format, + decimals, + minDigits = 1, + prefix, + suffix, + duration = 700, + fontSize = "inherit", + fontWeight = "inherit", + textColor = "inherit", + className = "", + style, +}: CounterProps) { + const number = Number.isFinite(value) ? value : 0; + const numberRef = useRef(null); + const previousValue = useRef(number); + const numberFormat = useMemo(() => { + const options = { ...format }; + if (decimals !== undefined) { + options.minimumFractionDigits = Math.max( + 0, + Math.min(20, Math.round(decimals)), + ); + options.maximumFractionDigits = options.minimumFractionDigits; + } + options.minimumIntegerDigits = Math.max( + 1, + Math.min(21, Math.round(minDigits)), + ); + try { + return new Intl.NumberFormat(locale, options); + } catch { + return new Intl.NumberFormat("en-US", options); + } + }, [locale, format, decimals, minDigits]); + const formatted = numberFormat.format(number); + const motionDuration = Math.max(0, duration); + + useEffect(() => { + const target = numberRef.current; + if (!target) return; + let frame = 0; + let observer: IntersectionObserver | undefined; + let started = false; + const finish = () => { + target.textContent = formatted; + previousValue.current = number; + }; + const run = () => { + if (started) return; + started = true; + if ( + window.matchMedia("(prefers-reduced-motion: reduce)").matches || + document.hidden || + motionDuration === 0 + ) { + finish(); + return; + } + const previous = previousValue.current; + previousValue.current = number; + const start = performance.now(); + const tick = (now: number) => { + const progress = Math.min(1, (now - start) / motionDuration); + const eased = 1 - (1 - progress) ** 4; + target.textContent = numberFormat.format( + previous + (number - previous) * eased, + ); + if (progress < 1) frame = requestAnimationFrame(tick); + }; + frame = requestAnimationFrame(tick); + }; + if (typeof IntersectionObserver === "undefined") run(); + else { + observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + observer?.disconnect(); + run(); + } + }, + { threshold: 0.1 }, + ); + observer.observe(target); + } + return () => { + cancelAnimationFrame(frame); + observer?.disconnect(); + }; + }, [formatted, motionDuration, number, numberFormat]); + + return ( + + + + + + ); +} diff --git a/dashboard/src/components/Dock.css b/dashboard/src/components/Dock.css new file mode 100644 index 0000000..bac75da --- /dev/null +++ b/dashboard/src/components/Dock.css @@ -0,0 +1,119 @@ +.quick-dock { + position: fixed; + z-index: 8; + right: max(24px, env(safe-area-inset-right)); + bottom: max(48px, env(safe-area-inset-bottom)); + left: calc(248px + (100vw - 248px) / 2); + display: flex; + width: max-content; + max-width: calc(100vw - 290px); + align-items: center; + justify-content: center; + gap: 5px; + padding: 7px; + transform: translateX(-50%); + border: 1px solid rgb(161 207 208 / 15%); + border-radius: 16px; + background: rgb(13 23 29 / 86%); + box-shadow: + 0 12px 40px rgb(0 0 0 / 38%), + inset 0 1px rgb(255 255 255 / 5%); + -webkit-backdrop-filter: blur(18px) saturate(1.35); + backdrop-filter: blur(18px) saturate(1.35); +} +.quick-dock__item { + position: relative; + display: grid; + width: 38px; + height: 38px; + flex: 0 0 38px; + place-items: center; + border: 1px solid transparent; + border-radius: 11px; + color: #8c9ca7; + transition: + transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1), + color 160ms ease, + background 160ms ease; +} +.quick-dock__item:hover, +.quick-dock__item:focus-visible { + z-index: 1; + transform: translateY(-5px) scale(1.12); + color: #dff8ee; + background: #17342f; +} +.quick-dock__item:hover + .quick-dock__item, +.quick-dock__item:has(+ .quick-dock__item:hover) { + transform: translateY(-2px) scale(1.04); +} +.quick-dock__item.is-active { + color: #6ed9ad; + background: #142923; + border-color: rgb(98 211 165 / 20%); +} +.quick-dock__item.is-active::after { + position: absolute; + bottom: -4px; + width: 3px; + height: 3px; + border-radius: 50%; + background: #6ed9ad; + content: ""; +} +.quick-dock__label { + position: absolute; + bottom: calc(100% + 9px); + left: 50%; + padding: 6px 8px; + border: 1px solid #293b45; + border-radius: 6px; + background: #111b22; + color: #dce5eb; + font-size: 10px; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transform: translate(-50%, 3px); + transition: + opacity 140ms ease, + transform 140ms ease; +} +.quick-dock__item:hover .quick-dock__label, +.quick-dock__item:focus-visible .quick-dock__label { + opacity: 1; + transform: translate(-50%, 0); +} +@media (max-width: 820px) { + .quick-dock { + right: 10px; + bottom: max(44px, env(safe-area-inset-bottom)); + left: 50%; + max-width: calc(100vw - 76px); + gap: 2px; + padding: 5px; + } + .quick-dock__item { + width: 30px; + height: 32px; + flex-basis: 30px; + } + .quick-dock__item svg { + width: 15px; + height: 15px; + } + .quick-dock__label { + display: none; + } +} +@media (max-width: 600px) { + .quick-dock__item:nth-child(n + 9) { + display: none; + } +} +@media (prefers-reduced-motion: reduce) { + .quick-dock__item, + .quick-dock__label { + transition: none; + } +} diff --git a/dashboard/src/components/Dock.tsx b/dashboard/src/components/Dock.tsx new file mode 100644 index 0000000..bccea4c --- /dev/null +++ b/dashboard/src/components/Dock.tsx @@ -0,0 +1,52 @@ +import { + Activity, + Bot, + Cpu, + Gauge, + Grid2X2, + Layers3, + ListTodo, + ScanEye, + Settings, + TerminalSquare, + Workflow, +} from "lucide-react"; +import { NavLink } from "react-router-dom"; +import "./Dock.css"; + +const items = [ + { to: "/", label: "Home", icon: Grid2X2 }, + { to: "/mission-control", label: "Mission Control", icon: ScanEye }, + { to: "/clusters", label: "Clusters", icon: Layers3 }, + { to: "/nodes", label: "Nodes", icon: Cpu }, + { to: "/agents", label: "Agents", icon: Bot }, + { to: "/automation", label: "Automation", icon: Workflow }, + { to: "/monitoring", label: "Monitoring", icon: Gauge }, + { to: "/activity", label: "Activity", icon: Activity }, + { to: "/software", label: "AI Software", icon: TerminalSquare }, + { to: "/tasks", label: "Tasks", icon: ListTodo }, + { to: "/settings", label: "Settings", icon: Settings }, +]; + +export function Dock() { + return ( + + ); +} diff --git a/dashboard/src/components/GlobeScene.tsx b/dashboard/src/components/GlobeScene.tsx new file mode 100644 index 0000000..9a42fd6 --- /dev/null +++ b/dashboard/src/components/GlobeScene.tsx @@ -0,0 +1,372 @@ +import { useReducedMotion } from "framer-motion"; +import { Minus, Plus, RotateCcw } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { + BufferAttribute, + BufferGeometry, + Color, + Group, + Line, + LineBasicMaterial, + Mesh, + MeshBasicMaterial, + PerspectiveCamera, + Points, + PointsMaterial, + Raycaster, + Scene, + SphereGeometry, + Vector2, + Vector3, + WebGLRenderer, +} from "three"; +import type { Node } from "../data/models"; +import { useDashboardStore } from "../state/useDashboardStore"; +import { Button, HealthBadge } from "./ui"; + +function nodePosition(index: number, count: number) { + const y = 1 - ((index + 0.5) / count) * 2; + const radius = Math.sqrt(1 - y * y); + const theta = Math.PI * (3 - Math.sqrt(5)) * index; + return new Vector3( + Math.cos(theta) * radius, + y, + Math.sin(theta) * radius, + ).multiplyScalar(1.12); +} + +export function GlobeScene({ nodes }: { nodes: Node[] }) { + const canvasHost = useRef(null); + const cameraRef = useRef(null); + const zoomRef = useRef(4.2); + const [failed, setFailed] = useState(false); + const [zoom, setZoom] = useState(4.2); + const selectedNodeId = useDashboardStore((state) => state.selectedNodeId); + const selectNode = useDashboardStore((state) => state.selectNode); + const demoMotion = useDashboardStore((state) => state.demoMotion); + const performanceMode = useDashboardStore((state) => state.performanceMode); + const visualQuality = useDashboardStore((state) => state.visualQuality); + const reducedMotion = useReducedMotion(); + const animate = demoMotion && !performanceMode && !reducedMotion; + + useEffect(() => { + const host = canvasHost.current; + if (!host || nodes.length === 0) return; + let renderer: WebGLRenderer; + try { + renderer = new WebGLRenderer({ + alpha: true, + antialias: false, + powerPreference: "low-power", + }); + } catch { + setFailed(true); + return; + } + const scene = new Scene(); + const camera = new PerspectiveCamera(42, 1, 0.1, 30); + camera.position.z = zoomRef.current; + cameraRef.current = camera; + const globe = new Group(); + scene.add(globe); + const sphere = new Mesh( + new SphereGeometry(1, 32, 24), + new MeshBasicMaterial({ + color: 0x0b2a3b, + wireframe: true, + transparent: true, + opacity: 0.18, + }), + ); + globe.add(sphere); + const dotPositions: number[] = []; + const dotCount = 360; + for (let i = 0; i < dotCount; i += 1) { + const point = nodePosition(i, dotCount).multiplyScalar(0.995); + dotPositions.push(point.x, point.y, point.z); + } + const dotGeometry = new BufferGeometry(); + dotGeometry.setAttribute( + "position", + new BufferAttribute(new Float32Array(dotPositions), 3), + ); + globe.add( + new Points( + dotGeometry, + new PointsMaterial({ + color: 0x6bbbd0, + size: 0.012, + transparent: true, + opacity: 0.62, + }), + ), + ); + const nodeGeometry = new SphereGeometry(0.035, 10, 8); + const nodeMeshes: Mesh[] = []; + const raycaster = new Raycaster(); + const pointer = new Vector2(); + nodes.forEach((node, index) => { + const position = nodePosition(index, nodes.length); + const color = + node.health === "healthy" + ? 0x77e1d0 + : node.health === "warning" + ? 0xf0bd72 + : 0x77858d; + const line = new Line( + new BufferGeometry().setFromPoints([ + new Vector3(0, 0, 0), + position.clone().multiplyScalar(0.86), + ]), + new LineBasicMaterial({ + color: 0x51abc2, + transparent: true, + opacity: 0.24, + }), + ); + globe.add(line); + const mesh = new Mesh( + nodeGeometry, + new MeshBasicMaterial({ color: new Color(color) }), + ); + mesh.position.copy(position); + mesh.userData.nodeId = node.id; + globe.add(mesh); + nodeMeshes.push(mesh); + }); + setFailed(false); + renderer.setPixelRatio( + Math.min( + window.devicePixelRatio || 1, + performanceMode ? 1 : visualQuality === "high" ? 2 : 1.5, + ), + ); + renderer.setClearColor(0x000000, 0); + host.appendChild(renderer.domElement); + renderer.domElement.setAttribute("aria-hidden", "true"); + let frame = 0; + let resizeFrame = 0; + let pointerFrame = 0; + let dragging = false; + let moved = false; + let lastX = 0; + let lastY = 0; + const resize = () => { + cancelAnimationFrame(resizeFrame); + resizeFrame = requestAnimationFrame(() => { + const { width, height } = host.getBoundingClientRect(); + if (!width || !height) return; + renderer.setSize(width, height, false); + camera.aspect = width / height; + camera.updateProjectionMatrix(); + renderer.render(scene, camera); + }); + }; + const render = () => { + if (document.hidden) return; + if (animate) globe.rotation.y += 0.0012; + renderer.render(scene, camera); + if (animate) frame = requestAnimationFrame(render); + }; + const renderAfterPointer = () => { + if (animate || pointerFrame) return; + pointerFrame = requestAnimationFrame(() => { + pointerFrame = 0; + renderer.render(scene, camera); + }); + }; + const visibility = () => { + cancelAnimationFrame(frame); + if (!document.hidden) render(); + }; + const pointerDown = (event: PointerEvent) => { + dragging = true; + moved = false; + lastX = event.clientX; + lastY = event.clientY; + renderer.domElement.setPointerCapture(event.pointerId); + }; + const pointerMove = (event: PointerEvent) => { + if (!dragging) return; + const dx = event.clientX - lastX; + const dy = event.clientY - lastY; + if (Math.abs(dx) + Math.abs(dy) > 2) moved = true; + globe.rotation.y += dx * 0.006; + globe.rotation.x = Math.max( + -0.7, + Math.min(0.7, globe.rotation.x + dy * 0.004), + ); + lastX = event.clientX; + lastY = event.clientY; + renderAfterPointer(); + }; + const pointerUp = (event: PointerEvent) => { + dragging = false; + if (moved) return; + const rect = renderer.domElement.getBoundingClientRect(); + pointer.set( + ((event.clientX - rect.left) / rect.width) * 2 - 1, + -((event.clientY - rect.top) / rect.height) * 2 + 1, + ); + raycaster.setFromCamera(pointer, camera); + const hit = raycaster.intersectObjects(nodeMeshes)[0]?.object; + if (typeof hit?.userData.nodeId === "string") + selectNode(hit.userData.nodeId); + }; + const wheel = (event: WheelEvent) => { + event.preventDefault(); + camera.position.z = Math.max( + 2.8, + Math.min(7, camera.position.z + Math.sign(event.deltaY) * 0.25), + ); + zoomRef.current = camera.position.z; + setZoom(camera.position.z); + renderAfterPointer(); + }; + const observer = new ResizeObserver(resize); + observer.observe(host); + renderer.domElement.addEventListener("pointerdown", pointerDown); + renderer.domElement.addEventListener("pointermove", pointerMove); + renderer.domElement.addEventListener("pointerup", pointerUp); + renderer.domElement.addEventListener("pointercancel", pointerUp); + renderer.domElement.addEventListener("wheel", wheel, { passive: false }); + document.addEventListener("visibilitychange", visibility); + resize(); + render(); + return () => { + cancelAnimationFrame(frame); + cancelAnimationFrame(resizeFrame); + cancelAnimationFrame(pointerFrame); + observer.disconnect(); + document.removeEventListener("visibilitychange", visibility); + renderer.domElement.removeEventListener("pointerdown", pointerDown); + renderer.domElement.removeEventListener("pointermove", pointerMove); + renderer.domElement.removeEventListener("pointerup", pointerUp); + renderer.domElement.removeEventListener("pointercancel", pointerUp); + renderer.domElement.removeEventListener("wheel", wheel); + nodeGeometry.dispose(); + dotGeometry.dispose(); + sphere.geometry.dispose(); + (sphere.material as MeshBasicMaterial).dispose(); + globe.children.forEach((child) => { + if (child instanceof Line) { + child.geometry.dispose(); + child.material.dispose(); + } + if (child instanceof Points) child.material.dispose(); + if (child instanceof Mesh && child !== sphere) + (child.material as MeshBasicMaterial).dispose(); + }); + renderer.dispose(); + renderer.domElement.remove(); + cameraRef.current = null; + }; + }, [nodes, animate, performanceMode, visualQuality, selectNode]); + + const changeZoom = (delta: number) => + setZoom((value) => { + const next = Math.max(2.8, Math.min(7, value + delta)); + zoomRef.current = next; + if (cameraRef.current) cameraRef.current.position.z = next; + return next; + }); + const resetZoom = () => { + zoomRef.current = 4.2; + setZoom(4.2); + if (cameraRef.current) cameraRef.current.position.z = 4.2; + }; + const selected = nodes.find((node) => node.id === selectedNodeId); + return ( +
+
+ {failed ? ( +
+
+ +
+

+ 3D rendering is unavailable. Select a node from the list below. +

+
+ ) : ( +
+ )} +
+ HOLOGRAPHIC CLUSTER MODEL SIMULATION +
+
+ Globe controls + + {Math.round((4.2 / zoom) * 100)}% + + +
+
+ Select a node + {nodes.map((node) => ( + + ))} +
+
+ +
+ ); +} diff --git a/dashboard/src/components/HologramVisual.tsx b/dashboard/src/components/HologramVisual.tsx new file mode 100644 index 0000000..2a3e93a --- /dev/null +++ b/dashboard/src/components/HologramVisual.tsx @@ -0,0 +1,150 @@ +import { useReducedMotion } from "framer-motion"; +import { gsap } from "gsap"; +import { useEffect, useRef } from "react"; +import { useDashboardStore } from "../state/useDashboardStore"; + +const signals = [ + [48, 16], + [66, 24], + [79, 42], + [74, 65], + [56, 80], + [33, 73], + [20, 53], + [28, 31], + [50, 28], + [66, 51], + [47, 67], + [35, 46], +] as const; + +export function HologramVisual() { + const root = useRef(null); + const demoMotion = useDashboardStore((state) => state.demoMotion); + const performanceMode = useDashboardStore((state) => state.performanceMode); + const cinematicMotion = useDashboardStore((state) => state.cinematicMotion); + const reducedMotion = useReducedMotion(); + + useEffect(() => { + if (!demoMotion || performanceMode || reducedMotion || !root.current) + return; + let timeline: gsap.core.Timeline | undefined; + const context = gsap.context(() => { + timeline = gsap + .timeline({ repeat: -1, yoyo: true, defaults: { ease: "sine.inOut" } }) + .to( + ".holo-orbit-outer", + { + rotation: cinematicMotion ? 22 : 13, + duration: cinematicMotion ? 6 : 8, + }, + 0, + ) + .to( + ".holo-orbit-inner", + { + rotation: cinematicMotion ? -28 : -18, + duration: cinematicMotion ? 5 : 7, + }, + 0, + ) + .to( + ".holo-core-glow", + { + opacity: cinematicMotion ? 0.88 : 0.72, + scale: cinematicMotion ? 1.2 : 1.12, + duration: cinematicMotion ? 2.4 : 3, + }, + 0, + ) + .to( + ".holo-signal", + { + opacity: 0.35, + stagger: cinematicMotion ? 0.11 : 0.16, + duration: 1.3, + }, + 0, + ); + }, root); + const visibility = () => + document.hidden ? timeline?.pause() : timeline?.play(); + document.addEventListener("visibilitychange", visibility); + return () => { + document.removeEventListener("visibilitychange", visibility); + context.revert(); + }; + }, [cinematicMotion, demoMotion, performanceMode, reducedMotion]); + + return ( +
+
+ CLUSTER SIGNAL MAP DEMO +
+ +
+ 01 / COORDINATOR + 12 / SIGNAL PATHS +
+
+ ); +} diff --git a/dashboard/src/components/Topology.tsx b/dashboard/src/components/Topology.tsx new file mode 100644 index 0000000..6d88516 --- /dev/null +++ b/dashboard/src/components/Topology.tsx @@ -0,0 +1,336 @@ +/* ORIGINAL (preserved, not deleted — disabled by comment): +import { useMemo } from "react"; +import { Cpu, Server, X } from "lucide-react"; +import type { Node } from "../data/models"; +import { useDashboardStore } from "../state/useDashboardStore"; +import { Button, HealthBadge, Progress } from "./ui"; + +export function Topology({ nodes, layout = "topology" }: { nodes: Node[]; layout?: "topology" | "orbit" }) { + const selectedNodeId = useDashboardStore((state) => state.selectedNodeId); const selectNode = useDashboardStore((state) => state.selectNode); + const coordinator = nodes.find((node) => node.role === "coordinator") ?? nodes[0]; const workers = nodes.filter((node) => node.id !== coordinator?.id); + const positions = useMemo(() => workers.map((node, index) => ({ node, x: 25 + index * (50 / Math.max(workers.length - 1, 1)), y: index % 2 === 0 ? 73 : 82 })), [workers]); + const selected = nodes.find((node) => node.id === selectedNodeId); + return
{coordinator && <>{positions.map(({ node, x, y }) => )}} +
DEMO TOPOLOGY · {nodes.length} SAMPLE NODES
{selected ? : }
; +} +*/ + +import { + Cpu, + Maximize2, + Minimize2, + Minus, + Plus, + RotateCcw, + Server, + X, +} from "lucide-react"; +import type { + KeyboardEvent as ReactKeyboardEvent, + PointerEvent as ReactPointerEvent, +} from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import type { Node } from "../data/models"; +import { useDashboardStore } from "../state/useDashboardStore"; +import { Button, HealthBadge, Progress } from "./ui"; + +type PanPosition = { x: number; y: number }; + +export function Topology({ + nodes, + layout = "topology", +}: { + nodes: Node[]; + layout?: "topology" | "orbit"; +}) { + const selectedNodeId = useDashboardStore((state) => state.selectedNodeId); + const selectNode = useDashboardStore((state) => state.selectNode); + const coordinator = + nodes.find((node) => node.role === "coordinator") ?? nodes[0]; + const workers = nodes.filter((node) => node.id !== coordinator?.id); + const positions = useMemo( + () => + workers.map((node, index) => { + const angle = + (index / Math.max(workers.length, 1)) * Math.PI * 2 - Math.PI / 2; + return layout === "orbit" + ? { node, x: 50 + Math.cos(angle) * 32, y: 50 + Math.sin(angle) * 32 } + : { + node, + x: 25 + index * (50 / Math.max(workers.length - 1, 1)), + y: index % 2 === 0 ? 73 : 82, + }; + }), + [workers, layout], + ); + const selected = nodes.find((node) => node.id === selectedNodeId); + const board = useRef(null); + const drag = useRef<{ + pointerId: number; + origin: PanPosition; + start: PanPosition; + } | null>(null); + const [zoom, setZoom] = useState(1); + const [pan, setPan] = useState({ x: 0, y: 0 }); + const [fullscreen, setFullscreen] = useState(false); + const [fullscreenError, setFullscreenError] = useState(""); + + useEffect(() => { + const update = () => + setFullscreen(document.fullscreenElement === board.current); + document.addEventListener("fullscreenchange", update); + return () => document.removeEventListener("fullscreenchange", update); + }, []); + + const changeZoom = (delta: number) => + setZoom((value) => + Math.max(0.65, Math.min(1.6, Number((value + delta).toFixed(2)))), + ); + const startPan = (event: ReactPointerEvent) => { + if (event.target instanceof HTMLElement && event.target.closest("button")) + return; + if (event.button !== 0) return; + drag.current = { + pointerId: event.pointerId, + origin: pan, + start: { x: event.clientX, y: event.clientY }, + }; + event.currentTarget.setPointerCapture(event.pointerId); + }; + const movePan = (event: ReactPointerEvent) => { + if (!drag.current || drag.current.pointerId !== event.pointerId) return; + setPan({ + x: drag.current.origin.x + event.clientX - drag.current.start.x, + y: drag.current.origin.y + event.clientY - drag.current.start.y, + }); + }; + const stopPan = (event: ReactPointerEvent) => { + if (drag.current?.pointerId === event.pointerId) drag.current = null; + }; + const toggleFullscreen = async () => { + if (!board.current) return; + try { + if (document.fullscreenElement === board.current) + await document.exitFullscreen(); + else await board.current.requestFullscreen(); + setFullscreenError(""); + } catch { + setFullscreenError("Fullscreen is unavailable in this browser context."); + } + }; + const handleBoardKeyDown = ( + event: ReactKeyboardEvent, + ) => { + if (event.key === "ArrowLeft") + setPan((value) => ({ ...value, x: value.x + 24 })); + if (event.key === "ArrowRight") + setPan((value) => ({ ...value, x: value.x - 24 })); + if (event.key === "ArrowUp") + setPan((value) => ({ ...value, y: value.y + 24 })); + if (event.key === "ArrowDown") + setPan((value) => ({ ...value, y: value.y - 24 })); + if (event.key === "+" || event.key === "=") changeZoom(0.1); + if (event.key === "-") changeZoom(-0.1); + if (event.key === "0") { + setZoom(1); + setPan({ x: 0, y: 0 }); + } + if ( + [ + "ArrowLeft", + "ArrowRight", + "ArrowUp", + "ArrowDown", + "+", + "=", + "-", + "0", + ].includes(event.key) + ) + event.preventDefault(); + }; + + return ( +
+
+ Interactive demo cluster topology +
+ {layout === "orbit" && ( + + )} +
+ {coordinator && ( + <> + + + {positions.map(({ node, x, y }) => ( + + ))} + + )} +
+
+ Topology view controls + {Math.round(zoom * 100)}% + + + + +
+
+ DEMO TOPOLOGY ·{" "} + {nodes.length} SAMPLE NODES · DRAG TO PAN +
+
+ {fullscreenError && ( +

+ {fullscreenError} +

+ )} + + Drag to pan, use arrow keys to pan, plus and minus to zoom, and zero to + reset. + + {selected ? ( + + ) : ( + + )} +
+ ); +} diff --git a/dashboard/src/components/ui.tsx b/dashboard/src/components/ui.tsx new file mode 100644 index 0000000..d0b38c9 --- /dev/null +++ b/dashboard/src/components/ui.tsx @@ -0,0 +1,166 @@ +import { + AlertTriangle, + Check, + CircleHelp, + CircleOff, + LoaderCircle, +} from "lucide-react"; +import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react"; +import type { Health } from "../data/models"; +import { Counter } from "./Counter"; + +export function Button({ + className = "", + variant = "secondary", + ...props +}: ButtonHTMLAttributes & { + variant?: "primary" | "secondary" | "ghost" | "danger"; +}) { + return ( + +
+ ); +} diff --git a/dashboard/src/data/agentLayout.ts b/dashboard/src/data/agentLayout.ts new file mode 100644 index 0000000..a3bc39b --- /dev/null +++ b/dashboard/src/data/agentLayout.ts @@ -0,0 +1,93 @@ +import { localProvider } from "./localProvider"; + +export type AgentGraphMode = "network" | "radial"; +export type AgentGraphPoint = { x: number; y: number }; +export type AgentGraphCamera = { x: number; y: number; zoom: number }; +export type AgentGraphLayout = { + positions: Record>; + cameras: Record; +}; + +function restoreLayout( + value: unknown, + fallback: AgentGraphLayout, +): AgentGraphLayout { + if (typeof value !== "object" || value === null) return fallback; + const data = value as Partial; + const positions = { ...fallback.positions }; + const cameras = { ...fallback.cameras }; + for (const mode of ["network", "radial"] as const) { + const candidate = data.positions?.[mode]; + if (candidate && typeof candidate === "object") { + positions[mode] = { + ...fallback.positions[mode], + ...Object.fromEntries( + Object.entries(candidate).filter( + (entry): entry is [string, AgentGraphPoint] => + typeof entry[1] === "object" && + entry[1] !== null && + Number.isFinite((entry[1] as AgentGraphPoint).x) && + Number.isFinite((entry[1] as AgentGraphPoint).y), + ), + ), + }; + } + const camera = data.cameras?.[mode]; + if ( + camera && + Number.isFinite(camera.x) && + Number.isFinite(camera.y) && + Number.isFinite(camera.zoom) + ) { + cameras[mode] = { + x: camera.x, + y: camera.y, + zoom: Math.max(0.65, Math.min(1.8, camera.zoom)), + }; + } + } + return { positions, cameras }; +} + +export async function readAgentGraphLayout( + fallback: AgentGraphLayout, +): Promise { + try { + return restoreLayout( + await localProvider.get("agent-layouts", "default"), + fallback, + ); + } catch { + return fallback; + } +} + +export async function writeAgentGraphLayout( + layout: AgentGraphLayout, +): Promise { + try { + await localProvider.put("agent-layouts", "default", layout); + } catch { + // Layout persistence is best effort; graph editing remains available if IndexedDB is unavailable. + } +} + +/* ORIGINAL (preserved, not deleted — disabled by comment): +const STORAGE_KEY = "opencluster.agents.layout.v1"; +export function readAgentGraphLayoutFromLocalStorage(fallback: AgentGraphLayout): AgentGraphLayout { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return fallback; + return restoreLayout(JSON.parse(raw), fallback); + } catch { + return fallback; + } +} +export function writeAgentGraphLayoutToLocalStorage(layout: AgentGraphLayout): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(layout)); + } catch { + // Layout persistence is best effort; graph editing remains available if storage is blocked or full. + } +} +*/ diff --git a/dashboard/src/data/agents.ts b/dashboard/src/data/agents.ts new file mode 100644 index 0000000..f064b91 --- /dev/null +++ b/dashboard/src/data/agents.ts @@ -0,0 +1,199 @@ +export type AgentStatus = "active" | "idle" | "attention" | "paused"; +export type AgentKind = + | "agent" + | "model" + | "tool" + | "memory" + | "task" + | "service"; +export type AgentEntity = { + id: string; + name: string; + vendor: string; + model: string; + role: string; + status: AgentStatus; + tasks: number; + latency: string; + color: string; + icon: "codex" | "claude" | "hermes" | "openclaw" | "custom"; + kind: AgentKind; +}; +export type AgentRelation = { + source: string; + target: string; + label: string; + strength: number; +}; + +export const agentSeedCatalog: AgentEntity[] = [ + { + id: "codex", + name: "Codex", + vendor: "OpenAI", + model: "Codex · demo adapter", + role: "Workflow coordinator", + status: "active", + tasks: 4, + latency: "42 ms", + color: "mint", + icon: "codex", + kind: "agent", + }, + { + id: "claude", + name: "Claude Code", + vendor: "Anthropic", + model: "Claude · demo adapter", + role: "Code analysis", + status: "active", + tasks: 2, + latency: "58 ms", + color: "peach", + icon: "claude", + kind: "agent", + }, + { + id: "hermes", + name: "Hermes Agent", + vendor: "Open source", + model: "Hermes · demo adapter", + role: "Task automation", + status: "idle", + tasks: 0, + latency: "—", + color: "violet", + icon: "hermes", + kind: "agent", + }, + { + id: "openclaw", + name: "OpenClaw", + vendor: "Community", + model: "OpenClaw · demo adapter", + role: "Tool routing", + status: "attention", + tasks: 1, + latency: "91 ms", + color: "blue", + icon: "openclaw", + kind: "agent", + }, + { + id: "custom", + name: "Custom adapter", + vendor: "Local example", + model: "Compatible endpoint · demo", + role: "Custom integration", + status: "paused", + tasks: 0, + latency: "—", + color: "gold", + icon: "custom", + kind: "agent", + }, + { + id: "model", + name: "Reasoning model", + vendor: "MODEL", + model: "Local demo model", + role: "Language model", + status: "active", + tasks: 3, + latency: "36 ms", + color: "violet", + icon: "custom", + kind: "model", + }, + { + id: "tools", + name: "Tool registry", + vendor: "TOOLS", + model: "Local demo tools", + role: "Tool collection", + status: "idle", + tasks: 0, + latency: "—", + color: "blue", + icon: "openclaw", + kind: "tool", + }, + { + id: "memory", + name: "Shared context", + vendor: "MEMORY", + model: "Session memory · demo", + role: "Context store", + status: "active", + tasks: 2, + latency: "12 ms", + color: "gold", + icon: "hermes", + kind: "memory", + }, + { + id: "task", + name: "Build review", + vendor: "TASK", + model: "Sample job · demo", + role: "Queued task", + status: "idle", + tasks: 1, + latency: "—", + color: "peach", + icon: "claude", + kind: "task", + }, + { + id: "service", + name: "Git provider", + vendor: "SERVICE", + model: "External service · illustrative", + role: "Integration endpoint", + status: "paused", + tasks: 0, + latency: "—", + color: "blue", + icon: "custom", + kind: "service", + }, +]; + +export const agentRelationSeeds: AgentRelation[] = [ + { + source: "codex", + target: "claude", + label: "delegates review", + strength: 0.82, + }, + { source: "codex", target: "hermes", label: "routes tasks", strength: 0.62 }, + { + source: "codex", + target: "openclaw", + label: "shares context", + strength: 0.46, + }, + { source: "claude", target: "custom", label: "handoff", strength: 0.34 }, + { source: "hermes", target: "custom", label: "tool access", strength: 0.3 }, + { + source: "openclaw", + target: "hermes", + label: "event bridge", + strength: 0.56, + }, + { source: "codex", target: "model", label: "uses model", strength: 0.66 }, + { source: "claude", target: "tools", label: "uses tools", strength: 0.42 }, + { + source: "hermes", + target: "memory", + label: "reads context", + strength: 0.52, + }, + { source: "codex", target: "task", label: "runs task", strength: 0.72 }, + { + source: "task", + target: "service", + label: "requests change", + strength: 0.38, + }, +]; diff --git a/dashboard/src/data/demoApi.ts b/dashboard/src/data/demoApi.ts new file mode 100644 index 0000000..a152718 --- /dev/null +++ b/dashboard/src/data/demoApi.ts @@ -0,0 +1,87 @@ +import { + initialActivity, + initialClusters, + initialNodes, + initialTasks, +} from "./mock"; +import type { ActivityEvent, Cluster, DemoTask, Node } from "./models"; + +export interface DashboardData { + getClusters(): Promise; + getNodes(clusterId?: string): Promise; + getTasks(): Promise; + getActivity(): Promise; + createCluster(name: string, region: string, description: string): Cluster; + updateCluster( + cluster: Cluster, + changes: Partial>, + ): Cluster; + moveNode(node: Node, clusterId: string): Node; + designateCoordinator(nodes: Node[], nodeId: string): Node[]; + createTask(nodeId: string, id: string): DemoTask; + assignTask(task: DemoTask, nodeId: string): DemoTask; +} +const pause = () => + new Promise((resolve) => window.setTimeout(resolve, 120)); +export const demoApi: DashboardData = { + async getClusters() { + await pause(); + return structuredClone(initialClusters); + }, + async getNodes(clusterId) { + await pause(); + return structuredClone( + clusterId + ? initialNodes.filter((node) => node.clusterId === clusterId) + : initialNodes, + ); + }, + async getTasks() { + await pause(); + return structuredClone(initialTasks); + }, + async getActivity() { + await pause(); + return structuredClone(initialActivity); + }, + createCluster(name, region, description) { + return { + id: `demo-${Date.now()}`, + name: name.trim(), + region, + description, + nodeIds: [], + createdAt: new Date().toISOString().slice(0, 10), + }; + }, + updateCluster(cluster, changes) { + return { ...cluster, ...changes }; + }, + moveNode(node, clusterId) { + return { ...node, clusterId }; + }, + designateCoordinator(nodes, nodeId) { + const target = nodes.find((node) => node.id === nodeId); + if (!target) return nodes; + return nodes.map((node) => + node.clusterId === target.clusterId + ? { ...node, role: node.id === nodeId ? "coordinator" : "worker" } + : node, + ); + }, + createTask(nodeId, id) { + return { + id, + name: "Manual demo task", + nodeId, + status: "queued", + progress: 0, + started: "Waiting", + duration: "—", + type: "Manual", + }; + }, + assignTask(task, nodeId) { + return { ...task, nodeId }; + }, +}; diff --git a/dashboard/src/data/localProvider.ts b/dashboard/src/data/localProvider.ts new file mode 100644 index 0000000..db28d75 --- /dev/null +++ b/dashboard/src/data/localProvider.ts @@ -0,0 +1,481 @@ +import { type DBSchema, type IDBPDatabase, openDB } from "idb"; +import { z } from "zod"; +import { + type AgentEntity, + type AgentRelation, + agentRelationSeeds, + agentSeedCatalog, +} from "./agents"; +import { + initialActivity, + initialClusters, + initialNodes, + initialTasks, + softwareCatalog, + telemetry, +} from "./mock"; +import type { + ActivityEvent, + Cluster, + DemoTask, + Node, + SoftwareConfig, + TelemetryPoint, +} from "./models"; +import type { + DashboardProvider, + QueryOptions, + Repository, +} from "./providerContracts"; +import type { Workflow } from "./workflows"; + +const clusterSchema = z.object({ + id: z.string().min(1), + name: z.string().trim().min(1).max(80), + region: z.string().max(100), + description: z.string().max(500), + nodeIds: z.array(z.string()), + createdAt: z.string(), +}); +const nodeSchema = z.object({ + id: z.string().min(1), + name: z.string().min(1), + role: z.enum(["coordinator", "worker"]), + clusterId: z.string().min(1), + host: z.string(), + os: z.string(), + health: z.enum(["healthy", "warning", "offline", "unknown"]), + cpu: z.number().finite().min(0).max(100), + memory: z.number().finite().min(0).max(100), + disk: z.number().finite().min(0).max(100), + network: z.number().finite().min(0), + gpu: z.string().optional(), + uptime: z.string(), + lastSeen: z.string(), + labels: z.array(z.string()), +}); +const workflowSchema = z.object({ + version: z.literal(1), + name: z.string().trim().min(1).max(120), + nodes: z + .array( + z.object({ + id: z.string().min(1).max(120), + kind: z.enum([ + "trigger", + "schedule", + "agent", + "condition", + "delay", + "transform", + "notify", + "success", + "failure", + ]), + title: z.string().min(1).max(120), + detail: z.string().max(500), + x: z.number().finite(), + y: z.number().finite(), + config: z.record( + z.string(), + z.union([z.string(), z.number().finite(), z.boolean()]), + ), + }), + ) + .max(200), + edges: z + .array( + z.object({ + id: z.string().min(1).max(240), + source: z.string().min(1).max(120), + target: z.string().min(1).max(120), + label: z.string().max(120).optional(), + }), + ) + .max(500), +}); +const recordValueSchemas: Record = { + agents: z.object({ + id: z.string(), + name: z.string(), + vendor: z.string(), + model: z.string(), + role: z.string(), + status: z.enum(["active", "idle", "attention", "paused"]), + tasks: z.number().int().nonnegative(), + latency: z.string(), + color: z.string(), + icon: z.enum(["codex", "claude", "hermes", "openclaw", "custom"]), + kind: z.enum(["agent", "model", "tool", "memory", "task", "service"]), + }), + "agent-relations": z.object({ + source: z.string().min(1), + target: z.string().min(1), + label: z.string().max(120), + strength: z.number().finite().min(0).max(1), + }), + workflows: workflowSchema, + "agent-layouts": z.object({ + positions: z.object({ + network: z.record( + z.string(), + z.object({ x: z.number().finite(), y: z.number().finite() }), + ), + radial: z.record( + z.string(), + z.object({ x: z.number().finite(), y: z.number().finite() }), + ), + }), + cameras: z.object({ + network: z.object({ + x: z.number().finite(), + y: z.number().finite(), + zoom: z.number().finite().min(0.1).max(10), + }), + radial: z.object({ + x: z.number().finite(), + y: z.number().finite(), + zoom: z.number().finite().min(0.1).max(10), + }), + }), + }), + "software-configs": z.object({ + id: z.string().min(1), + softwareId: z.string().min(1), + nodeId: z.string().min(1), + profile: z.string().min(1).max(120), + }), + meta: z.object({ version: z.literal(1) }), + clusters: clusterSchema, + nodes: nodeSchema, + telemetry: z.object({ + time: z.string(), + cpu: z.number().finite(), + memory: z.number().finite(), + networkIn: z.number().finite(), + networkOut: z.number().finite(), + }), + tasks: z.object({ + id: z.string(), + name: z.string(), + nodeId: z.string(), + status: z.enum(["running", "queued", "completed", "failed"]), + progress: z.number().finite().min(0).max(100), + started: z.string(), + duration: z.string(), + type: z.string(), + }), + activity: z.object({ + id: z.string(), + title: z.string(), + detail: z.string(), + time: z.string(), + severity: z.enum(["info", "success", "warning", "error"]), + source: z.string(), + }), + "software-catalog": z.object({ + id: z.string(), + name: z.string(), + vendor: z.string(), + description: z.string(), + category: z.string(), + color: z.string(), + status: z.enum(["available", "planned"]), + }), +}; +const recordSchema = z.object({ + key: z.string(), + bucket: z.string(), + id: z.string(), + value: z.unknown(), +}); +type Bucket = + | "clusters" + | "nodes" + | "workflows" + | "agent-layouts" + | "agents" + | "agent-relations" + | "software-catalog" + | "software-configs" + | "telemetry" + | "tasks" + | "activity" + | "meta"; +type RecordRow = z.infer; +interface OpenClusterDB extends DBSchema { + records: { key: string; value: RecordRow; indexes: { bucket: string } }; +} + +let database: Promise> | undefined; +const db = () => + (database ??= openDB("opencluster-dashboard", 1, { + upgrade(database) { + const store = database.createObjectStore("records", { keyPath: "key" }); + store.createIndex("bucket", "bucket"); + }, + })); +const keyFor = (bucket: Bucket, id: string) => `${bucket}:${id}`; +const recordFor = (bucket: Bucket, id: string, value: T) => { + recordValueSchemas[bucket].parse(value); + return recordSchema.parse({ key: keyFor(bucket, id), bucket, id, value }); +}; +const valueFor = (bucket: Bucket, value: unknown): T => + recordValueSchemas[bucket].parse(value) as T; + +export const localProvider = { + async list(bucket: Bucket): Promise { + const rows = await (await db()).getAllFromIndex( + "records", + "bucket", + bucket, + ); + return rows.map((row) => + valueFor(bucket, recordSchema.parse(row).value), + ); + }, + async get(bucket: Bucket, id: string): Promise { + const row = await (await db()).get("records", keyFor(bucket, id)); + return row ? valueFor(bucket, recordSchema.parse(row).value) : undefined; + }, + async put(bucket: Bucket, id: string, value: T): Promise { + const row = recordFor(bucket, id, value); + await (await db()).put("records", row); + }, + async putMany( + bucket: Bucket, + records: Array<{ id: string; value: T }>, + ): Promise { + const validated = records.map((record) => + recordFor(bucket, record.id, record.value), + ); + const database = await db(); + const tx = database.transaction("records", "readwrite"); + for (const record of validated) { + // ORIGINAL (preserved, not deleted — disabled by comment): + // tx.store.put(recordFor(bucket, record.id, record.value)); + tx.store.put(record); + } + await tx.done; + }, + async replace( + bucket: Bucket, + records: Array<{ id: string; value: T }>, + ): Promise { + await this.replaceBuckets({ [bucket]: records }); + }, + async replaceBuckets( + buckets: Partial>>, + ): Promise { + // Validate every bucket before starting a transaction that clears saved records. + for (const [bucket, records] of Object.entries(buckets)) { + for (const record of records ?? []) + recordFor(bucket as Bucket, record.id, record.value); + } + const database = await db(); + const tx = database.transaction("records", "readwrite"); + for (const [bucket, records] of Object.entries(buckets) as Array< + [Bucket, Array<{ id: string; value: unknown }> | undefined] + >) { + if (!records) continue; + const existing = await tx.store.index("bucket").getAllKeys(bucket); + await Promise.all(existing.map((key) => tx.store.delete(key))); + for (const record of records) + tx.store.put(recordFor(bucket, record.id, record.value)); + } + await tx.done; + }, + async writeClusterNodeState( + clusters: Cluster[], + nodes: Node[], + ): Promise { + await this.replaceBuckets({ + clusters: clusters.map((value) => ({ id: value.id, value })), + nodes: nodes.map((value) => ({ id: value.id, value })), + }); + }, + async delete(bucket: Bucket, id: string): Promise { + await (await db()).delete("records", keyFor(bucket, id)); + }, + async initialize( + legacy?: unknown, + ): Promise<{ clusters: Cluster[]; nodes: Node[] }> { + const database = await db(); + const marker = await database.get("records", keyFor("meta", "initialized")); + const firstRun = + !marker || !recordValueSchemas.meta.safeParse(marker.value).success; + const existingClusters = ( + await database.getAllFromIndex("records", "bucket", "clusters") + ) + .map((row) => clusterSchema.safeParse(row.value)) + .filter((r) => r.success) + .map((r) => r.data); + const existingNodes = ( + await database.getAllFromIndex("records", "bucket", "nodes") + ) + .map((row) => nodeSchema.safeParse(row.value)) + .filter((r) => r.success) + .map((r) => r.data); + const old = z + .object({ + clusters: z.array(clusterSchema).optional(), + nodes: z.array(nodeSchema).optional(), + }) + .safeParse(legacy); + const clusters = existingClusters.length + ? existingClusters + : firstRun && old.success && old.data.clusters?.length + ? old.data.clusters + : firstRun + ? structuredClone(initialClusters) + : []; + const nodes = existingNodes.length + ? existingNodes + : firstRun && old.success && old.data.nodes?.length + ? old.data.nodes + : firstRun + ? structuredClone(initialNodes) + : []; + const validClusterIds = new Set(clusters.map((cluster) => cluster.id)); + const safeNodes = nodes.filter((node) => + validClusterIds.has(node.clusterId), + ); + const consistentClusters = clusters.map((cluster) => ({ + ...cluster, + nodeIds: safeNodes + .filter((node) => node.clusterId === cluster.id) + .map((node) => node.id), + })); + if (!existingClusters.length && consistentClusters.length) + await this.putMany( + "clusters", + consistentClusters.map((value) => ({ id: value.id, value })), + ); + if (!existingNodes.length && safeNodes.length) + await this.putMany( + "nodes", + safeNodes.map((value) => ({ id: value.id, value })), + ); + if ( + firstRun && + !(await database.countFromIndex("records", "bucket", "telemetry")) + ) + await this.putMany( + "telemetry", + telemetry.map((value, index) => ({ id: String(index), value })), + ); + if ( + firstRun && + !(await database.countFromIndex("records", "bucket", "tasks")) + ) + await this.putMany( + "tasks", + initialTasks.map((value: DemoTask) => ({ id: value.id, value })), + ); + if ( + firstRun && + !(await database.countFromIndex("records", "bucket", "activity")) + ) + await this.putMany( + "activity", + initialActivity.map((value: ActivityEvent) => ({ + id: value.id, + value, + })), + ); + if ( + firstRun && + !(await database.countFromIndex("records", "bucket", "software-catalog")) + ) + await this.putMany( + "software-catalog", + softwareCatalog.map((value) => ({ id: value.id, value })), + ); + if ( + firstRun && + !(await database.countFromIndex("records", "bucket", "agents")) + ) + await this.putMany( + "agents", + agentSeedCatalog.map((value: AgentEntity) => ({ id: value.id, value })), + ); + if ( + firstRun && + !(await database.countFromIndex("records", "bucket", "agent-relations")) + ) + await this.putMany( + "agent-relations", + agentRelationSeeds.map((value) => ({ + id: `${value.source}:${value.target}`, + value, + })), + ); + if (firstRun) await this.put("meta", "initialized", { version: 1 }); + return { clusters: consistentClusters, nodes: safeNodes }; + }, + async resetRecords(): Promise { + await this.replaceBuckets({ + clusters: initialClusters.map((value) => ({ id: value.id, value })), + nodes: initialNodes.map((value) => ({ id: value.id, value })), + telemetry: telemetry.map((value, index) => ({ + id: String(index), + value, + })), + tasks: initialTasks.map((value) => ({ id: value.id, value })), + activity: initialActivity.map((value) => ({ id: value.id, value })), + "software-catalog": softwareCatalog.map((value) => ({ + id: value.id, + value, + })), + workflows: [], + "agent-layouts": [], + agents: agentSeedCatalog.map((value) => ({ id: value.id, value })), + meta: [{ id: "initialized", value: { version: 1 } }], + "agent-relations": agentRelationSeeds.map((value) => ({ + id: `${value.source}:${value.target}`, + value, + })), + "software-configs": [], + }); + }, +}; + +export type { Bucket, DashboardProvider, TelemetryPoint }; + +function repository(bucket: Bucket): Repository { + return { + async list(options: QueryOptions = {}) { + let values = await localProvider.list(bucket); + if (options.search) { + const query = options.search.trim().toLocaleLowerCase(); + values = values.filter((value) => + JSON.stringify(value).toLocaleLowerCase().includes(query), + ); + } + if (options.filter) values = values.filter(options.filter); + if (options.sort) values.sort(options.sort); + return values; + }, + get: (id) => localProvider.get(bucket, id), + async create(id, value) { + if ((await localProvider.get(bucket, id)) !== undefined) + throw new Error(`Record ${id} already exists.`); + await localProvider.put(bucket, id, value); + }, + update: (id, value) => localProvider.put(bucket, id, value), + delete: (id) => localProvider.delete(bucket, id), + }; +} + +export const dashboardLocalProvider: DashboardProvider = { + clusters: repository("clusters"), + nodes: repository("nodes"), + agents: repository("agents"), + agentRelations: repository("agent-relations"), + workflows: repository("workflows"), + softwareCatalog: repository("software-catalog"), + softwareConfigurations: repository("software-configs"), + tasks: repository("tasks"), + activity: repository("activity"), + telemetry: repository("telemetry"), +}; diff --git a/dashboard/src/data/mock.ts b/dashboard/src/data/mock.ts new file mode 100644 index 0000000..7e69a20 --- /dev/null +++ b/dashboard/src/data/mock.ts @@ -0,0 +1,274 @@ +import type { + ActivityEvent, + Cluster, + DemoTask, + Node, + Software, + TelemetryPoint, +} from "./models"; + +export const initialClusters: Cluster[] = [ + { + id: "cl-main", + name: "Homelab Core", + region: "Local network", + description: "Primary demo cluster for local workloads", + nodeIds: ["node-01", "node-02", "node-03", "node-04"], + createdAt: "2026-10-18", + }, + { + id: "cl-edge", + name: "Edge Lab", + region: "Remote site · demo", + description: "A second sample environment", + nodeIds: ["node-05", "node-06"], + createdAt: "2026-10-20", + }, +]; +export const initialNodes: Node[] = [ + { + id: "node-01", + name: "atlas-control", + role: "coordinator", + clusterId: "cl-main", + host: "10.0.0.10", + os: "Arch Linux · x86_64", + health: "healthy", + cpu: 32, + memory: 58, + disk: 41, + network: 26, + gpu: "NVIDIA RTX 4080 · 16 GB", + uptime: "18d 04h", + lastSeen: "Just now", + labels: ["control", "gpu"], + }, + { + id: "node-02", + name: "atlas-worker-01", + role: "worker", + clusterId: "cl-main", + host: "10.0.0.21", + os: "Debian 13 · x86_64", + health: "healthy", + cpu: 67, + memory: 74, + disk: 62, + network: 48, + uptime: "12d 11h", + lastSeen: "Just now", + labels: ["compute"], + }, + { + id: "node-03", + name: "atlas-worker-02", + role: "worker", + clusterId: "cl-main", + host: "10.0.0.22", + os: "Arch Linux · x86_64", + health: "warning", + cpu: 84, + memory: 81, + disk: 55, + network: 71, + uptime: "8d 02h", + lastSeen: "1 min ago", + labels: ["compute", "memory-heavy"], + }, + { + id: "node-04", + name: "atlas-storage", + role: "worker", + clusterId: "cl-main", + host: "10.0.0.30", + os: "Debian 12 · x86_64", + health: "healthy", + cpu: 18, + memory: 44, + disk: 78, + network: 32, + uptime: "41d 16h", + lastSeen: "Just now", + labels: ["storage"], + }, + { + id: "node-05", + name: "edge-gateway", + role: "coordinator", + clusterId: "cl-edge", + host: "192.168.4.2", + os: "Debian 13 · aarch64", + health: "healthy", + cpu: 23, + memory: 39, + disk: 27, + network: 19, + uptime: "6d 09h", + lastSeen: "Just now", + labels: ["edge"], + }, + { + id: "node-06", + name: "edge-worker-01", + role: "worker", + clusterId: "cl-edge", + host: "192.168.4.8", + os: "Ubuntu 24.04 · aarch64", + health: "offline", + cpu: 0, + memory: 0, + disk: 33, + network: 0, + uptime: "—", + lastSeen: "22 min ago", + labels: ["edge", "compute"], + }, +]; +export const telemetry: TelemetryPoint[] = Array.from( + { length: 24 }, + (_, i) => { + const hour = String((i + 1) % 24).padStart(2, "0"); + return { + time: `${hour}:00`, + cpu: Math.round(35 + Math.sin(i * 0.7) * 15 + (i % 5) * 3), + memory: Math.round(52 + Math.sin(i * 0.38) * 8 + (i % 4) * 2), + networkIn: Math.round(36 + Math.sin(i * 0.9) * 24 + (i % 3) * 8), + networkOut: Math.round(28 + Math.cos(i * 0.65) * 18 + (i % 4) * 5), + }; + }, +); +export const softwareCatalog: Software[] = [ + { + id: "codex", + name: "Codex", + vendor: "OpenAI", + description: + "Coding agent for repository tasks and software engineering workflows.", + category: "Coding agent", + color: "#8fdfb0", + status: "available", + }, + { + id: "claude", + name: "Claude Code", + vendor: "Anthropic", + description: + "Agentic coding assistant designed to work in development environments.", + category: "Coding agent", + color: "#e7a76e", + status: "available", + }, + { + id: "hermes", + name: "Hermes Agent", + vendor: "Nous Research", + description: + "Extensible assistant runtime for configurable agent workflows.", + category: "AI agent", + color: "#b7a4ff", + status: "available", + }, + { + id: "openclaw", + name: "OpenClaw", + vendor: "Community", + description: "Open agent platform integration concept for future adapters.", + category: "Integration", + color: "#77b9ff", + status: "planned", + }, +]; +export const initialTasks: DemoTask[] = [ + { + id: "job-1042", + name: "Build documentation preview", + nodeId: "node-02", + status: "running", + progress: 68, + started: "2 min ago", + duration: "00:02:14", + type: "Build", + }, + { + id: "job-1041", + name: "Index model artifacts", + nodeId: "node-03", + status: "queued", + progress: 0, + started: "Waiting", + duration: "—", + type: "AI workload", + }, + { + id: "job-1040", + name: "Nightly storage check", + nodeId: "node-04", + status: "completed", + progress: 100, + started: "18 min ago", + duration: "00:00:42", + type: "Maintenance", + }, + { + id: "job-1039", + name: "Container image scan", + nodeId: "node-02", + status: "completed", + progress: 100, + started: "1 hr ago", + duration: "00:01:09", + type: "Security scan", + }, + { + id: "job-1038", + name: "GPU benchmark", + nodeId: "node-03", + status: "failed", + progress: 46, + started: "3 hrs ago", + duration: "00:00:31", + type: "Benchmark", + }, +]; +export const initialActivity: ActivityEvent[] = [ + { + id: "evt-1", + title: "Node atlas-worker-02 reached high CPU usage", + detail: "CPU crossed the 80% demo threshold · Homelab Core", + time: "2 min ago", + severity: "warning", + source: "Monitoring", + }, + { + id: "evt-2", + title: "Build documentation preview started", + detail: "Assigned to atlas-worker-01 · job-1042", + time: "4 min ago", + severity: "info", + source: "Tasks", + }, + { + id: "evt-3", + title: "Nightly storage check completed", + detail: "atlas-storage · 00:00:42", + time: "18 min ago", + severity: "success", + source: "Tasks", + }, + { + id: "evt-4", + title: "edge-worker-01 went offline", + detail: "Last sample received 22 minutes ago · Edge Lab", + time: "22 min ago", + severity: "error", + source: "Nodes", + }, + { + id: "evt-5", + title: "Demo cluster initialized", + detail: "Homelab Core · 4 sample nodes", + time: "Yesterday", + severity: "success", + source: "Clusters", + }, +]; diff --git a/dashboard/src/data/models.ts b/dashboard/src/data/models.ts new file mode 100644 index 0000000..b3eab21 --- /dev/null +++ b/dashboard/src/data/models.ts @@ -0,0 +1,66 @@ +export type Health = "healthy" | "warning" | "offline" | "unknown"; +export type Cluster = { + id: string; + name: string; + region: string; + description: string; + nodeIds: string[]; + createdAt: string; +}; +export type Node = { + id: string; + name: string; + role: "coordinator" | "worker"; + clusterId: string; + host: string; + os: string; + health: Health; + cpu: number; + memory: number; + disk: number; + network: number; + gpu?: string; + uptime: string; + lastSeen: string; + labels: string[]; +}; +export type TelemetryPoint = { + time: string; + cpu: number; + memory: number; + networkIn: number; + networkOut: number; +}; +export type Software = { + id: string; + name: string; + vendor: string; + description: string; + category: string; + color: string; + status: "available" | "planned"; +}; +export type SoftwareConfig = { + id: string; + softwareId: string; + nodeId: string; + profile: string; +}; +export type DemoTask = { + id: string; + name: string; + nodeId: string; + status: "running" | "queued" | "completed" | "failed"; + progress: number; + started: string; + duration: string; + type: string; +}; +export type ActivityEvent = { + id: string; + title: string; + detail: string; + time: string; + severity: "info" | "success" | "warning" | "error"; + source: string; +}; diff --git a/dashboard/src/data/provider.ts b/dashboard/src/data/provider.ts new file mode 100644 index 0000000..c39653c --- /dev/null +++ b/dashboard/src/data/provider.ts @@ -0,0 +1,4 @@ +import { dashboardLocalProvider } from "./localProvider"; +import type { DashboardProvider } from "./providerContracts"; + +export const dashboardProvider: DashboardProvider = dashboardLocalProvider; diff --git a/dashboard/src/data/providerContracts.ts b/dashboard/src/data/providerContracts.ts new file mode 100644 index 0000000..4f6a938 --- /dev/null +++ b/dashboard/src/data/providerContracts.ts @@ -0,0 +1,49 @@ +import type { AgentEntity, AgentRelation } from "./agents"; +import type { + ActivityEvent, + Cluster, + DemoTask, + Node, + Software, + SoftwareConfig, + TelemetryPoint, +} from "./models"; +import type { Workflow } from "./workflows"; + +export type QueryOptions = { + search?: string; + sort?: (left: T, right: T) => number; + filter?: (value: T) => boolean; +}; + +export interface Repository { + list(options?: QueryOptions): Promise; + get(id: string): Promise; + create(id: string, value: T): Promise; + update(id: string, value: T): Promise; + delete(id: string): Promise; +} + +export interface DashboardProvider { + clusters: Repository; + nodes: Repository; + agents: Repository; + agentRelations: Repository; + workflows: Repository; + softwareCatalog: Repository; + softwareConfigurations: Repository; + tasks: Repository; + activity: Repository; + telemetry: Repository; +} + +export interface RemoteDashboardTransport { + request(input: { + method: "GET" | "POST" | "PATCH" | "DELETE"; + path: string; + body?: unknown; + }): Promise; + close(): void; +} + +export type DataProviderMode = "local" | "cloud"; diff --git a/dashboard/src/data/queryClient.ts b/dashboard/src/data/queryClient.ts new file mode 100644 index 0000000..847a491 --- /dev/null +++ b/dashboard/src/data/queryClient.ts @@ -0,0 +1,5 @@ +import { QueryClient } from "@tanstack/react-query"; + +export const dashboardQueryClient = new QueryClient({ + defaultOptions: { queries: { staleTime: 30_000, retry: false } }, +}); diff --git a/dashboard/src/data/useDashboardData.ts b/dashboard/src/data/useDashboardData.ts new file mode 100644 index 0000000..b703073 --- /dev/null +++ b/dashboard/src/data/useDashboardData.ts @@ -0,0 +1,67 @@ +import { useQuery } from "@tanstack/react-query"; +import { useDashboardStore } from "../state/useDashboardStore"; +import type { Bucket } from "./localProvider"; +import { dashboardProvider } from "./provider"; + +type CollectionBucket = Exclude; + +function listCollection(bucket: CollectionBucket): Promise { + switch (bucket) { + case "clusters": + return dashboardProvider.clusters.list() as Promise; + case "nodes": + return dashboardProvider.nodes.list() as Promise; + case "agents": + return dashboardProvider.agents.list() as Promise; + case "agent-relations": + return dashboardProvider.agentRelations.list() as Promise; + case "workflows": + return dashboardProvider.workflows.list() as Promise; + case "software-catalog": + return dashboardProvider.softwareCatalog.list() as Promise; + case "software-configs": + return dashboardProvider.softwareConfigurations.list() as Promise; + case "tasks": + return dashboardProvider.tasks.list() as Promise; + case "activity": + return dashboardProvider.activity.list() as Promise; + case "telemetry": + return dashboardProvider.telemetry.list() as Promise; + default: + throw new Error(`Unsupported dashboard collection: ${bucket}`); + } +} + +export function useDemoData() { + const clusterId = useDashboardStore((state) => state.selectedClusterId); + const clusters = useDashboardStore((state) => state.clusters); + const nodes = useDashboardStore((state) => state.nodes); + const nodeQuery = useQuery({ + queryKey: ["nodes", clusterId], + queryFn: async () => + (await dashboardProvider.nodes.list()).filter( + (node) => node.clusterId === clusterId, + ), + }); + const clusterQuery = useQuery({ + queryKey: ["clusters"], + queryFn: () => dashboardProvider.clusters.list(), + }); + const filteredNodes = + nodeQuery.data ?? nodes.filter((node) => node.clusterId === clusterId); + const availableClusters = clusterQuery.data ?? clusters; + return { + cluster: availableClusters.find((cluster) => cluster.id === clusterId), + nodes: filteredNodes, + clusters: availableClusters, + isLoading: nodeQuery.isLoading || clusterQuery.isLoading, + isError: nodeQuery.isError || clusterQuery.isError, + }; +} + +export function useLocalCollection(bucket: CollectionBucket) { + return useQuery({ + queryKey: ["local-records", bucket], + queryFn: () => listCollection(bucket), + }); +} diff --git a/dashboard/src/data/workflows.ts b/dashboard/src/data/workflows.ts new file mode 100644 index 0000000..58d6d64 --- /dev/null +++ b/dashboard/src/data/workflows.ts @@ -0,0 +1,322 @@ +import { dashboardProvider } from "./provider"; + +export type WorkflowKind = + | "trigger" + | "schedule" + | "agent" + | "condition" + | "delay" + | "transform" + | "notify" + | "success" + | "failure"; + +export type WorkflowNode = { + id: string; + kind: WorkflowKind; + title: string; + detail: string; + x: number; + y: number; + config: Record; +}; + +export type WorkflowEdge = { + id: string; + source: string; + target: string; + label?: string; +}; +export type Workflow = { + version: 1; + name: string; + nodes: WorkflowNode[]; + edges: WorkflowEdge[]; +}; + +export type WorkflowHistory = { past: Workflow[]; future: Workflow[] }; + +export function recordWorkflowChange( + history: WorkflowHistory, + current: Workflow, +): WorkflowHistory { + return { + past: [...history.past.slice(-19), structuredClone(current)], + future: [], + }; +} + +export function undoWorkflow( + history: WorkflowHistory, + current: Workflow, +): { history: WorkflowHistory; workflow: Workflow } | undefined { + const previous = history.past.at(-1); + if (!previous) return undefined; + return { + history: { + past: history.past.slice(0, -1), + future: [structuredClone(current), ...history.future], + }, + workflow: structuredClone(previous), + }; +} + +export function redoWorkflow( + history: WorkflowHistory, + current: Workflow, +): { history: WorkflowHistory; workflow: Workflow } | undefined { + const next = history.future[0]; + if (!next) return undefined; + return { + history: { + past: [...history.past, structuredClone(current)].slice(-20), + future: history.future.slice(1), + }, + workflow: structuredClone(next), + }; +} + +export function isConnectionValid( + flow: Workflow, + source: string, + target: string, +): boolean { + if (!source || !target || source === target) return false; + if ( + !flow.nodes.some((node) => node.id === source) || + !flow.nodes.some((node) => node.id === target) + ) + return false; + if ( + flow.edges.some((edge) => edge.source === source && edge.target === target) + ) + return false; + const reachable = new Set(); + const pending = [target]; + while (pending.length) { + const id = pending.pop(); + if (!id || reachable.has(id)) continue; + if (id === source) return false; + reachable.add(id); + pending.push( + ...flow.edges + .filter((edge) => edge.source === id) + .map((edge) => edge.target), + ); + } + return true; +} + +const workflowRecordId = "active"; +const maxImportBytes = 100_000; +const kinds: WorkflowKind[] = [ + "trigger", + "schedule", + "agent", + "condition", + "delay", + "transform", + "notify", + "success", + "failure", +]; + +export const starterWorkflow: Workflow = { + version: 1, + name: "Cluster health check", + nodes: [ + { + id: "trigger", + kind: "schedule", + title: "Schedule trigger", + detail: "Starts a sample health check on a fixed interval.", + x: 54, + y: 210, + config: { intervalMinutes: 15 }, + }, + { + id: "inspect", + kind: "transform", + title: "Inspect cluster", + detail: "Collects mock health values from the selected demo cluster.", + x: 300, + y: 210, + config: { scope: "active cluster" }, + }, + { + id: "threshold", + kind: "condition", + title: "Health below 80%?", + detail: "Routes the sample run based on a configurable threshold.", + x: 548, + y: 210, + config: { threshold: 80 }, + }, + { + id: "notify", + kind: "notify", + title: "Notify operator", + detail: "Adds a preview notification to this simulated workflow run.", + x: 806, + y: 102, + config: { channel: "dashboard" }, + }, + { + id: "agent", + kind: "agent", + title: "Summarize status", + detail: "Creates a local sample summary. No AI service is contacted.", + x: 806, + y: 326, + config: { agent: "OpenCluster assistant" }, + }, + ], + edges: [ + { id: "e1", source: "trigger", target: "inspect", label: "on schedule" }, + { + id: "e2", + source: "inspect", + target: "threshold", + label: "health sample", + }, + { + id: "e3", + source: "threshold", + target: "notify", + label: "below threshold", + }, + { id: "e4", source: "threshold", target: "agent", label: "healthy" }, + ], +}; + +export function validateWorkflow(value: unknown): value is Workflow { + if (!value || typeof value !== "object") return false; + const flow = value as Partial; + if ( + flow.version !== 1 || + typeof flow.name !== "string" || + !flow.name.trim() || + flow.name.length > 120 || + !Array.isArray(flow.nodes) || + !Array.isArray(flow.edges) || + flow.nodes.length > 200 || + flow.edges.length > 500 + ) + return false; + const ids = new Set(); + for (const node of flow.nodes) { + if ( + !node || + typeof node.id !== "string" || + !node.id || + ids.has(node.id) || + !kinds.includes(node.kind) || + typeof node.title !== "string" || + !node.title || + node.title.length > 120 || + typeof node.detail !== "string" || + node.detail.length > 500 || + !Number.isFinite(node.x) || + !Number.isFinite(node.y) || + !node.config || + typeof node.config !== "object" || + Object.entries(node.config).length > 50 || + Object.entries(node.config).some( + ([key, value]) => + !key || + key.length > 120 || + !( + typeof value === "string" || + typeof value === "boolean" || + (typeof value === "number" && Number.isFinite(value)) + ), + ) + ) + return false; + ids.add(node.id); + } + const edgeIds = new Set(); + for (const edge of flow.edges) { + if ( + !edge || + typeof edge.id !== "string" || + !edge.id || + edgeIds.has(edge.id) || + !ids.has(edge.source) || + !ids.has(edge.target) || + edge.source === edge.target + ) + return false; + edgeIds.add(edge.id); + } + const outgoing = new Map([...ids].map((id) => [id, [] as string[]])); + for (const edge of flow.edges) outgoing.get(edge.source)?.push(edge.target); + const visiting = new Set(); + const visited = new Set(); + const hasCycle = (id: string): boolean => { + if (visiting.has(id)) return true; + if (visited.has(id)) return false; + visiting.add(id); + if (outgoing.get(id)?.some(hasCycle)) return true; + visiting.delete(id); + visited.add(id); + return false; + }; + return ![...ids].some(hasCycle); +} + +export function workflowIssues(flow: Workflow): string[] { + const issues: string[] = []; + if ( + !flow.nodes.some( + (node) => node.kind === "trigger" || node.kind === "schedule", + ) + ) + issues.push("Add a trigger or schedule node."); + for (const node of flow.nodes) + if ( + !flow.edges.some((edge) => edge.source === node.id) && + !["success", "failure", "notify", "agent"].includes(node.kind) + ) + issues.push(`${node.title} has no outgoing connection.`); + for (const edge of flow.edges) + if ( + !flow.edges.some((next) => next.target === edge.source) && + !flow.nodes.some( + (node) => + node.id === edge.source && + (node.kind === "trigger" || node.kind === "schedule"), + ) + ) + issues.push(`${edge.source} is disconnected from the trigger.`); + return [...new Set(issues)]; +} + +export async function loadWorkflow(): Promise { + try { + const parsed: unknown = + await dashboardProvider.workflows.get(workflowRecordId); + return validateWorkflow(parsed) ? parsed : structuredClone(starterWorkflow); + } catch { + return structuredClone(starterWorkflow); + } +} + +export async function saveWorkflow(flow: Workflow): Promise { + if (!validateWorkflow(flow)) + throw new Error("This workflow is not valid and could not be saved."); + await dashboardProvider.workflows.update(workflowRecordId, flow); +} + +export function parseWorkflowImport(text: string): Workflow { + assertWorkflowImportSize(new Blob([text]).size); + const value: unknown = JSON.parse(text); + if (!validateWorkflow(value)) + throw new Error("This file is not a valid OpenCluster workflow."); + return value; +} + +export function assertWorkflowImportSize(size: number): void { + if (size > maxImportBytes) + throw new Error("Workflow file exceeds the 100 KB demo limit."); +} diff --git a/dashboard/src/main.tsx b/dashboard/src/main.tsx new file mode 100644 index 0000000..cdd47e4 --- /dev/null +++ b/dashboard/src/main.tsx @@ -0,0 +1,65 @@ +import { QueryClientProvider } from "@tanstack/react-query"; +import React from "react"; +import ReactDOM from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import "@fontsource-variable/inter/wght.css"; +import "@fontsource-variable/sora/wght.css"; +import { AppErrorBoundary } from "./components/AppErrorBoundary"; +import { localProvider } from "./data/localProvider"; +import { dashboardQueryClient } from "./data/queryClient"; +import { DashboardApp } from "./pages"; +import { useDashboardStore } from "./state/useDashboardStore"; +import "./styles.css"; + +const root = document.getElementById("root"); +if (!root) throw new Error("Missing dashboard root element."); + +const appRoot = ReactDOM.createRoot(root); +appRoot.render( +
+ Loading your local OpenCluster workspace… +
, +); + +async function startDashboard() { + try { + const persisted = localStorage.getItem("opencluster-demo-preferences"); + let legacy: unknown; + if (persisted) { + try { + legacy = (JSON.parse(persisted) as { state?: unknown }).state; + } catch { + legacy = undefined; + } + } + const data = await localProvider.initialize(legacy); + const selectedClusterId = useDashboardStore.getState().selectedClusterId; + useDashboardStore.setState({ + ...data, + selectedClusterId: data.clusters.some( + (cluster) => cluster.id === selectedClusterId, + ) + ? selectedClusterId + : (data.clusters[0]?.id ?? ""), + }); + } catch (error) { + useDashboardStore.setState({ + persistenceError: + error instanceof Error + ? error.message + : "Local browser storage is unavailable.", + }); + } + appRoot.render( + + + + + + + + + , + ); +} +void startDashboard(); diff --git a/dashboard/src/pages/Activity.tsx b/dashboard/src/pages/Activity.tsx new file mode 100644 index 0000000..cf9dd4c --- /dev/null +++ b/dashboard/src/pages/Activity.tsx @@ -0,0 +1,116 @@ +import { useQueryClient } from "@tanstack/react-query"; +import * as Icons from "lucide-react"; +import { useState } from "react"; +import * as UI from "../components/ui"; +import type { ActivityEvent } from "../data/models"; +import { useLocalCollection } from "../data/useDashboardData"; +import { ActivityLine } from "./shared"; + +export function ActivityPage() { + const { + data: initialActivity = [], + isPending, + isError, + } = useLocalCollection("activity"); + const queryClient = useQueryClient(); + const [search, setSearch] = useState(""); + const [severity, setSeverity] = useState("all"); + const [source, setSource] = useState("all"); + const [readAll, setReadAll] = useState(false); + const events = initialActivity + .filter((event) => + `${event.title} ${event.detail} ${event.source}` + .toLowerCase() + .includes(search.toLowerCase()), + ) + .filter((event) => severity === "all" || event.severity === severity) + .filter((event) => source === "all" || event.source === source); + return ( + <> + setReadAll(true)}> + + {readAll ? "Sample events marked read" : "Mark all read"} + + } + /> +
+ + + + {events.length} events +
+ +
+ DEMO EVENTS FIXED SAMPLE HISTORY +
+ {isPending ? ( + + ) : isError ? ( + + void queryClient.invalidateQueries({ + queryKey: ["local-records", "activity"], + }) + } + /> + ) : events.length ? ( + events.map((event) => ) + ) : ( +
+ + No matching events + Try another search or filter. +
+ )} + {events.some((event) => event.time === "Yesterday") && ( +
+ PREVIOUS SAMPLE HISTORY +
+ )} +
+
+ This activity feed is preloaded sample + data, not an audit log. +
+ + ); +} diff --git a/dashboard/src/pages/Agents.css b/dashboard/src/pages/Agents.css new file mode 100644 index 0000000..3a52d4c --- /dev/null +++ b/dashboard/src/pages/Agents.css @@ -0,0 +1,1150 @@ +.agents-page { + --agent-ink: #dce8e9; + --agent-muted: #7c8e98; + --agent-line: rgba(110, 142, 151, 0.16); + --agent-panel: rgba(13, 22, 29, 0.82); + color: var(--agent-ink); +} + +.agents-config-form { + display: grid; + gap: 9px; + margin: 12px 0; +} + +.agents-config-form label { + display: grid; + gap: 5px; + color: var(--agent-muted); + font-size: 10px; +} + +.agents-config-form input { + min-width: 0; + padding: 8px 9px; + border: 1px solid var(--agent-line); + border-radius: 6px; + color: var(--agent-ink); + background: rgba(2, 8, 12, 0.34); +} + +.agents-demo-tag, +.agents-summary-indicator, +.agents-status-chip { + display: inline-flex; + align-items: center; + gap: 8px; + border: 1px solid rgba(98, 211, 165, 0.24); + border-radius: 999px; + padding: 7px 10px; + color: #a0d8bf; + background: rgba(24, 57, 47, 0.28); + font: 600 9px var(--mono); + letter-spacing: 0.55px; + text-transform: uppercase; +} + +.agents-demo-tag > span, +.agents-summary-indicator > i, +.agents-status-chip > i, +.agents-legend > span > i { + width: 7px; + height: 7px; + border-radius: 50%; + background: #67d2a5; + box-shadow: 0 0 10px rgba(103, 210, 165, 0.48); +} + +.agents-summary { + min-height: 78px; + margin: 19px 0 17px; + padding: 13px 17px; + display: flex; + align-items: center; + gap: 22px; + border: 1px solid var(--agent-line); + border-radius: 10px; + background: linear-gradient( + 110deg, + rgba(23, 42, 45, 0.72), + var(--agent-panel) 54%, + rgba(15, 22, 29, 0.85) + ); + box-shadow: 0 12px 36px rgba(0, 0, 0, 0.11); +} + +.agents-summary-intro { + min-width: 230px; + display: flex; + align-items: center; + gap: 12px; +} + +.agents-summary-icon { + width: 38px; + height: 38px; + display: grid; + place-items: center; + border: 1px solid rgba(98, 211, 165, 0.22); + border-radius: 9px; + color: #83d9b5; + background: rgba(57, 131, 103, 0.12); +} + +.agents-summary-intro strong, +.agents-summary-intro small { + display: block; +} + +.agents-summary-intro strong { + font-size: 12px; + font-weight: 600; +} + +.agents-summary-intro small { + margin-top: 4px; + color: var(--agent-muted); + font-size: 10px; +} + +.agents-summary-stats { + flex: 1; + display: flex; + gap: clamp(20px, 4vw, 54px); + padding-left: 24px; + border-left: 1px solid var(--agent-line); +} + +.agents-summary-stats > div span, +.agents-summary-stats > div strong { + display: block; +} + +.agents-summary-stats > div span, +.agents-inspector-kicker, +.agents-inspector-count, +.agents-section-heading, +.agents-model-box > span, +.agents-inspector-metrics > div span { + color: #778a94; + font: 500 8px var(--mono); + letter-spacing: 0.85px; +} + +.agents-summary-stats > div strong { + margin-top: 5px; + color: #d8e6e5; + font: 600 17px var(--mono); +} + +.agents-summary-indicator { + flex: none; + color: #91a79e; + border-color: rgba(122, 160, 146, 0.16); + background: rgba(16, 28, 27, 0.45); + font-size: 8px; +} + +.agents-workspace { + display: grid; + grid-template-columns: minmax(0, 1fr) 276px; + align-items: stretch; + gap: 14px; +} + +.agents-canvas-panel, +.agents-inspector { + min-width: 0; + border: 1px solid var(--agent-line); + border-radius: 10px; + background: var(--agent-panel); + box-shadow: 0 14px 34px rgba(0, 0, 0, 0.13); +} + +.agents-canvas-panel { + overflow: hidden; +} + +.agents-canvas-toolbar { + min-height: 61px; + padding: 12px 15px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + border-bottom: 1px solid var(--agent-line); +} + +.agents-toolbar-title, +.agents-toolbar-subtitle { + display: block; +} + +.agents-toolbar-title { + color: #d5e0e2; + font-size: 11px; + font-weight: 600; +} + +.agents-toolbar-subtitle { + margin-top: 4px; + color: #72838d; + font-size: 9px; +} + +.agents-view-switch { + display: inline-flex; + gap: 3px; + padding: 3px; + border: 1px solid rgba(128, 153, 163, 0.14); + border-radius: 7px; + background: rgba(2, 8, 12, 0.34); +} + +.agents-view-switch button { + height: 27px; + padding: 0 9px; + display: inline-flex; + align-items: center; + gap: 6px; + border: 0; + border-radius: 5px; + color: #85959d; + background: transparent; + font-size: 9px; + transition: + color 140ms ease, + background 140ms ease; +} + +.agents-view-switch button:hover { + color: #d4e1e2; +} + +.agents-view-switch button[aria-pressed="true"] { + color: #d6e8e3; + background: #1b302e; + box-shadow: inset 0 0 0 1px rgba(106, 197, 161, 0.19); +} + +.agents-graph-wrap { + position: relative; + min-height: 410px; + overflow: hidden; + background: + radial-gradient(ellipse at 46% 44%, rgba(34, 98, 88, 0.12), transparent 45%), + linear-gradient(145deg, rgba(8, 16, 21, 0.2), rgba(9, 17, 23, 0.78)); +} + +.agents-reactflow { + min-height: 510px; + height: clamp(510px, 54vw, 680px); + background: + radial-gradient(ellipse at 46% 44%, rgba(34, 98, 88, 0.12), transparent 45%), + linear-gradient(145deg, rgba(8, 16, 21, 0.2), rgba(9, 17, 23, 0.78)); +} + +.agents-reactflow .react-flow__background pattern circle { + fill: rgba(115, 153, 161, 0.2); +} + +.agents-reactflow .react-flow__controls, +.agents-reactflow .react-flow__minimap { + overflow: hidden; + border: 1px solid rgba(124, 155, 165, 0.18); + border-radius: 7px; + background: rgba(8, 15, 20, 0.92); +} + +.agents-reactflow .react-flow__controls-button { + border-bottom: 1px solid rgba(124, 155, 165, 0.12); + background: transparent; + fill: #a9bcc2; +} + +.agents-reactflow .react-flow__edge-textbg { + fill: #0d171c; +} + +.agents-reactflow .react-flow__node-agent { + width: 172px; + border: 0; + background: transparent; +} + +.agent-flow-card { + position: relative; + min-height: 92px; + padding: 14px 12px 10px 48px; + border: 1px solid rgba(128, 159, 166, 0.34); + border-radius: 11px; + background: linear-gradient(145deg, #16252c, #101a20); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.38); + color: #e2ecec; +} + +.agent-flow-card.selected, +.react-flow__node.selected .agent-flow-card { + border-color: rgba(124, 211, 180, 0.78); + box-shadow: + 0 0 0 2px rgba(124, 211, 180, 0.11), + 0 10px 24px rgba(0, 0, 0, 0.38); +} + +.agent-flow-icon { + position: absolute; + top: 20px; + left: 11px; + width: 26px; + height: 26px; + display: grid; + place-items: center; + border: 1px solid rgba(103, 196, 159, 0.25); + border-radius: 7px; + color: #8edcba; + background: rgba(103, 196, 159, 0.12); +} + +.agent-flow-card.peach .agent-flow-icon { + color: #efb68f; + background: rgba(241, 173, 125, 0.12); +} +.agent-flow-card.violet .agent-flow-icon { + color: #b6a0f3; + background: rgba(176, 151, 243, 0.12); +} +.agent-flow-card.blue .agent-flow-icon { + color: #7bbde9; + background: rgba(104, 179, 236, 0.12); +} +.agent-flow-card.gold .agent-flow-icon { + color: #dfbf7d; + background: rgba(229, 191, 121, 0.12); +} +.agent-flow-card small, +.agent-flow-card strong, +.agent-flow-card em { + display: block; +} +.agent-flow-card small { + color: #84969d; + font: 500 7px var(--mono); + letter-spacing: 0.8px; +} +.agent-flow-card strong { + margin-top: 6px; + font: 600 11px var(--display); +} +.agent-flow-card em { + margin-top: 5px; + color: #788e96; + font: normal 6.5px var(--mono); + letter-spacing: 0.55px; +} +.agent-flow-status { + position: absolute; + top: 11px; + right: 11px; + width: 6px; + height: 6px; + border-radius: 50%; + background: #67d2a5; + box-shadow: 0 0 8px rgba(103, 210, 165, 0.48); +} +.agent-flow-status.idle { + background: #8d9ba4; + box-shadow: none; +} +.agent-flow-status.attention { + background: #e5b76b; +} +.agent-flow-status.paused { + background: #7b8490; + box-shadow: none; +} +.agents-toolbar-actions { + display: flex; + align-items: center; + gap: 8px; +} +.agents-fit-button { + min-height: 29px; + padding: 0 9px; + border: 1px solid rgba(124, 155, 165, 0.18); + border-radius: 5px; + color: #a9bcc2; + background: rgba(8, 15, 20, 0.65); + font-size: 9px; +} +.agents-fit-button:hover { + color: #dcf1e9; + border-color: rgba(121, 194, 171, 0.36); +} + +.agents-graph { + width: 100%; + height: clamp(410px, 49vw, 570px); + display: block; + color: #65818b; + touch-action: none; + cursor: grab; + user-select: none; +} + +.agents-graph:active { + cursor: grabbing; +} + +.agents-graph:focus-visible { + outline: 2px solid #66b9eb; + outline-offset: -3px; +} + +.agent-node { + cursor: grab; + outline: none; +} + +.agent-node:active { + cursor: grabbing; +} + +.agent-node:focus-visible .agent-node-card { + stroke: #d0e3de; + stroke-width: 2; +} + +.agent-node-card { + fill: #111e25; + stroke: rgba(128, 159, 166, 0.29); + stroke-width: 1; + filter: drop-shadow(0 8px 13px rgba(0, 0, 0, 0.37)); + transition: + fill 160ms ease, + stroke 160ms ease, + stroke-width 160ms ease; +} + +.agent-node:hover .agent-node-card, +.agent-node.selected .agent-node-card { + fill: #15282d; + stroke: rgba(124, 211, 180, 0.68); +} + +.agent-node.selected .agent-node-card { + stroke-width: 1.5; +} + +.agent-node-glow { + fill: rgba(85, 206, 163, 0.08); + opacity: 0; + filter: url(#agent-glow); + transition: opacity 180ms ease; +} + +.agent-node.selected .agent-node-glow { + opacity: 0.7; +} + +.agent-node-icon-bg { + fill: rgba(103, 196, 159, 0.13); + stroke: rgba(103, 196, 159, 0.25); +} + +.agent-node-icon-inner { + width: 20px; + height: 20px; + display: grid; + place-items: center; + color: #8edcba; +} + +.agent-node.peach .agent-node-icon-bg, +.agent-profile-mark.peach { + fill: rgba(241, 173, 125, 0.13); + background: rgba(241, 173, 125, 0.12); + stroke: rgba(241, 173, 125, 0.28); + color: #efb68f; +} + +.agent-node.violet .agent-node-icon-bg, +.agent-profile-mark.violet { + fill: rgba(176, 151, 243, 0.13); + background: rgba(176, 151, 243, 0.12); + stroke: rgba(176, 151, 243, 0.3); + color: #b6a0f3; +} + +.agent-node.blue .agent-node-icon-bg, +.agent-profile-mark.blue { + fill: rgba(104, 179, 236, 0.13); + background: rgba(104, 179, 236, 0.12); + stroke: rgba(104, 179, 236, 0.3); + color: #7bbde9; +} + +.agent-node.gold .agent-node-icon-bg, +.agent-profile-mark.gold { + fill: rgba(229, 191, 121, 0.13); + background: rgba(229, 191, 121, 0.12); + stroke: rgba(229, 191, 121, 0.28); + color: #dfbf7d; +} + +.agent-node-status { + stroke: #111e25; + stroke-width: 2; +} + +.agent-node-status.active, +.agents-status-chip.active > i, +.agents-legend i.active { + fill: #67d2a5; + background: #67d2a5; +} + +.agent-node-status.idle, +.agents-status-chip.idle > i, +.agents-legend i.idle { + fill: #8d9ba4; + background: #8d9ba4; + box-shadow: none; +} + +.agent-node-status.attention, +.agents-status-chip.attention > i, +.agents-legend i.attention { + fill: #e5b76b; + background: #e5b76b; + box-shadow: 0 0 10px rgba(229, 183, 107, 0.38); +} + +.agent-node-status.paused, +.agents-status-chip.paused > i { + fill: #7b8490; + background: #7b8490; + box-shadow: none; +} + +.agent-node-vendor, +.agent-node-meta { + fill: #84969d; + font: 500 7px var(--mono); + letter-spacing: 0.8px; +} + +.agent-node-name { + fill: #e2ecec; + font: 600 12px var(--display); + letter-spacing: -0.2px; +} + +.agent-node-meta { + fill: #788e96; + font-size: 6.5px; + letter-spacing: 0.55px; +} + +.agents-edge path { + fill: none; + stroke: rgba(103, 144, 150, 0.32); + stroke-dasharray: 4 7; + transition: + stroke 150ms ease, + opacity 150ms ease; +} + +.agents-edge.highlighted path { + stroke: rgba(113, 208, 171, 0.61); +} + +.agents-edge-pulse { + fill: #8de4bd; + filter: drop-shadow(0 0 4px #59cd9d); +} + +.agents-page.motion-off .agents-edge-pulse { + display: none; +} + +.agents-edge text { + fill: #8ab6a7; + stroke: #0d161b; + stroke-width: 5px; + paint-order: stroke; + font: 500 8px var(--mono); + text-anchor: middle; +} + +.agents-canvas-hint { + fill: #526672; + font: 500 7px var(--mono); + letter-spacing: 0.8px; +} + +.agents-canvas-controls { + position: absolute; + right: 12px; + bottom: 13px; + min-width: 0; + margin: 0; + display: flex; + align-items: center; + gap: 4px; + padding: 4px; + border: 1px solid rgba(124, 155, 165, 0.18); + border-radius: 7px; + background: rgba(8, 15, 20, 0.88); + box-shadow: 0 6px 22px rgba(0, 0, 0, 0.23); +} + +.agents-canvas-controls legend { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} + +.agents-canvas-controls button { + width: 27px; + height: 27px; + display: grid; + place-items: center; + border: 0; + border-radius: 4px; + color: #9aabb2; + background: transparent; +} + +.agents-canvas-controls button:hover { + color: #dcf1e9; + background: #1a2a2f; +} + +.agents-canvas-controls span:not(.agents-control-divider) { + min-width: 37px; + color: #8b9ca4; + font: 500 9px var(--mono); + text-align: center; +} + +.agents-control-divider { + height: 17px; + margin: 0 3px; + border-left: 1px solid var(--agent-line); +} + +.agents-legend { + min-height: 39px; + padding: 0 15px; + display: flex; + align-items: center; + gap: 18px; + border-top: 1px solid var(--agent-line); + color: #84949c; + font-size: 9px; +} + +.agents-legend > span { + display: inline-flex; + align-items: center; + gap: 7px; + white-space: nowrap; +} + +.agents-legend > span > i { + width: 6px; + height: 6px; + box-shadow: none; +} + +.agents-legend-note { + margin-left: auto; + color: #62737d; + font-size: 8px; +} + +.agents-inspector { + padding: 15px; + display: flex; + flex-direction: column; +} + +.agents-inspector-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.agents-inspector-head > div { + display: flex; + align-items: center; + gap: 8px; +} + +.agents-inspector-count { + color: #566a74; + font-size: 7px; +} + +.agents-status-chip { + gap: 6px; + padding: 5px 7px; + color: #9ab0a5; + border-color: rgba(113, 157, 136, 0.17); + background: rgba(18, 34, 29, 0.35); + font-size: 7px; +} + +.agents-status-chip > i { + width: 5px; + height: 5px; +} + +.agents-profile-mark { + width: 45px; + height: 45px; + margin-top: 24px; + display: grid; + place-items: center; + border: 1px solid rgba(103, 196, 159, 0.25); + border-radius: 11px; + color: #8edcba; + background: rgba(103, 196, 159, 0.12); +} + +.agents-profile-mark.mint { + color: #8edcba; +} + +.agents-inspector h2 { + margin: 13px 0 5px; + color: #e2ebeb; + font-size: 18px; + font-weight: 600; + letter-spacing: -0.5px; +} + +.agents-profile-vendor { + margin: 0; + color: #82939b; + font-size: 9px; +} + +.agents-profile-vendor span { + color: #52646d; + padding: 0 3px; +} + +.agents-model-box { + margin-top: 19px; + padding: 11px; + border: 1px solid rgba(119, 148, 158, 0.13); + border-radius: 7px; + background: rgba(7, 14, 19, 0.45); +} + +.agents-model-box > span, +.agents-model-box > strong, +.agents-model-box > small { + display: block; +} + +.agents-model-box > strong { + margin-top: 8px; + color: #c2d2d5; + font-size: 10px; + font-weight: 500; + line-height: 1.45; +} + +.agents-model-box > small { + margin-top: 5px; + color: #61747d; + font-size: 8px; +} + +.agents-inspector-metrics { + display: grid; + grid-template-columns: 1fr 1fr; + margin: 14px 0; + padding: 0 0 13px; + border-bottom: 1px solid var(--agent-line); +} + +.agents-inspector-metrics > div + div { + padding-left: 14px; + border-left: 1px solid var(--agent-line); +} + +.agents-inspector-metrics > div span, +.agents-inspector-metrics > div strong { + display: block; +} + +.agents-inspector-metrics > div span { + font-size: 7px; +} + +.agents-inspector-metrics > div strong { + margin-top: 7px; + color: #d4e0e0; + font: 600 13px var(--mono); +} + +.agents-section-heading { + display: flex; + align-items: center; + justify-content: space-between; +} + +.agents-section-heading > span:last-child { + color: #687c85; +} + +.agents-connected-row { + width: 100%; + min-height: 45px; + margin-top: 5px; + padding: 6px 7px; + display: flex; + align-items: center; + gap: 9px; + border: 1px solid transparent; + border-radius: 6px; + color: #c4d1d3; + text-align: left; + background: transparent; +} + +.agents-connected-row:hover { + border-color: rgba(114, 166, 153, 0.16); + background: rgba(26, 43, 43, 0.42); +} + +.agents-connected-dot { + width: 7px; + height: 7px; + flex: none; + border-radius: 50%; + background: #67d2a5; +} + +.agents-connected-dot.peach { + background: #eeb28c; +} +.agents-connected-dot.violet { + background: #b09aee; +} +.agents-connected-dot.blue { + background: #73b6e7; +} +.agents-connected-dot.gold { + background: #d8b978; +} + +.agents-connected-row > span:nth-child(2) { + flex: 1; + min-width: 0; + font-size: 9px; +} + +.agents-connected-row small { + display: block; + margin-top: 3px; + color: #687b84; + font-size: 8px; +} + +.agents-connected-arrow { + color: #61747d; + transform: rotate(-90deg); +} + +.agents-no-links { + color: #788a92; + font-size: 9px; + line-height: 1.5; +} + +.agents-directory { + margin-top: 15px; + padding-top: 12px; + border-top: 1px solid var(--agent-line); +} + +.agents-directory > button { + width: 100%; + min-height: 34px; + padding: 5px 6px; + display: flex; + align-items: center; + gap: 8px; + border: 1px solid transparent; + border-radius: 5px; + color: #c4d1d3; + text-align: left; + background: transparent; + font-size: 9px; +} + +.agents-directory > button:hover, +.agents-directory > button[aria-pressed="true"] { + border-color: rgba(114, 166, 153, 0.16); + background: rgba(26, 43, 43, 0.42); +} + +.agents-directory > button > span:last-child { + flex: 1; +} + +.agents-directory small { + display: block; + margin-top: 2px; + color: #687b84; + font-size: 8px; +} + +.agents-demo-action { + min-height: 34px; + margin-top: auto; + padding: 0 10px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + border: 1px solid rgba(110, 150, 154, 0.18); + border-radius: 6px; + color: #b7c9ca; + background: rgba(24, 38, 42, 0.42); + font-size: 9px; +} + +.agents-demo-action:hover { + border-color: rgba(121, 194, 171, 0.36); + color: #e0f0e9; +} + +.agents-inspector-note { + margin: 9px 0 0; + color: #60727a; + font-size: 8px; + line-height: 1.5; +} + +.agents-footer-note { + min-height: 41px; + display: flex; + align-items: center; + gap: 8px; + color: #778891; + font-size: 9px; +} + +.agents-footer-note > svg { + color: #76a99a; +} + +.agents-footer-note > span { + height: 12px; + margin: 0 2px; + border-left: 1px solid var(--agent-line); +} + +.agents-footer-note button { + padding: 4px 7px; + border: 1px solid rgba(112, 145, 152, 0.17); + border-radius: 5px; + color: #9cb0b4; + background: rgba(14, 24, 29, 0.75); + font-size: 8px; +} + +.agents-footer-note small { + color: #5f7078; + font-size: 8px; +} + +@media (max-width: 1180px) { + .agents-workspace { + grid-template-columns: minmax(0, 1fr) 255px; + } + + .agents-legend { + gap: 11px; + } +} + +@media (max-width: 900px) { + .agents-workspace { + grid-template-columns: 1fr; + } + + .agents-inspector { + display: grid; + grid-template-columns: minmax(140px, 0.8fr) minmax(210px, 1.2fr); + column-gap: 18px; + } + + .agents-inspector-head, + .agents-inspector > .agents-profile-mark, + .agents-inspector > h2, + .agents-inspector > .agents-profile-vendor, + .agents-inspector > .agents-demo-action, + .agents-inspector > .agents-inspector-note { + grid-column: 1 / -1; + } + + .agents-inspector > .agents-profile-mark { + margin-top: 18px; + } + + .agents-inspector > h2 { + margin-top: -50px; + padding-left: 57px; + } + + .agents-inspector > .agents-profile-vendor { + margin-top: -18px; + padding-left: 57px; + } + + .agents-model-box { + margin-top: 17px; + } + + .agents-inspector-metrics { + align-self: start; + margin-top: 17px; + border-bottom: 0; + } + + .agents-links-section { + grid-column: 1 / -1; + margin-top: 7px; + padding-top: 12px; + border-top: 1px solid var(--agent-line); + } + + .agents-demo-action { + grid-row: 5; + margin-top: 12px; + } + + .agents-inspector-note { + grid-row: 6; + } +} + +@media (max-width: 640px) { + .agents-summary { + flex-wrap: wrap; + gap: 14px; + padding: 12px; + } + + .agents-summary-intro { + min-width: 100%; + } + + .agents-summary-stats { + flex: 1; + gap: clamp(15px, 7vw, 36px); + padding-left: 0; + border-left: 0; + } + + .agents-summary-indicator { + margin-left: auto; + } + + .agents-canvas-toolbar { + align-items: flex-start; + flex-direction: column; + } + + .agents-graph-wrap { + min-height: 360px; + } + + .agents-graph { + height: 390px; + } + + .agents-legend { + padding: 9px 12px; + flex-wrap: wrap; + gap: 9px 13px; + } + + .agents-legend-note { + width: 100%; + margin-left: 0; + } + + .agents-inspector { + display: flex; + } + + .agents-inspector > h2 { + margin-top: 13px; + padding-left: 0; + } + + .agents-inspector > .agents-profile-vendor { + margin-top: 0; + padding-left: 0; + } + + .agents-model-box, + .agents-inspector-metrics { + margin-top: 14px; + } + + .agents-inspector-metrics { + border-bottom: 1px solid var(--agent-line); + } + + .agents-links-section { + margin-top: 0; + padding-top: 0; + border-top: 0; + } + + .agents-demo-action { + margin-top: 13px; + } + + .agents-footer-note { + align-items: flex-start; + flex-wrap: wrap; + padding: 10px 0; + } + + .agents-footer-note small { + width: 100%; + padding-left: 22px; + } +} + +@media (prefers-reduced-motion: reduce) { + .agents-edge-pulse { + display: none; + } + + .agents-page *, + .agents-page *::before, + .agents-page *::after { + animation-duration: 0.01ms; + animation-iteration-count: 1; + scroll-behavior: auto; + transition-duration: 0.01ms; + } +} + +html[data-motion="reduced"] .agents-edge-pulse { + display: none; +} diff --git a/dashboard/src/pages/Agents.tsx b/dashboard/src/pages/Agents.tsx new file mode 100644 index 0000000..8b88dd1 --- /dev/null +++ b/dashboard/src/pages/Agents.tsx @@ -0,0 +1,1673 @@ +import { + // ORIGINAL (preserved, not deleted — disabled by comment): addEdge, + Background, + type Connection, + Controls, + type Edge, + type EdgeChange, + Handle, + MarkerType, + MiniMap, + type Node, + type NodeProps, + Position, + ReactFlow, + ReactFlowProvider, + useEdgesState, + useNodesState, + useReactFlow, +} from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; +import { useQueryClient } from "@tanstack/react-query"; +import { + Activity, + Bot, + BrainCircuit, + ChevronDown, + CircleHelp, + Command, + Cpu, + GitBranch, + RotateCcw, + Sparkles, + Waypoints, +} from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { PageTitle } from "../components/ui"; +import { + type AgentGraphLayout, + type AgentGraphMode, + type AgentGraphPoint, + readAgentGraphLayout, + writeAgentGraphLayout, +} from "../data/agentLayout"; +import type { AgentEntity, AgentRelation } from "../data/agents"; +import { dashboardProvider } from "../data/provider"; +import { useLocalCollection } from "../data/useDashboardData"; +import "./Agents.css"; + +type AgentNodeData = { agent: AgentEntity }; +type FlowNode = Node; +const VIEW = { width: 900, height: 570 }; +const NETWORK_POSITIONS: Record = { + codex: { x: 250, y: 280 }, + claude: { x: 570, y: 145 }, + hermes: { x: 675, y: 310 }, + openclaw: { x: 515, y: 445 }, + custom: { x: 225, y: 460 }, + model: { x: 110, y: 155 }, + tools: { x: 775, y: 165 }, + memory: { x: 800, y: 480 }, + task: { x: 345, y: 510 }, + service: { x: 110, y: 390 }, +}; +function positionsFor( + mode: AgentGraphMode, + agents: AgentEntity[], +): Record { + if (mode === "network") return NETWORK_POSITIONS; + const center = { x: VIEW.width / 2, y: VIEW.height / 2 }; + const peers = agents.filter((agent) => agent.id !== "codex"); + return Object.fromEntries( + agents.map((agent) => { + if (agent.id === "codex") return [agent.id, center]; + const index = peers.findIndex((peer) => peer.id === agent.id); + const angle = + -Math.PI / 2 + (index * (Math.PI * 2)) / Math.max(1, peers.length); + return [ + agent.id, + { + x: center.x + Math.cos(angle) * 255, + y: center.y + Math.sin(angle) * 190, + }, + ]; + }), + ); +} +function initialLayout(agents: AgentEntity[]): AgentGraphLayout { + return { + positions: { + network: positionsFor("network", agents), + radial: positionsFor("radial", agents), + }, + cameras: { + network: { x: 0, y: 0, zoom: 1 }, + radial: { x: 0, y: 0, zoom: 1 }, + }, + }; +} +function AgentGlyph({ icon }: { icon: AgentEntity["icon"] }) { + if (icon === "codex") return ; + if (icon === "claude") return ; + if (icon === "hermes") return ; + if (icon === "openclaw") return ; + return ; +} +function AgentFlowCard({ data }: NodeProps) { + const agent = data.agent; + return ( + <> + +
+ + + + + {agent.kind.toUpperCase()} + {agent.name} + {agent.status.toUpperCase()} +
+ + + ); +} +const nodeTypes = { agent: AgentFlowCard }; +function asNodes( + agents: AgentEntity[], + positions: Record, +): FlowNode[] { + return agents.map((agent) => ({ + id: agent.id, + type: "agent", + position: positions[agent.id] ?? { x: VIEW.width / 2, y: VIEW.height / 2 }, + data: { agent }, + })); +} +function asEdges(relations: AgentRelation[], selectedId: string): Edge[] { + return relations.map((relation) => { + const highlighted = + relation.source === selectedId || relation.target === selectedId; + return { + id: relation.source + ":" + relation.target, + source: relation.source, + target: relation.target, + label: highlighted ? relation.label : undefined, + animated: highlighted, + markerEnd: { + type: MarkerType.ArrowClosed, + color: highlighted ? "#75d4ae" : "#617c84", + }, + style: { + stroke: highlighted ? "#75d4ae" : "#617c84", + strokeWidth: highlighted ? 1.8 : 1.2, + }, + labelStyle: { fill: "#a1c9b9", fontSize: 9 }, + labelBgStyle: { fill: "#0d171c", fillOpacity: 0.92 }, + }; + }); +} +function AgentsContent() { + const { + data: storedAgents = [], + isPending, + isError, + } = useLocalCollection("agents"); + const { data: relations = [] } = + useLocalCollection("agent-relations"); + const queryClient = useQueryClient(); + const [graphError, setGraphError] = useState(""); + const [agentDraft, setAgentDraft] = useState({ + name: "", + role: "", + model: "", + }); + const agents = useMemo(() => storedAgents, [storedAgents]); + const [layout, setLayout] = useState(() => + initialLayout([]), + ); + const layoutEdited = useRef(false); + const [layoutLoaded, setLayoutLoaded] = useState(false); + const [mode, setMode] = useState("network"); + const [selectedId, setSelectedId] = useState("codex"); + const [live, setLive] = useState(true); + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const flow = useReactFlow(); + const selected = agents.find((agent) => agent.id === selectedId) ?? agents[0]; + const selectedEdges = relations.filter( + (edge) => edge.source === selected?.id || edge.target === selected?.id, + ); + + useEffect(() => { + if (selected) + setAgentDraft({ + name: selected.name, + role: selected.role, + model: selected.model, + }); + }, [selected]); + + useEffect(() => { + if (isPending) return; + let active = true; + void readAgentGraphLayout(initialLayout(storedAgents)).then((saved) => { + if (!active) return; + if (!layoutEdited.current) setLayout(saved); + setLayoutLoaded(true); + }); + return () => { + active = false; + }; + }, [isPending, storedAgents]); + useEffect(() => { + if (!layoutLoaded) return; + const timeout = window.setTimeout( + () => void writeAgentGraphLayout(layout), + 250, + ); + return () => window.clearTimeout(timeout); + }, [layout, layoutLoaded]); + useEffect(() => { + if (!agents.length) return; + setNodes(asNodes(agents, layout.positions[mode])); + setEdges(asEdges(relations, selectedId)); + }, [ + agents, + relations, + layout.positions, + mode, + selectedId, + setEdges, + setNodes, + ]); + useEffect(() => { + if (layoutLoaded) + void flow.setViewport(layout.cameras[mode], { duration: 0 }); + }, [flow, layout.cameras, layoutLoaded, mode]); + + const updateCamera = useCallback( + ( + _event: unknown, + viewport: AgentGraphLayout["cameras"][AgentGraphMode], + ) => { + setLayout((value) => ({ + ...value, + cameras: { ...value.cameras, [mode]: viewport }, + })); + }, + [mode], + ); + const connect = useCallback( + (connection: Connection) => { + if (!connection.source || !connection.target) return; + const relation: AgentRelation = { + source: connection.source, + target: connection.target, + label: "local relation", + strength: 0.5, + }; + if ( + relation.source === relation.target || + relations.some( + (edge) => + edge.source === relation.source && edge.target === relation.target, + ) + ) + return; + void dashboardProvider.agentRelations + .create(`${relation.source}:${relation.target}`, relation) + .then(() => + queryClient.invalidateQueries({ + queryKey: ["local-records", "agent-relations"], + }), + ) + .catch(() => setGraphError("Relationship could not be saved locally.")); + }, + [queryClient, relations], + ); + const changeEdges = useCallback( + (changes: EdgeChange[]) => { + for (const change of changes) { + if (change.type !== "remove") continue; + const [source, target] = change.id.split(":"); + if (source && target) + void dashboardProvider.agentRelations + .delete(`${source}:${target}`) + .then(() => + queryClient.invalidateQueries({ + queryKey: ["local-records", "agent-relations"], + }), + ) + .catch(() => + setGraphError("Relationship could not be removed locally."), + ); + } + onEdgesChange(changes); + }, + [onEdgesChange, queryClient], + ); + const toggleAgentStatus = () => { + if (selected) + void dashboardProvider.agents + .update(selected.id, { + ...selected, + status: selected.status === "paused" ? "idle" : "paused", + }) + .then(() => + queryClient.invalidateQueries({ + queryKey: ["local-records", "agents"], + }), + ) + .catch(() => + setGraphError("Agent configuration could not be saved locally."), + ); + }; + const fitView = () => void flow.fitView({ padding: 0.18, duration: 250 }); + const resetGraph = () => { + layoutEdited.current = true; + setLayout((value) => ({ + ...value, + positions: { ...value.positions, [mode]: positionsFor(mode, agents) }, + cameras: { ...value.cameras, [mode]: { x: 0, y: 0, zoom: 1 } }, + })); + void flow.fitView({ padding: 0.18, duration: 0 }); + }; + const onNodeDragStop = (_event: MouseEvent | TouchEvent, node: FlowNode) => { + layoutEdited.current = true; + setLayout((value) => ({ + ...value, + positions: { + ...value.positions, + [mode]: { ...value.positions[mode], [node.id]: node.position }, + }, + })); + }; + + if (isPending) + return ( +
+ +
+ ); + if (isError || !selected) + return ( +
+ +
+ ); + + return ( +
+ + DEMO GRAPH + + } + /> +
+
+ + + +
+ Agent network + Local sample topology · no external connections +
+
+
+
+ ENTITIES + {agents.length.toString().padStart(2, "0")} +
+
+ DEMO LINKS + {relations.length.toString().padStart(2, "0")} +
+
+ ACTIVE TASKS + + {agents + .reduce((sum, agent) => sum + agent.tasks, 0) + .toString() + .padStart(2, "0")} + +
+
+ + Local demo data + +
+
+
+
+
+ Connection map + + Drag nodes or canvas to explore · select an entity for details + +
+
+
+ + +
+ +
+
+
+ setSelectedId(node.id)} + onNodeDragStop={onNodeDragStop} + onMoveEnd={updateCamera} + onPaneClick={() => setSelectedId("")} + fitView + fitViewOptions={{ padding: 0.18 }} + minZoom={0.55} + maxZoom={1.8} + nodesDraggable + nodesConnectable + elementsSelectable + aria-label="Agent relationship graph" + > + + + + node.id === selectedId ? "#75d4ae" : "#607b85" + } + /> + +
+
+ + Active + + + Idle + + + Needs attention + + + Lines show illustrative directed relationships + +
+
+ +
+
+ Sample graph data only + + Decorative connection motion can be switched off. + +
+
+ ); +} +export function AgentsPage() { + return ( + + + + ); +} + +// ORIGINAL (preserved, not deleted — disabled by comment): import { +// ORIGINAL (preserved, not deleted — disabled by comment): Activity, +// ORIGINAL (preserved, not deleted — disabled by comment): ArrowDown, +// ORIGINAL (preserved, not deleted — disabled by comment): ArrowLeft, +// ORIGINAL (preserved, not deleted — disabled by comment): ArrowRight, +// ORIGINAL (preserved, not deleted — disabled by comment): ArrowUp, +// ORIGINAL (preserved, not deleted — disabled by comment): Bot, +// ORIGINAL (preserved, not deleted — disabled by comment): BrainCircuit, +// ORIGINAL (preserved, not deleted — disabled by comment): ChevronDown, +// ORIGINAL (preserved, not deleted — disabled by comment): CircleHelp, +// ORIGINAL (preserved, not deleted — disabled by comment): Command, +// ORIGINAL (preserved, not deleted — disabled by comment): Cpu, +// ORIGINAL (preserved, not deleted — disabled by comment): GitBranch, +// ORIGINAL (preserved, not deleted — disabled by comment): Minus, +// ORIGINAL (preserved, not deleted — disabled by comment): Plus, +// ORIGINAL (preserved, not deleted — disabled by comment): RotateCcw, +// ORIGINAL (preserved, not deleted — disabled by comment): Sparkles, +// ORIGINAL (preserved, not deleted — disabled by comment): Waypoints, +// ORIGINAL (preserved, not deleted — disabled by comment): } from "lucide-react"; +// ORIGINAL (preserved, not deleted — disabled by comment): import { +// ORIGINAL (preserved, not deleted — disabled by comment): // ORIGINAL (preserved, not deleted — disabled by comment): type KeyboardEvent as ReactKeyboardEvent, +// ORIGINAL (preserved, not deleted — disabled by comment): type PointerEvent as ReactPointerEvent, +// ORIGINAL (preserved, not deleted — disabled by comment): type WheelEvent as ReactWheelEvent, +// ORIGINAL (preserved, not deleted — disabled by comment): useCallback, +// ORIGINAL (preserved, not deleted — disabled by comment): useEffect, +// ORIGINAL (preserved, not deleted — disabled by comment): useRef, +// ORIGINAL (preserved, not deleted — disabled by comment): useState, +// ORIGINAL (preserved, not deleted — disabled by comment): } from "react"; +// ORIGINAL (preserved, not deleted — disabled by comment): import { PageTitle } from "../components/ui"; +// ORIGINAL (preserved, not deleted — disabled by comment): import { +// ORIGINAL (preserved, not deleted — disabled by comment): type AgentGraphLayout, +// ORIGINAL (preserved, not deleted — disabled by comment): readAgentGraphLayout, +// ORIGINAL (preserved, not deleted — disabled by comment): writeAgentGraphLayout, +// ORIGINAL (preserved, not deleted — disabled by comment): } from "../data/agentLayout"; +// ORIGINAL (preserved, not deleted — disabled by comment): import "./Agents.css"; +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): type AgentStatus = "active" | "idle" | "attention" | "paused"; +// ORIGINAL (preserved, not deleted — disabled by comment): type GraphMode = "network" | "radial"; +// ORIGINAL (preserved, not deleted — disabled by comment): type Point = { x: number; y: number }; +// ORIGINAL (preserved, not deleted — disabled by comment): type AgentNode = { +// ORIGINAL (preserved, not deleted — disabled by comment): id: string; +// ORIGINAL (preserved, not deleted — disabled by comment): name: string; +// ORIGINAL (preserved, not deleted — disabled by comment): vendor: string; +// ORIGINAL (preserved, not deleted — disabled by comment): model: string; +// ORIGINAL (preserved, not deleted — disabled by comment): role: string; +// ORIGINAL (preserved, not deleted — disabled by comment): status: AgentStatus; +// ORIGINAL (preserved, not deleted — disabled by comment): tasks: number; +// ORIGINAL (preserved, not deleted — disabled by comment): latency: string; +// ORIGINAL (preserved, not deleted — disabled by comment): color: string; +// ORIGINAL (preserved, not deleted — disabled by comment): icon: "codex" | "claude" | "hermes" | "openclaw" | "custom"; +// ORIGINAL (preserved, not deleted — disabled by comment): kind: "agent" | "model" | "tool" | "memory" | "task" | "service"; +// ORIGINAL (preserved, not deleted — disabled by comment): }; +// ORIGINAL (preserved, not deleted — disabled by comment): type AgentEdge = { +// ORIGINAL (preserved, not deleted — disabled by comment): source: string; +// ORIGINAL (preserved, not deleted — disabled by comment): target: string; +// ORIGINAL (preserved, not deleted — disabled by comment): label: string; +// ORIGINAL (preserved, not deleted — disabled by comment): strength: number; +// ORIGINAL (preserved, not deleted — disabled by comment): }; +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): const VIEW = { width: 900, height: 570 }; +// ORIGINAL (preserved, not deleted — disabled by comment): const INITIAL_AGENTS: AgentNode[] = [ +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): id: "codex", +// ORIGINAL (preserved, not deleted — disabled by comment): name: "Codex", +// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "OpenAI", +// ORIGINAL (preserved, not deleted — disabled by comment): model: "Codex · demo adapter", +// ORIGINAL (preserved, not deleted — disabled by comment): role: "Workflow coordinator", +// ORIGINAL (preserved, not deleted — disabled by comment): status: "active", +// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 4, +// ORIGINAL (preserved, not deleted — disabled by comment): latency: "42 ms", +// ORIGINAL (preserved, not deleted — disabled by comment): color: "mint", +// ORIGINAL (preserved, not deleted — disabled by comment): icon: "codex", +// ORIGINAL (preserved, not deleted — disabled by comment): kind: "agent", +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): id: "claude", +// ORIGINAL (preserved, not deleted — disabled by comment): name: "Claude Code", +// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "Anthropic", +// ORIGINAL (preserved, not deleted — disabled by comment): model: "Claude · demo adapter", +// ORIGINAL (preserved, not deleted — disabled by comment): role: "Code analysis", +// ORIGINAL (preserved, not deleted — disabled by comment): status: "active", +// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 2, +// ORIGINAL (preserved, not deleted — disabled by comment): latency: "58 ms", +// ORIGINAL (preserved, not deleted — disabled by comment): color: "peach", +// ORIGINAL (preserved, not deleted — disabled by comment): icon: "claude", +// ORIGINAL (preserved, not deleted — disabled by comment): kind: "agent", +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): id: "hermes", +// ORIGINAL (preserved, not deleted — disabled by comment): name: "Hermes Agent", +// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "Open source", +// ORIGINAL (preserved, not deleted — disabled by comment): model: "Hermes · demo adapter", +// ORIGINAL (preserved, not deleted — disabled by comment): role: "Task automation", +// ORIGINAL (preserved, not deleted — disabled by comment): status: "idle", +// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 0, +// ORIGINAL (preserved, not deleted — disabled by comment): latency: "—", +// ORIGINAL (preserved, not deleted — disabled by comment): color: "violet", +// ORIGINAL (preserved, not deleted — disabled by comment): icon: "hermes", +// ORIGINAL (preserved, not deleted — disabled by comment): kind: "agent", +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): id: "openclaw", +// ORIGINAL (preserved, not deleted — disabled by comment): name: "OpenClaw", +// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "Community", +// ORIGINAL (preserved, not deleted — disabled by comment): model: "OpenClaw · demo adapter", +// ORIGINAL (preserved, not deleted — disabled by comment): role: "Tool routing", +// ORIGINAL (preserved, not deleted — disabled by comment): status: "attention", +// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 1, +// ORIGINAL (preserved, not deleted — disabled by comment): latency: "91 ms", +// ORIGINAL (preserved, not deleted — disabled by comment): color: "blue", +// ORIGINAL (preserved, not deleted — disabled by comment): icon: "openclaw", +// ORIGINAL (preserved, not deleted — disabled by comment): kind: "agent", +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): id: "custom", +// ORIGINAL (preserved, not deleted — disabled by comment): name: "Custom adapter", +// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "Local example", +// ORIGINAL (preserved, not deleted — disabled by comment): model: "Compatible endpoint · demo", +// ORIGINAL (preserved, not deleted — disabled by comment): role: "Custom integration", +// ORIGINAL (preserved, not deleted — disabled by comment): status: "paused", +// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 0, +// ORIGINAL (preserved, not deleted — disabled by comment): latency: "—", +// ORIGINAL (preserved, not deleted — disabled by comment): color: "gold", +// ORIGINAL (preserved, not deleted — disabled by comment): icon: "custom", +// ORIGINAL (preserved, not deleted — disabled by comment): kind: "agent", +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): id: "model", +// ORIGINAL (preserved, not deleted — disabled by comment): name: "Reasoning model", +// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "MODEL", +// ORIGINAL (preserved, not deleted — disabled by comment): model: "Local demo model", +// ORIGINAL (preserved, not deleted — disabled by comment): role: "Language model", +// ORIGINAL (preserved, not deleted — disabled by comment): status: "active", +// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 3, +// ORIGINAL (preserved, not deleted — disabled by comment): latency: "36 ms", +// ORIGINAL (preserved, not deleted — disabled by comment): color: "violet", +// ORIGINAL (preserved, not deleted — disabled by comment): icon: "custom", +// ORIGINAL (preserved, not deleted — disabled by comment): kind: "model", +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): id: "tools", +// ORIGINAL (preserved, not deleted — disabled by comment): name: "Tool registry", +// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "TOOLS", +// ORIGINAL (preserved, not deleted — disabled by comment): model: "Local demo tools", +// ORIGINAL (preserved, not deleted — disabled by comment): role: "Tool collection", +// ORIGINAL (preserved, not deleted — disabled by comment): status: "idle", +// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 0, +// ORIGINAL (preserved, not deleted — disabled by comment): latency: "—", +// ORIGINAL (preserved, not deleted — disabled by comment): color: "blue", +// ORIGINAL (preserved, not deleted — disabled by comment): icon: "openclaw", +// ORIGINAL (preserved, not deleted — disabled by comment): kind: "tool", +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): id: "memory", +// ORIGINAL (preserved, not deleted — disabled by comment): name: "Shared context", +// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "MEMORY", +// ORIGINAL (preserved, not deleted — disabled by comment): model: "Session memory · demo", +// ORIGINAL (preserved, not deleted — disabled by comment): role: "Context store", +// ORIGINAL (preserved, not deleted — disabled by comment): status: "active", +// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 2, +// ORIGINAL (preserved, not deleted — disabled by comment): latency: "12 ms", +// ORIGINAL (preserved, not deleted — disabled by comment): color: "gold", +// ORIGINAL (preserved, not deleted — disabled by comment): icon: "hermes", +// ORIGINAL (preserved, not deleted — disabled by comment): kind: "memory", +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): id: "task", +// ORIGINAL (preserved, not deleted — disabled by comment): name: "Build review", +// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "TASK", +// ORIGINAL (preserved, not deleted — disabled by comment): model: "Sample job · demo", +// ORIGINAL (preserved, not deleted — disabled by comment): role: "Queued task", +// ORIGINAL (preserved, not deleted — disabled by comment): status: "idle", +// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 1, +// ORIGINAL (preserved, not deleted — disabled by comment): latency: "—", +// ORIGINAL (preserved, not deleted — disabled by comment): color: "peach", +// ORIGINAL (preserved, not deleted — disabled by comment): icon: "claude", +// ORIGINAL (preserved, not deleted — disabled by comment): kind: "task", +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): id: "service", +// ORIGINAL (preserved, not deleted — disabled by comment): name: "Git provider", +// ORIGINAL (preserved, not deleted — disabled by comment): vendor: "SERVICE", +// ORIGINAL (preserved, not deleted — disabled by comment): model: "External service · illustrative", +// ORIGINAL (preserved, not deleted — disabled by comment): role: "Integration endpoint", +// ORIGINAL (preserved, not deleted — disabled by comment): status: "paused", +// ORIGINAL (preserved, not deleted — disabled by comment): tasks: 0, +// ORIGINAL (preserved, not deleted — disabled by comment): latency: "—", +// ORIGINAL (preserved, not deleted — disabled by comment): color: "blue", +// ORIGINAL (preserved, not deleted — disabled by comment): icon: "custom", +// ORIGINAL (preserved, not deleted — disabled by comment): kind: "service", +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): ]; +// ORIGINAL (preserved, not deleted — disabled by comment): const INITIAL_EDGES: AgentEdge[] = [ +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): source: "codex", +// ORIGINAL (preserved, not deleted — disabled by comment): target: "claude", +// ORIGINAL (preserved, not deleted — disabled by comment): label: "delegates review", +// ORIGINAL (preserved, not deleted — disabled by comment): strength: 0.82, +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): { source: "codex", target: "hermes", label: "routes tasks", strength: 0.62 }, +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): source: "codex", +// ORIGINAL (preserved, not deleted — disabled by comment): target: "openclaw", +// ORIGINAL (preserved, not deleted — disabled by comment): label: "shares context", +// ORIGINAL (preserved, not deleted — disabled by comment): strength: 0.46, +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): { source: "claude", target: "custom", label: "handoff", strength: 0.34 }, +// ORIGINAL (preserved, not deleted — disabled by comment): { source: "hermes", target: "custom", label: "tool access", strength: 0.3 }, +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): source: "openclaw", +// ORIGINAL (preserved, not deleted — disabled by comment): target: "hermes", +// ORIGINAL (preserved, not deleted — disabled by comment): label: "event bridge", +// ORIGINAL (preserved, not deleted — disabled by comment): strength: 0.56, +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): { source: "codex", target: "model", label: "uses model", strength: 0.66 }, +// ORIGINAL (preserved, not deleted — disabled by comment): { source: "claude", target: "tools", label: "uses tools", strength: 0.42 }, +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): source: "hermes", +// ORIGINAL (preserved, not deleted — disabled by comment): target: "memory", +// ORIGINAL (preserved, not deleted — disabled by comment): label: "reads context", +// ORIGINAL (preserved, not deleted — disabled by comment): strength: 0.52, +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): { source: "codex", target: "task", label: "runs task", strength: 0.72 }, +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): source: "task", +// ORIGINAL (preserved, not deleted — disabled by comment): target: "service", +// ORIGINAL (preserved, not deleted — disabled by comment): label: "requests change", +// ORIGINAL (preserved, not deleted — disabled by comment): strength: 0.38, +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): ]; +// ORIGINAL (preserved, not deleted — disabled by comment): // ORIGINAL (preserved, not deleted — disabled by comment): const edges = INITIAL_EDGES; +// ORIGINAL (preserved, not deleted — disabled by comment): const NETWORK_POSITIONS: Record = { +// ORIGINAL (preserved, not deleted — disabled by comment): codex: { x: 250, y: 280 }, +// ORIGINAL (preserved, not deleted — disabled by comment): claude: { x: 570, y: 145 }, +// ORIGINAL (preserved, not deleted — disabled by comment): hermes: { x: 675, y: 310 }, +// ORIGINAL (preserved, not deleted — disabled by comment): openclaw: { x: 515, y: 445 }, +// ORIGINAL (preserved, not deleted — disabled by comment): custom: { x: 225, y: 460 }, +// ORIGINAL (preserved, not deleted — disabled by comment): model: { x: 110, y: 155 }, +// ORIGINAL (preserved, not deleted — disabled by comment): tools: { x: 775, y: 165 }, +// ORIGINAL (preserved, not deleted — disabled by comment): memory: { x: 800, y: 480 }, +// ORIGINAL (preserved, not deleted — disabled by comment): task: { x: 345, y: 510 }, +// ORIGINAL (preserved, not deleted — disabled by comment): service: { x: 110, y: 390 }, +// ORIGINAL (preserved, not deleted — disabled by comment): }; +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): function positionsFor(mode: GraphMode): Record { +// ORIGINAL (preserved, not deleted — disabled by comment): if (mode === "network") return NETWORK_POSITIONS; +// ORIGINAL (preserved, not deleted — disabled by comment): const center = { x: VIEW.width / 2, y: VIEW.height / 2 }; +// ORIGINAL (preserved, not deleted — disabled by comment): const radiusX = 255; +// ORIGINAL (preserved, not deleted — disabled by comment): const radiusY = 190; +// ORIGINAL (preserved, not deleted — disabled by comment): return Object.fromEntries( +// ORIGINAL (preserved, not deleted — disabled by comment): INITIAL_AGENTS.map((agent) => { +// ORIGINAL (preserved, not deleted — disabled by comment): if (agent.id === "codex") return [agent.id, center]; +// ORIGINAL (preserved, not deleted — disabled by comment): const peers = INITIAL_AGENTS.filter((item) => item.id !== "codex"); +// ORIGINAL (preserved, not deleted — disabled by comment): const spoke = peers.findIndex((item) => item.id === agent.id); +// ORIGINAL (preserved, not deleted — disabled by comment): const angle = -Math.PI / 2 + (spoke * (Math.PI * 2)) / peers.length; +// ORIGINAL (preserved, not deleted — disabled by comment): return [ +// ORIGINAL (preserved, not deleted — disabled by comment): agent.id, +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): x: center.x + Math.cos(angle) * radiusX, +// ORIGINAL (preserved, not deleted — disabled by comment): y: center.y + Math.sin(angle) * radiusY, +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): ]; +// ORIGINAL (preserved, not deleted — disabled by comment): }), +// ORIGINAL (preserved, not deleted — disabled by comment): ); +// ORIGINAL (preserved, not deleted — disabled by comment): } +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): function initialLayout(): AgentGraphLayout { +// ORIGINAL (preserved, not deleted — disabled by comment): return { +// ORIGINAL (preserved, not deleted — disabled by comment): positions: { network: NETWORK_POSITIONS, radial: positionsFor("radial") }, +// ORIGINAL (preserved, not deleted — disabled by comment): cameras: { +// ORIGINAL (preserved, not deleted — disabled by comment): network: { x: 0, y: 0, zoom: 1 }, +// ORIGINAL (preserved, not deleted — disabled by comment): radial: { x: 0, y: 0, zoom: 1 }, +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): }; +// ORIGINAL (preserved, not deleted — disabled by comment): } +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): function AgentGlyph({ icon }: { icon: AgentNode["icon"] }) { +// ORIGINAL (preserved, not deleted — disabled by comment): if (icon === "codex") return ; +// ORIGINAL (preserved, not deleted — disabled by comment): if (icon === "claude") return ; +// ORIGINAL (preserved, not deleted — disabled by comment): if (icon === "hermes") return ; +// ORIGINAL (preserved, not deleted — disabled by comment): if (icon === "openclaw") return ; +// ORIGINAL (preserved, not deleted — disabled by comment): return ; +// ORIGINAL (preserved, not deleted — disabled by comment): } +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): function edgePath(a: Point, b: Point) { +// ORIGINAL (preserved, not deleted — disabled by comment): const middleX = (a.x + b.x) / 2; +// ORIGINAL (preserved, not deleted — disabled by comment): const middleY = (a.y + b.y) / 2; +// ORIGINAL (preserved, not deleted — disabled by comment): const curve = (b.x - a.x) * 0.08; +// ORIGINAL (preserved, not deleted — disabled by comment): return `M ${a.x} ${a.y} Q ${middleX - curve} ${middleY + curve} ${b.x} ${b.y}`; +// ORIGINAL (preserved, not deleted — disabled by comment): } +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): export function AgentsPage() { +// ORIGINAL (preserved, not deleted — disabled by comment): const [agents, setAgents] = useState(INITIAL_AGENTS); +// ORIGINAL (preserved, not deleted — disabled by comment): const [layout, setLayout] = useState(initialLayout); +// ORIGINAL (preserved, not deleted — disabled by comment): const layoutEdited = useRef(false); +// ORIGINAL (preserved, not deleted — disabled by comment): const [layoutLoaded, setLayoutLoaded] = useState(false); +// ORIGINAL (preserved, not deleted — disabled by comment): const [mode, setMode] = useState("network"); +// ORIGINAL (preserved, not deleted — disabled by comment): const positions = layout.positions[mode]; +// ORIGINAL (preserved, not deleted — disabled by comment): const [selectedId, setSelectedId] = useState("codex"); +// ORIGINAL (preserved, not deleted — disabled by comment): const [zoom, setZoom] = useState(1); +// ORIGINAL (preserved, not deleted — disabled by comment): const [pan, setPan] = useState({ x: 0, y: 0 }); +// ORIGINAL (preserved, not deleted — disabled by comment): const [live, setLive] = useState(true); +// ORIGINAL (preserved, not deleted — disabled by comment): const svgRef = useRef(null); +// ORIGINAL (preserved, not deleted — disabled by comment): const gesture = useRef< +// ORIGINAL (preserved, not deleted — disabled by comment): | { type: "node"; id: string; pointer: Point } +// ORIGINAL (preserved, not deleted — disabled by comment): | { type: "pan"; pointer: Point; pan: Point } +// ORIGINAL (preserved, not deleted — disabled by comment): | null +// ORIGINAL (preserved, not deleted — disabled by comment): >(null); +// ORIGINAL (preserved, not deleted — disabled by comment): const selected = agents.find((agent) => agent.id === selectedId) ?? agents[0]; +// ORIGINAL (preserved, not deleted — disabled by comment): const selectedEdges = INITIAL_EDGES.filter( +// ORIGINAL (preserved, not deleted — disabled by comment): (edge) => edge.source === selected.id || edge.target === selected.id, +// ORIGINAL (preserved, not deleted — disabled by comment): ); +// ORIGINAL (preserved, not deleted — disabled by comment): useEffect(() => { +// ORIGINAL (preserved, not deleted — disabled by comment): let active = true; +// ORIGINAL (preserved, not deleted — disabled by comment): void readAgentGraphLayout(initialLayout()).then((saved) => { +// ORIGINAL (preserved, not deleted — disabled by comment): if (!active) return; +// ORIGINAL (preserved, not deleted — disabled by comment): if (!layoutEdited.current) setLayout(saved); +// ORIGINAL (preserved, not deleted — disabled by comment): setLayoutLoaded(true); +// ORIGINAL (preserved, not deleted — disabled by comment): }); +// ORIGINAL (preserved, not deleted — disabled by comment): return () => { +// ORIGINAL (preserved, not deleted — disabled by comment): active = false; +// ORIGINAL (preserved, not deleted — disabled by comment): }; +// ORIGINAL (preserved, not deleted — disabled by comment): }, []); +// ORIGINAL (preserved, not deleted — disabled by comment): useEffect(() => { +// ORIGINAL (preserved, not deleted — disabled by comment): if (!layoutLoaded) return; +// ORIGINAL (preserved, not deleted — disabled by comment): const timeout = window.setTimeout( +// ORIGINAL (preserved, not deleted — disabled by comment): () => void writeAgentGraphLayout(layout), +// ORIGINAL (preserved, not deleted — disabled by comment): 250, +// ORIGINAL (preserved, not deleted — disabled by comment): ); +// ORIGINAL (preserved, not deleted — disabled by comment): return () => window.clearTimeout(timeout); +// ORIGINAL (preserved, not deleted — disabled by comment): }, [layout, layoutLoaded]); +// ORIGINAL (preserved, not deleted — disabled by comment): useEffect(() => { +// ORIGINAL (preserved, not deleted — disabled by comment): setZoom(layout.cameras[mode].zoom); +// ORIGINAL (preserved, not deleted — disabled by comment): setPan({ x: layout.cameras[mode].x, y: layout.cameras[mode].y }); +// ORIGINAL (preserved, not deleted — disabled by comment): }, [layout.cameras, mode]); +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): const updateCamera = (next: Partial & { zoom?: number }) => { +// ORIGINAL (preserved, not deleted — disabled by comment): layoutEdited.current = true; +// ORIGINAL (preserved, not deleted — disabled by comment): setLayout((value) => ({ +// ORIGINAL (preserved, not deleted — disabled by comment): ...value, +// ORIGINAL (preserved, not deleted — disabled by comment): cameras: { +// ORIGINAL (preserved, not deleted — disabled by comment): ...value.cameras, +// ORIGINAL (preserved, not deleted — disabled by comment): [mode]: { ...value.cameras[mode], ...next }, +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): })); +// ORIGINAL (preserved, not deleted — disabled by comment): }; +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): const pointerPoint = useCallback( +// ORIGINAL (preserved, not deleted — disabled by comment): ( +// ORIGINAL (preserved, not deleted — disabled by comment): event: ReactPointerEvent | ReactWheelEvent, +// ORIGINAL (preserved, not deleted — disabled by comment): ) => { +// ORIGINAL (preserved, not deleted — disabled by comment): const svg = svgRef.current; +// ORIGINAL (preserved, not deleted — disabled by comment): if (!svg) return { x: 0, y: 0 }; +// ORIGINAL (preserved, not deleted — disabled by comment): const transform = svg.getScreenCTM()?.inverse(); +// ORIGINAL (preserved, not deleted — disabled by comment): if (!transform) return { x: 0, y: 0 }; +// ORIGINAL (preserved, not deleted — disabled by comment): const point = svg.createSVGPoint(); +// ORIGINAL (preserved, not deleted — disabled by comment): point.x = event.clientX; +// ORIGINAL (preserved, not deleted — disabled by comment): point.y = event.clientY; +// ORIGINAL (preserved, not deleted — disabled by comment): const local = point.matrixTransform(transform); +// ORIGINAL (preserved, not deleted — disabled by comment): return { x: local.x, y: local.y }; +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): [], +// ORIGINAL (preserved, not deleted — disabled by comment): ); +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): const onPointerDown = (event: ReactPointerEvent) => { +// ORIGINAL (preserved, not deleted — disabled by comment): if (event.button !== 0) return; +// ORIGINAL (preserved, not deleted — disabled by comment): const target = (event.target as Element).closest( +// ORIGINAL (preserved, not deleted — disabled by comment): "[data-agent-id]", +// ORIGINAL (preserved, not deleted — disabled by comment): ); +// ORIGINAL (preserved, not deleted — disabled by comment): const pointer = pointerPoint(event); +// ORIGINAL (preserved, not deleted — disabled by comment): if (target?.dataset.agentId) { +// ORIGINAL (preserved, not deleted — disabled by comment): const id = target.dataset.agentId; +// ORIGINAL (preserved, not deleted — disabled by comment): setSelectedId(id); +// ORIGINAL (preserved, not deleted — disabled by comment): gesture.current = { type: "node", id, pointer }; +// ORIGINAL (preserved, not deleted — disabled by comment): } else { +// ORIGINAL (preserved, not deleted — disabled by comment): gesture.current = { type: "pan", pointer, pan }; +// ORIGINAL (preserved, not deleted — disabled by comment): } +// ORIGINAL (preserved, not deleted — disabled by comment): event.currentTarget.setPointerCapture(event.pointerId); +// ORIGINAL (preserved, not deleted — disabled by comment): }; +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): const onPointerMove = (event: ReactPointerEvent) => { +// ORIGINAL (preserved, not deleted — disabled by comment): const active = gesture.current; +// ORIGINAL (preserved, not deleted — disabled by comment): if (!active) return; +// ORIGINAL (preserved, not deleted — disabled by comment): const current = pointerPoint(event); +// ORIGINAL (preserved, not deleted — disabled by comment): const delta = { +// ORIGINAL (preserved, not deleted — disabled by comment): x: current.x - active.pointer.x, +// ORIGINAL (preserved, not deleted — disabled by comment): y: current.y - active.pointer.y, +// ORIGINAL (preserved, not deleted — disabled by comment): }; +// ORIGINAL (preserved, not deleted — disabled by comment): if (active.type === "node") { +// ORIGINAL (preserved, not deleted — disabled by comment): layoutEdited.current = true; +// ORIGINAL (preserved, not deleted — disabled by comment): setLayout((previous) => { +// ORIGINAL (preserved, not deleted — disabled by comment): const original = previous.positions[mode][active.id]; +// ORIGINAL (preserved, not deleted — disabled by comment): return { +// ORIGINAL (preserved, not deleted — disabled by comment): ...previous, +// ORIGINAL (preserved, not deleted — disabled by comment): positions: { +// ORIGINAL (preserved, not deleted — disabled by comment): ...previous.positions, +// ORIGINAL (preserved, not deleted — disabled by comment): [mode]: { +// ORIGINAL (preserved, not deleted — disabled by comment): ...previous.positions[mode], +// ORIGINAL (preserved, not deleted — disabled by comment): [active.id]: { +// ORIGINAL (preserved, not deleted — disabled by comment): x: original.x + delta.x / zoom, +// ORIGINAL (preserved, not deleted — disabled by comment): y: original.y + delta.y / zoom, +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): }, +// ORIGINAL (preserved, not deleted — disabled by comment): }; +// ORIGINAL (preserved, not deleted — disabled by comment): }); +// ORIGINAL (preserved, not deleted — disabled by comment): active.pointer = current; +// ORIGINAL (preserved, not deleted — disabled by comment): } else { +// ORIGINAL (preserved, not deleted — disabled by comment): const next = { x: active.pan.x + delta.x, y: active.pan.y + delta.y }; +// ORIGINAL (preserved, not deleted — disabled by comment): setPan(next); +// ORIGINAL (preserved, not deleted — disabled by comment): updateCamera(next); +// ORIGINAL (preserved, not deleted — disabled by comment): } +// ORIGINAL (preserved, not deleted — disabled by comment): }; +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): const onWheel = (event: ReactWheelEvent) => { +// ORIGINAL (preserved, not deleted — disabled by comment): event.preventDefault(); +// ORIGINAL (preserved, not deleted — disabled by comment): setZoom((current) => { +// ORIGINAL (preserved, not deleted — disabled by comment): const next = Math.max( +// ORIGINAL (preserved, not deleted — disabled by comment): 0.65, +// ORIGINAL (preserved, not deleted — disabled by comment): Math.min(1.8, current * (event.deltaY < 0 ? 1.08 : 0.92)), +// ORIGINAL (preserved, not deleted — disabled by comment): ); +// ORIGINAL (preserved, not deleted — disabled by comment): updateCamera({ zoom: next }); +// ORIGINAL (preserved, not deleted — disabled by comment): return next; +// ORIGINAL (preserved, not deleted — disabled by comment): }); +// ORIGINAL (preserved, not deleted — disabled by comment): }; +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): /* ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): const onGraphKeyDown = (event: ReactKeyboardEvent) => { +// ORIGINAL (preserved, not deleted — disabled by comment): const step = event.shiftKey ? 40 : 18; +// ORIGINAL (preserved, not deleted — disabled by comment): if (event.key === "ArrowLeft") +// ORIGINAL (preserved, not deleted — disabled by comment): setPan((value) => ({ ...value, x: value.x + step })); +// ORIGINAL (preserved, not deleted — disabled by comment): else if (event.key === "ArrowRight") +// ORIGINAL (preserved, not deleted — disabled by comment): setPan((value) => ({ ...value, x: value.x - step })); +// ORIGINAL (preserved, not deleted — disabled by comment): else if (event.key === "ArrowUp") +// ORIGINAL (preserved, not deleted — disabled by comment): setPan((value) => ({ ...value, y: value.y + step })); +// ORIGINAL (preserved, not deleted — disabled by comment): else if (event.key === "ArrowDown") +// ORIGINAL (preserved, not deleted — disabled by comment): setPan((value) => ({ ...value, y: value.y - step })); +// ORIGINAL (preserved, not deleted — disabled by comment): else if (event.key === "+" || event.key === "=") +// ORIGINAL (preserved, not deleted — disabled by comment): setZoom((value) => Math.min(1.8, value + 0.1)); +// ORIGINAL (preserved, not deleted — disabled by comment): else if (event.key === "-") setZoom((value) => Math.max(0.65, value - 0.1)); +// ORIGINAL (preserved, not deleted — disabled by comment): else return; +// ORIGINAL (preserved, not deleted — disabled by comment): event.preventDefault(); +// ORIGINAL (preserved, not deleted — disabled by comment): }; +// ORIGINAL (preserved, not deleted — disabled by comment): */ +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): const setModeAndLayout = (nextMode: GraphMode) => { +// ORIGINAL (preserved, not deleted — disabled by comment): setMode(nextMode); +// ORIGINAL (preserved, not deleted — disabled by comment): setPan({ x: 0, y: 0 }); +// ORIGINAL (preserved, not deleted — disabled by comment): }; +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): const toggleAgentStatus = () => { +// ORIGINAL (preserved, not deleted — disabled by comment): setAgents((current) => +// ORIGINAL (preserved, not deleted — disabled by comment): current.map((agent) => +// ORIGINAL (preserved, not deleted — disabled by comment): agent.id === selected.id +// ORIGINAL (preserved, not deleted — disabled by comment): ? { ...agent, status: agent.status === "paused" ? "idle" : "paused" } +// ORIGINAL (preserved, not deleted — disabled by comment): : agent, +// ORIGINAL (preserved, not deleted — disabled by comment): ), +// ORIGINAL (preserved, not deleted — disabled by comment): ); +// ORIGINAL (preserved, not deleted — disabled by comment): }; +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): return ( +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): DEMO GRAPH +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): } +// ORIGINAL (preserved, not deleted — disabled by comment): /> +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Agent network +// ORIGINAL (preserved, not deleted — disabled by comment): Local sample topology · no external connections +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): ADAPTERS +// ORIGINAL (preserved, not deleted — disabled by comment): {agents.length.toString().padStart(2, "0")} +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): DEMO LINKS +// ORIGINAL (preserved, not deleted — disabled by comment): {INITIAL_EDGES.length.toString().padStart(2, "0")} +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): ACTIVE TASKS +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): {agents +// ORIGINAL (preserved, not deleted — disabled by comment): .reduce((sum, agent) => sum + agent.tasks, 0) +// ORIGINAL (preserved, not deleted — disabled by comment): .toString() +// ORIGINAL (preserved, not deleted — disabled by comment): .padStart(2, "0")} +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): Local demo data +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Connection map +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): Drag nodes or canvas to explore +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): gesture.current = null; +// ORIGINAL (preserved, not deleted — disabled by comment): }} +// ORIGINAL (preserved, not deleted — disabled by comment): onPointerCancel={() => { +// ORIGINAL (preserved, not deleted — disabled by comment): gesture.current = null; +// ORIGINAL (preserved, not deleted — disabled by comment): }} +// ORIGINAL (preserved, not deleted — disabled by comment): onWheel={onWheel} +// ORIGINAL (preserved, not deleted — disabled by comment): > +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): Demo AI agent relationship map +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): {INITIAL_EDGES.map((edge) => { +// ORIGINAL (preserved, not deleted — disabled by comment): const from = positions[edge.source]; +// ORIGINAL (preserved, not deleted — disabled by comment): const to = positions[edge.target]; +// ORIGINAL (preserved, not deleted — disabled by comment): const highlighted = +// ORIGINAL (preserved, not deleted — disabled by comment): edge.source === selected.id || +// ORIGINAL (preserved, not deleted — disabled by comment): edge.target === selected.id; +// ORIGINAL (preserved, not deleted — disabled by comment): const labelX = (from.x + to.x) / 2; +// ORIGINAL (preserved, not deleted — disabled by comment): const labelY = (from.y + to.y) / 2; +// ORIGINAL (preserved, not deleted — disabled by comment): return ( +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): {highlighted && ( +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): {edge.label} +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): )} +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): ); +// ORIGINAL (preserved, not deleted — disabled by comment): })} +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): {agents.map((agent) => { +// ORIGINAL (preserved, not deleted — disabled by comment): const point = positions[agent.id]; +// ORIGINAL (preserved, not deleted — disabled by comment): const isSelected = agent.id === selectedId; +// ORIGINAL (preserved, not deleted — disabled by comment): return ( +// ORIGINAL (preserved, not deleted — disabled by comment): /* biome-ignore lint/a11y/useSemanticElements: SVG node geometry needs a keyboard-accessible button role. */ +// ORIGINAL (preserved, not deleted — disabled by comment): { +// ORIGINAL (preserved, not deleted — disabled by comment): event.stopPropagation(); +// ORIGINAL (preserved, not deleted — disabled by comment): setSelectedId(agent.id); +// ORIGINAL (preserved, not deleted — disabled by comment): }} +// ORIGINAL (preserved, not deleted — disabled by comment): onKeyDown={(event) => { +// ORIGINAL (preserved, not deleted — disabled by comment): if (event.key === "Enter" || event.key === " ") { +// ORIGINAL (preserved, not deleted — disabled by comment): event.preventDefault(); +// ORIGINAL (preserved, not deleted — disabled by comment): setSelectedId(agent.id); +// ORIGINAL (preserved, not deleted — disabled by comment): } +// ORIGINAL (preserved, not deleted — disabled by comment): }} +// ORIGINAL (preserved, not deleted — disabled by comment): > +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): {agent.kind.toUpperCase()} +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): {agent.name} +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): {agent.tasks} TASK{agent.tasks === 1 ? "" : "S"} ·{" "} +// ORIGINAL (preserved, not deleted — disabled by comment): {agent.status.toUpperCase()} +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): ); +// ORIGINAL (preserved, not deleted — disabled by comment): })} +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): DRAG TO REPOSITION · WHEEL TO ZOOM · SELECT A NODE FOR DETAILS +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Canvas controls +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): {Math.round(zoom * 100)}% +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): Active +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): Idle +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): Needs attention +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): Lines show illustrative adapter relationships +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): Sample graph data only {" "} +// ORIGINAL (preserved, not deleted — disabled by comment): +// ORIGINAL (preserved, not deleted — disabled by comment): Decorative connection motion can be switched off. +// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment):
+// ORIGINAL (preserved, not deleted — disabled by comment): ); +// ORIGINAL (preserved, not deleted — disabled by comment): } diff --git a/dashboard/src/pages/Automation.css b/dashboard/src/pages/Automation.css new file mode 100644 index 0000000..e8d35fa --- /dev/null +++ b/dashboard/src/pages/Automation.css @@ -0,0 +1,739 @@ +.automation-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 276px; + gap: 14px; + align-items: stretch; + margin-bottom: 16px; +} +.automation-workspace, +.automation-inspector { + min-width: 0; + border: 1px solid var(--border); + border-radius: 10px; + background: linear-gradient( + 145deg, + rgba(15, 25, 32, 0.96), + rgba(10, 17, 23, 0.96) + ); + overflow: hidden; +} +.automation-toolbar { + height: 65px; + padding: 0 17px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--border); +} +.automation-workflow-label, +.automation-toolbar-actions, +.automation-inspector-head, +.automation-section-head, +.automation-runbar, +.automation-path-item, +.preview-result, +.inspector-type, +.inspector-footer { + display: flex; + align-items: center; +} +.automation-workflow-label { + gap: 11px; +} +.automation-flow-icon { + width: 31px; + height: 31px; + display: grid; + place-items: center; + border: 1px solid #285343; + border-radius: 8px; + color: #78d9ae; + background: #12271f; +} +.automation-workflow-label span:last-child, +.inspector-type > div, +.automation-path-item > span:nth-child(2), +.preview-result > div:last-child { + display: grid; + gap: 4px; +} +.automation-workflow-label strong, +.automation-path-item strong, +.preview-result strong { + font-size: 11px; + font-weight: 600; +} +.automation-name { + width: 190px; + border: 0; + padding: 0; + outline: 0; + background: transparent; + color: var(--text); + font: 600 11px inherit; +} +.automation-name:focus-visible { + outline: 1px solid #6bc8a1; + outline-offset: 3px; +} +.automation-palette { + display: flex; + gap: 6px; + overflow-x: auto; + padding: 8px 10px; + border-bottom: 1px solid var(--border); + scrollbar-width: thin; +} +.automation-palette button { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 5px; + border: 1px solid var(--border-light); + border-radius: 5px; + padding: 5px 7px; + background: #111b23; + color: #9aabb2; + font-size: 9px; + text-transform: capitalize; + cursor: pointer; +} +.automation-palette button:hover { + color: #bcebd7; + border-color: #3b6655; +} +.automation-reactflow { + height: 450px; + background: + radial-gradient(ellipse at 50% 42%, rgba(24, 65, 60, 0.14), transparent 61%), + #0a1117; +} +.automation-reactflow .react-flow__controls, +.automation-reactflow .react-flow__minimap { + overflow: hidden; + border: 1px solid #2a3942; + border-radius: 7px; + background: #10191fdd; +} +.automation-reactflow .react-flow__controls-button { + background: #10191f; + fill: #91a1a8; + border-color: #2a3942; +} +.automation-reactflow .react-flow__node-default { + width: 188px; + border: 1px solid #35504e; + border-radius: 8px; + padding: 14px; + color: var(--text); + background: linear-gradient(150deg, #17232b, #101920); + box-shadow: 0 8px 24px #0005; +} +.automation-reactflow .react-flow__node-workflow { + width: 190px; + border: 1px solid #35504e; + border-radius: 8px; + color: var(--text); + background: linear-gradient(150deg, #17232b, #101920); + box-shadow: 0 8px 24px #0005; +} +.flow-card { + display: grid; + grid-template-columns: 18px 1fr; + gap: 5px 8px; + align-items: center; + padding: 12px; +} +.flow-card > span { + grid-row: span 2; + color: #7fd1ad; +} +.flow-card strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 10px; +} +.flow-card small { + color: #82939a; + font-size: 8px; + text-transform: uppercase; +} +.automation-reactflow .react-flow__node-workflow.selected { + border-color: #58b38b; + box-shadow: + 0 0 0 1px #58b38b55, + 0 9px 26px #0008; +} +.automation-reactflow .react-flow__node-default.selected { + border-color: #58b38b; + box-shadow: + 0 0 0 1px #58b38b55, + 0 9px 26px #0008; +} +.automation-reactflow .react-flow__handle { + width: 8px; + height: 8px; + border: 1px solid #12231d; + background: #6dd7a8; +} +.automation-reactflow .react-flow__edge-path { + stroke: #68b896; + stroke-width: 1.8; +} +.automation-reactflow .react-flow__edge-text { + fill: #9bb0aa; + font-size: 10px; +} +.automation-issues { + color: #e4b56b; + font-size: 10px; + line-height: 1.5; +} +.automation-issues p { + margin: 8px 0; +} +.automation-workflow-label small, +.automation-path-item small, +.preview-result small { + font-size: 9px; + color: var(--muted); +} +.automation-toolbar-actions { + gap: 8px; +} +.demo-pill, +.availability-tag { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 7px; + border: 1px solid #2b473e; + border-radius: 5px; + color: #8ed7b6; + background: #12221d; + font: 500 8px var(--mono); + letter-spacing: 0.5px; +} +.demo-pill > span, +.run-indicator > span { + width: 5px; + height: 5px; + border-radius: 50%; + background: #65d5a4; + box-shadow: 0 0 8px #65d5a488; +} +.automation-tool-button { + width: 30px; + height: 30px; + display: grid; + place-items: center; + border: 1px solid var(--border-light); + border-radius: 6px; + background: #111b23; + color: #8d9da8; +} +.automation-tool-button:hover, +.automation-canvas-controls button:hover { + color: #bcebd7; + border-color: #3b6655; +} +.automation-stage { + position: relative; + height: 430px; + overflow: hidden; + background: + radial-gradient(ellipse at 50% 42%, rgba(24, 65, 60, 0.14), transparent 61%), + #0a1117; + touch-action: none; + outline: none; +} +.automation-stage:focus-visible { + box-shadow: inset 0 0 0 2px #66b9eb; +} +.automation-grid { + position: absolute; + inset: 0; + opacity: 0.54; + background-image: radial-gradient(#33424a 1px, transparent 1px); + background-size: 20px 20px; + pointer-events: none; +} +.automation-world { + position: absolute; + left: 0; + top: 0; + transform-origin: top left; + will-change: transform; +} +.automation-edges { + position: absolute; + inset: 0; + overflow: visible; + color: #568b78; + pointer-events: none; +} +.automation-edge path { + fill: none; + stroke: currentColor; + stroke-width: 1.5; + opacity: 0.72; +} +.automation-edge.is-branch path { + stroke: #b79963; + stroke-dasharray: 4 4; + color: #c59e5e; +} +.automation-edge text { + fill: #82949a; + font: 9px var(--mono); + paint-order: stroke; + stroke: #0a1117; + stroke-width: 5px; + stroke-linejoin: round; +} +.automation-step { + position: absolute; + z-index: 1; + display: flex; + align-items: center; + gap: 10px; + padding: 12px; + text-align: left; + border: 1px solid #2c3d46; + border-radius: 8px; + color: var(--text); + background: linear-gradient(150deg, #17232b, #101920); + box-shadow: 0 8px 24px #0005; + cursor: grab; + touch-action: none; +} +.automation-step:active { + cursor: grabbing; +} +.automation-step:hover { + border-color: #537566; + box-shadow: + 0 8px 28px #0007, + 0 0 0 1px #36594944; +} +.automation-step:focus-visible { + outline: 2px solid #6bc8a1; + outline-offset: 2px; +} +.automation-step.is-selected { + border-color: #58b38b; + box-shadow: + 0 0 0 1px #58b38b55, + 0 9px 26px #0008; +} +.automation-step-icon, +.inspector-icon, +.path-icon { + width: 32px; + height: 32px; + flex: 0 0 32px; + display: grid; + place-items: center; + border-radius: 7px; +} +.step-trigger.automation-step-icon, +.step-trigger.inspector-icon, +.step-trigger.path-icon { + background: #122f2b; + color: #66d8b0; +} +.step-action.automation-step-icon, +.step-action.inspector-icon, +.step-action.path-icon { + background: #1d2d3d; + color: #70b7eb; +} +.step-condition.automation-step-icon, +.step-condition.inspector-icon, +.step-condition.path-icon { + background: #322b1d; + color: #e4b56b; +} +.step-agent.automation-step-icon, +.step-agent.inspector-icon, +.step-agent.path-icon { + background: #29223d; + color: #baa3f1; +} +.automation-step-copy { + display: grid; + gap: 5px; + min-width: 0; +} +.automation-step-copy strong { + font-size: 10px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.automation-step-copy small { + font-size: 9px; + color: #80909a; + white-space: nowrap; +} +.automation-step-state { + margin-left: auto; + color: #61737b; +} +.status-running .automation-step-state { + color: #e5b56c; +} +.status-complete .automation-step-state { + color: #6dd7a8; +} +.automation-pulse { + display: block; + width: 8px; + height: 8px; + border-radius: 50%; + background: #edbd6d; + box-shadow: 0 0 0 4px #edbd6d24; + animation: automation-pulse 1s ease-in-out infinite; +} +@keyframes automation-pulse { + 50% { + box-shadow: 0 0 0 7px #edbd6d08; + } +} +.automation-canvas-controls { + position: absolute; + right: 12px; + bottom: 12px; + z-index: 2; + display: flex; + align-items: center; + gap: 7px; + padding: 5px; + border: 1px solid #2a3942; + border-radius: 7px; + background: #10191fdd; + backdrop-filter: blur(10px); +} +.automation-canvas-controls button { + width: 25px; + height: 25px; + display: grid; + place-items: center; + border: 0; + border-radius: 4px; + background: transparent; + color: #91a1a8; +} +.automation-canvas-controls span { + min-width: 37px; + text-align: center; + color: #9caab0; + font: 9px var(--mono); +} +.automation-canvas-hint { + position: absolute; + left: 12px; + bottom: 17px; + display: flex; + align-items: center; + gap: 5px; + color: #70818a; + font-size: 9px; + pointer-events: none; +} +.automation-runbar { + height: 37px; + gap: 8px; + padding: 0 14px; + border-top: 1px solid var(--border); + font-size: 9px; + color: #8a999f; +} +.run-indicator { + display: grid; + place-items: center; + width: 9px; + height: 9px; + border-radius: 50%; + border: 1px solid #47705f; +} +.run-indicator > span { + width: 3px; + height: 3px; +} +.run-indicator.is-running > span { + animation: automation-pulse 1s infinite; +} +.automation-runbar small { + margin-left: auto; + color: #667780; + font: 8px var(--mono); + letter-spacing: 0.6px; +} +.automation-inspector { + padding: 17px 15px; + display: flex; + flex-direction: column; +} +.automation-inspector-head, +.automation-section-head { + justify-content: space-between; +} +.automation-inspector-head h2, +.automation-section-head h2 { + margin: 6px 0 0; + font-size: 14px; + letter-spacing: -0.25px; +} +.inspector-close { + width: 25px; + height: 25px; +} +.inspector-type { + gap: 9px; + padding: 17px 0 16px; + margin: 12px 0 14px; + border-bottom: 1px solid var(--border); +} +.inspector-type strong { + text-transform: capitalize; + font-size: 10px; +} +.inspector-type small { + font-size: 9px; + color: var(--muted); +} +.inspector-status { + margin-left: auto; + padding: 4px 6px; + border: 1px solid #293940; + border-radius: 4px; + color: #87949a; + font: 8px var(--mono); + text-transform: uppercase; +} +.inspector-status.status-complete { + color: #77d8ab; + border-color: #315d4c; +} +.inspector-status.status-running { + color: #e7c17e; + border-color: #66502c; +} +.inspector-field { + display: grid; + gap: 7px; + margin: 0 0 14px; + color: #82929b; + font-size: 9px; +} +.inspector-field input, +.inspector-field textarea, +.inspector-field select { + width: 100%; + padding: 9px; + border: 1px solid #293840; + border-radius: 6px; + background: #0b1319; + color: #dce5eb; + font-size: 10px; + resize: vertical; +} +.threshold-input { + position: relative; +} +.threshold-input span { + position: absolute; + right: 10px; + top: 9px; + color: #73828a; + font: 10px var(--mono); +} +.inspector-note { + display: flex; + gap: 8px; + padding: 10px; + border: 1px solid #2b3c35; + border-radius: 6px; + background: #11201b; + color: #7bb596; + font-size: 9px; + line-height: 1.5; +} +.inspector-note svg { + flex: 0 0 auto; +} +.inspector-footer { + justify-content: space-between; + gap: 8px; + margin-top: auto; + padding-top: 15px; + color: #65757e; + font-size: 8px; +} +.inspector-footer span { + display: flex; + gap: 4px; + align-items: center; +} +.text-button { + border: 0; + background: none; + color: #83c9a9; + font-size: 8px; +} +.automation-lower-grid { + display: grid; + grid-template-columns: minmax(0, 1.45fr) minmax(250px, 0.75fr); + gap: 14px; +} +.automation-steps-card, +.automation-preview-card { + padding: 17px; +} +.automation-section-head { + margin-bottom: 14px; +} +.automation-section-head h2 { + font-size: 13px; +} +.automation-path-list { + display: grid; +} +.automation-path-item { + width: 100%; + gap: 10px; + padding: 9px 0; + border: 0; + border-top: 1px solid #1b2931; + background: transparent; + color: var(--text); + text-align: left; +} +.automation-path-item:hover strong { + color: #8ce0b6; +} +.path-icon { + width: 27px; + height: 27px; + flex-basis: 27px; +} +.automation-path-item > span:nth-child(2) { + min-width: 0; +} +.automation-path-item small { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.path-status { + margin-left: auto; + color: #71828b; + font-size: 8px; +} +.path-status.status-complete { + color: #71d3a2; +} +.path-status.status-running { + color: #e4bb75; +} +.preview-symbol { + color: #7d949b; +} +.preview-result { + gap: 12px; + padding: 12px; + border: 1px solid #243b36; + border-radius: 7px; + background: linear-gradient(115deg, #142820, #101b1d); +} +.preview-score { + font: 500 25px var(--mono); + letter-spacing: -1px; + color: #78d7ab; +} +.preview-score span { + font-size: 12px; +} +.preview-meta { + display: flex; + justify-content: space-between; + margin-top: 13px; + color: #7f8f96; + font-size: 9px; +} +.preview-meta strong { + color: #bbc5c9; + font-size: 9px; + font-weight: 500; +} +.preview-disclaimer { + margin: 14px 0 0; + padding-top: 10px; + border-top: 1px solid #1c2b32; + color: #73838a; + font-size: 9px; + line-height: 1.5; +} +@media (max-width: 1100px) { + .automation-layout { + grid-template-columns: minmax(0, 1fr); + } + .automation-inspector { + min-height: 0; + } + .inspector-footer { + margin-top: 12px; + } + .automation-stage { + height: 400px; + } +} +@media (max-width: 680px) { + .automation-toolbar { + padding: 0 10px; + } + .automation-workflow-label { + gap: 7px; + } + .automation-workflow-label strong { + font-size: 10px; + } + .demo-pill { + font-size: 7px; + } + .automation-stage { + height: 360px; + } + .automation-canvas-hint { + max-width: 58%; + font-size: 8px; + } + .automation-lower-grid { + grid-template-columns: 1fr; + } + .automation-runbar { + font-size: 8px; + } + .automation-inspector { + padding: 13px; + } + .automation-path-item { + gap: 7px; + } + .path-status { + font-size: 7px; + } +} +@media (prefers-reduced-motion: reduce) { + .automation-pulse, + .run-indicator.is-running > span { + animation: none; + } + .automation-step, + .automation-tool-button { + transition: none; + } +} diff --git a/dashboard/src/pages/Automation.tsx b/dashboard/src/pages/Automation.tsx new file mode 100644 index 0000000..e051dc3 --- /dev/null +++ b/dashboard/src/pages/Automation.tsx @@ -0,0 +1,1312 @@ +import { + addEdge, + Background, + type Connection, + Controls, + type Edge, + Handle, + MiniMap, + type Node, + type NodeProps, + Position, + ReactFlow, + ReactFlowProvider, + useEdgesState, + useNodesState, + useReactFlow, +} from "@xyflow/react"; +import "@xyflow/react/dist/style.css"; +import { + Activity, + Bot, + CirclePlay, + Clock3, + GitBranch, + Plus, + RotateCcw, + Save, + Sparkles, + Webhook, +} from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import * as UI from "../components/ui"; +import { + assertWorkflowImportSize, + isConnectionValid, + loadWorkflow, + parseWorkflowImport, + recordWorkflowChange, + redoWorkflow, + saveWorkflow, + starterWorkflow, + undoWorkflow, + type Workflow, + type WorkflowHistory, + type WorkflowKind, + type WorkflowNode, + workflowIssues, +} from "../data/workflows"; +import "./Automation.css"; + +type FlowData = { + title: string; + detail: string; + kind: WorkflowKind; + config: WorkflowNode["config"]; +}; +const icons: Record = { + trigger: Webhook, + schedule: Clock3, + agent: Bot, + condition: GitBranch, + delay: Clock3, + transform: Activity, + notify: Activity, + success: CirclePlay, + failure: Activity, +}; +const palette: WorkflowKind[] = [ + "trigger", + "schedule", + "agent", + "condition", + "delay", + "transform", + "notify", + "success", + "failure", +]; +function WorkflowCard({ data }: NodeProps>) { + const Icon = icons[data.kind]; + return ( + <> + +
+ + + + {data.title} + {data.kind} +
+ + + ); +} +const nodeTypes = { workflow: WorkflowCard }; +const asNodes = (flow: Workflow): Node[] => + flow.nodes.map((n) => ({ + id: n.id, + type: "workflow", + position: { x: n.x, y: n.y }, + data: { title: n.title, detail: n.detail, kind: n.kind, config: n.config }, + })); +const asEdges = (flow: Workflow): Edge[] => + flow.edges.map((e) => ({ + id: e.id, + source: e.source, + target: e.target, + label: e.label, + type: "smoothstep", + })); + +function Editor() { + const initial = useMemo(loadWorkflow, []); + const [nodes, setNodes, onNodesChange] = useNodesState>( + asNodes(starterWorkflow), + ); + const [edges, setEdges, onEdgesChange] = useEdgesState( + asEdges(starterWorkflow), + ); + const [selectedId, setSelectedId] = useState( + starterWorkflow.nodes[1]?.id ?? "", + ); + const [name, setName] = useState(starterWorkflow.name); + const [hydrated, setHydrated] = useState(false); + const [message, setMessage] = useState( + "Changes are stored locally in this browser.", + ); + const [running, setRunning] = useState(false); + const [runs, setRuns] = useState(0); + const [history, setHistory] = useState({ + past: [], + future: [], + }); + const { screenToFlowPosition } = useReactFlow(); + const fileRef = useRef(null); + const selected = nodes.find((n) => n.id === selectedId); + const model = useMemo( + () => ({ + version: 1, + name, + nodes: nodes.map((n) => ({ + id: n.id, + kind: n.data.kind, + title: n.data.title, + detail: n.data.detail, + x: n.position.x, + y: n.position.y, + config: n.data.config, + })), + edges: edges.map((e) => ({ + id: e.id, + source: e.source, + target: e.target, + ...(typeof e.label === "string" ? { label: e.label } : {}), + })), + }), + [name, nodes, edges], + ); + const latestModel = useRef(model); + const hydratedRef = useRef(hydrated); + const issues = useMemo(() => workflowIssues(model), [model]); + latestModel.current = model; + hydratedRef.current = hydrated; + useEffect(() => { + let active = true; + initial + .then((flow) => { + if (!active) return; + setName(flow.name); + setNodes(asNodes(flow)); + setEdges(asEdges(flow)); + setSelectedId(flow.nodes[1]?.id ?? flow.nodes[0]?.id ?? ""); + setHydrated(true); + }) + .catch(() => { + if (active) + setMessage( + "Saved workflows could not be loaded; demo remains available.", + ); + }); + return () => { + active = false; + }; + }, [initial, setNodes, setEdges]); + useEffect(() => { + if (!hydrated) return; + const timer = window.setTimeout(() => { + void saveWorkflow(model).catch(() => + setMessage("Could not save workflow to this browser."), + ); + }, 350); + return () => window.clearTimeout(timer); + }, [hydrated, model]); + useEffect( + () => () => { + if (hydratedRef.current) + void saveWorkflow(latestModel.current).catch(() => undefined); + }, + [], + ); + const snapshot = () => + setHistory((items) => recordWorkflowChange(items, model)); + const connect = useCallback( + (connection: Connection) => { + setHistory((items) => recordWorkflowChange(items, model)); + setEdges((current) => + addEdge( + { ...connection, id: crypto.randomUUID(), type: "smoothstep" }, + current, + ), + ); + }, + [model, setEdges], + ); + const update = (patch: Partial) => { + snapshot(); + setNodes((current) => + current.map((n) => + n.id === selectedId ? { ...n, data: { ...n.data, ...patch } } : n, + ), + ); + }; + const addNode = (kind: WorkflowKind, position?: { x: number; y: number }) => { + snapshot(); + const id = kind + "-" + crypto.randomUUID().slice(0, 6); + const title = kind[0].toUpperCase() + kind.slice(1); + setNodes((current) => [ + ...current, + { + id, + type: "workflow", + position: position ?? { + x: 160 + (current.length % 4) * 220, + y: 120 + Math.floor(current.length / 4) * 140, + }, + data: { title, detail: title + " demo step.", kind, config: {} }, + }, + ]); + setSelectedId(id); + }; + const restore = (flow: Workflow) => { + setName(flow.name); + setNodes(asNodes(flow)); + setEdges(asEdges(flow)); + setSelectedId(flow.nodes[0]?.id ?? ""); + }; + const undo = () => { + const result = undoWorkflow(history, model); + if (!result) return; + setHistory(result.history); + restore(result.workflow); + }; + const redo = () => { + const result = redoWorkflow(history, model); + if (!result) return; + setHistory(result.history); + restore(result.workflow); + }; + const exportFlow = () => { + const url = URL.createObjectURL( + new Blob([JSON.stringify(model, null, 2)], { type: "application/json" }), + ); + const a = document.createElement("a"); + a.href = url; + a.download = + (name.toLowerCase().replace(/[^a-z0-9]+/g, "-") || "workflow") + ".json"; + a.click(); + URL.revokeObjectURL(url); + }; + const importFlow = async (file?: File) => { + if (!file) return; + try { + assertWorkflowImportSize(file.size); + const flow = parseWorkflowImport(await file.text()); + snapshot(); + setName(flow.name); + setNodes(asNodes(flow)); + setEdges(asEdges(flow)); + setSelectedId(flow.nodes[0]?.id ?? ""); + setMessage("Workflow imported and validated."); + } catch (error) { + setMessage( + error instanceof Error ? error.message : "Unable to import workflow.", + ); + } finally { + if (fileRef.current) fileRef.current.value = ""; + } + }; + const run = () => { + if (issues.length || running) return; + setRunning(true); + setRuns((n) => n + 1); + setMessage("Simulated preview · no jobs dispatched"); + window.setTimeout(() => { + setRunning(false); + setMessage("Demo run completed · sample result only"); + }, 1000); + }; + const saveNow = () => { + void saveWorkflow(model) + .then(() => setMessage("Workflow saved in this browser.")) + .catch(() => setMessage("Could not save workflow to this browser.")); + }; + const deleteSelected = () => { + if (!selected) return; + snapshot(); + setNodes((current) => current.filter((n) => n.id !== selectedId)); + setEdges((current) => + current.filter((e) => e.source !== selectedId && e.target !== selectedId), + ); + setSelectedId(""); + }; + const duplicateSelected = () => { + if (!selected) return; + snapshot(); + const id = selected.id + "-copy-" + crypto.randomUUID().slice(0, 5); + setNodes((current) => [ + ...current, + { + ...selected, + id, + selected: true, + position: { x: selected.position.x + 36, y: selected.position.y + 36 }, + data: { ...selected.data, title: selected.data.title + " copy" }, + }, + ]); + setSelectedId(id); + }; + const onDrop = (event: React.DragEvent) => { + event.preventDefault(); + const kind = event.dataTransfer.getData( + "application/opencluster-workflow-kind", + ) as WorkflowKind; + if (palette.includes(kind)) + addNode( + kind, + screenToFlowPosition({ x: event.clientX, y: event.clientY }), + ); + }; + const beforeDelete = async ({ + nodes: removedNodes, + edges: removedEdges, + }: { + nodes: Node[]; + edges: Edge[]; + }) => { + if (removedNodes.length || removedEdges.length) snapshot(); + if (removedNodes.some((node) => node.id === selectedId)) setSelectedId(""); + return true; + }; + + return ( + <> + 0} + > + + {running ? "Running demo…" : "Run demo"} + + } + /> +
+
+
+
+ + + + + setName(e.target.value)} + /> + {nodes.length} nodes · local draft + +
+
+ + DEMO ONLY + + + + + + + + void importFlow(e.target.files?.[0])} + /> +
+
+
+ {palette.map((kind) => { + const Icon = icons[kind]; + return ( + + ); + })} +
+
event.preventDefault()} + onDrop={onDrop} + > + + isConnectionValid(model, connection.source, connection.target) + } + onBeforeDelete={beforeDelete} + onNodeClick={(_, node) => setSelectedId(node.id)} + onPaneClick={() => setSelectedId("")} + fitView + deleteKeyCode={["Backspace", "Delete"]} + defaultEdgeOptions={{ type: "smoothstep" }} + > + + + + +
+
+ + + + {message} + + {runs ? "RUN #" + String(runs).padStart(2, "0") : "NO RUNS YET"} + +
+
+