diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..386fa3a
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,25 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "github-actions" # GitHub Actions
+ directory: "/"
+ schedule:
+ interval: "daily"
+ allow:
+ - dependency-type: "all"
+ groups:
+ all:
+ patterns:
+ - "*"
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ groups:
+ all:
+ patterns:
+ - "*"
diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml
index 26cf58d..6f00aaa 100644
--- a/.github/workflows/Build.yml
+++ b/.github/workflows/Build.yml
@@ -12,32 +12,38 @@ jobs:
contents: write
steps:
- name: Check out code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
- - name: Set up Bun
- uses: oven-sh/setup-bun@v1
+ - name: Set up pnpm
+ uses: pnpm/action-setup@v6
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v7
with:
- bun-version: "latest"
+ node-version: "22"
+ cache: "pnpm"
+
+
- name: Install dependencies
- run: bun install
+ run: pnpm install --frozen-lockfile
- name: Build static export
run: |
- bun run build
+ pnpm run build
- name: Compress dist folder
run: zip -r dist.zip dist
- name: Release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v3
with:
draft: ${{ contains(github.ref_name, '-alpha') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-rc') }}
files: dist.zip
- name: Changelog
- run: bun x changelogithub
+ run: pnpm dlx changelogithub
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
diff --git a/.github/workflows/auto-fix-lint-format-commit.yml b/.github/workflows/auto-fix-lint-format-commit.yml
index 46c0150..430fad0 100644
--- a/.github/workflows/auto-fix-lint-format-commit.yml
+++ b/.github/workflows/auto-fix-lint-format-commit.yml
@@ -1,52 +1,60 @@
name: Auto Fix Lint and Format
on:
- pull_request_target:
+ pull_request:
types: [opened, synchronize]
+permissions:
+ contents: write
+ pull-requests: write
+
jobs:
auto-fix:
+ if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
- repository: ${{ github.event.pull_request.head.repo.full_name }}
- - name: Set up Bun
- uses: oven-sh/setup-bun@v1
+ - name: Set up pnpm
+ uses: pnpm/action-setup@v6
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v7
with:
- bun-version: "latest"
+ node-version: "22"
+ cache: "pnpm"
- name: Install dependencies
- run: bun install
+ run: pnpm install --frozen-lockfile
- name: Run linter and fix issues
- run: bun run lint:fix
+ run: pnpm run lint:fix
- name: Run formatter
- run: bun run format
+ run: pnpm run format
- name: Check for changes
id: check_changes
run: |
- git diff --exit-code || echo "has_changes=true" >> $GITHUB_ENV
+ git diff --exit-code || echo "has_changes=true" >> "$GITHUB_OUTPUT"
- name: Commit and push changes
- if: steps.check_changes.outputs.has_changes == 'true' || env.has_changes == 'true'
- uses: stefanzweifel/git-auto-commit-action@v5
+ if: steps.check_changes.outputs.has_changes == 'true'
+ uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "chore: auto-fix linting and formatting issues"
commit_options: "--no-verify"
file_pattern: "."
- name: Add PR comment
- if: steps.check_changes.outputs.has_changes == 'true' || env.has_changes == 'true'
- uses: actions/github-script@v7
+ if: steps.check_changes.outputs.has_changes == 'true'
+ uses: actions/github-script@v9
with:
- github-token: ${{secrets.GITHUB_TOKEN}}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1b41c38..66c985e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,21 +18,26 @@ jobs:
contents: read
steps:
- name: Check out code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: Set up pnpm
- uses: pnpm/action-setup@v4
- with:
- version: 9
+ uses: pnpm/action-setup@v6
- name: Set up Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v7
with:
node-version: 22
cache: pnpm
+
- name: Install dependencies
run: pnpm install --frozen-lockfile
+ - name: Run quality checks
+ run: pnpm run check
+
+ - name: Run tests
+ run: pnpm run test:coverage
+
- name: Run build
- run: pnpm run build
\ No newline at end of file
+ run: pnpm run build
diff --git a/.gitignore b/.gitignore
index c234903..7b043b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ package-lock.json
dist
dist-ssr
dev-dist
+coverage
*.local
# Editor directories and files
diff --git a/biome.json b/biome.json
index 4f42c6f..a6924e8 100644
--- a/biome.json
+++ b/biome.json
@@ -1,5 +1,5 @@
{
- "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
+ "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
diff --git a/bun.lock b/bun.lock
deleted file mode 100644
index a9a33b1..0000000
--- a/bun.lock
+++ /dev/null
@@ -1,709 +0,0 @@
-{
- "lockfileVersion": 1,
- "configVersion": 0,
- "workspaces": {
- "": {
- "name": "nazha-dashboard-vite",
- "dependencies": {
- "@fontsource/inter": "5.1.1",
- "@heroicons/react": "2.2.0",
- "@radix-ui/react-accordion": "1.2.3",
- "@radix-ui/react-checkbox": "1.1.4",
- "@radix-ui/react-dialog": "^1.1.15",
- "@radix-ui/react-dropdown-menu": "2.1.6",
- "@radix-ui/react-label": "2.1.2",
- "@radix-ui/react-popover": "1.1.6",
- "@radix-ui/react-progress": "1.1.2",
- "@radix-ui/react-select": "2.1.6",
- "@radix-ui/react-separator": "1.1.2",
- "@radix-ui/react-slot": "1.1.2",
- "@radix-ui/react-switch": "1.1.3",
- "@radix-ui/react-tooltip": "1.1.8",
- "@tanstack/react-query": "5.66.7",
- "@tanstack/react-query-devtools": "5.66.7",
- "@tanstack/react-table": "8.21.2",
- "@types/d3-geo": "3.1.0",
- "@types/luxon": "3.4.2",
- "class-variance-authority": "0.7.1",
- "clsx": "2.1.1",
- "cmdk": "1.1.1",
- "country-flag-icons": "1.5.18",
- "d3-geo": "3.1.1",
- "dayjs": "1.11.13",
- "framer-motion": "12.23.26",
- "i18n-iso-countries": "7.14.0",
- "i18next": "24.2.2",
- "lucide-react": "0.460.0",
- "luxon": "3.5.0",
- "react": "19.0.0",
- "react-dom": "19.0.0",
- "react-i18next": "15.4.1",
- "react-router-dom": "^7.13.0",
- "recharts": "2.15.1",
- "sonner": "1.7.4",
- "tailwind-merge": "2.6.0",
- "tailwindcss-animate": "^1.0.7",
- },
- "devDependencies": {
- "@biomejs/biome": "2.3.10",
- "@tailwindcss/postcss": "^4.1.18",
- "@types/node": "22.13.4",
- "@types/react": "19.0.10",
- "@types/react-dom": "19.0.4",
- "@vitejs/plugin-react-swc": "3.8.0",
- "globals": "15.15.0",
- "postcss": "8.5.3",
- "tailwindcss": "^4.1.18",
- "typescript": "~5.6.3",
- "vite": "6.4.1",
- },
- },
- },
- "packages": {
- "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
-
- "@babel/runtime": ["@babel/runtime@7.26.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw=="],
-
- "@biomejs/biome": ["@biomejs/biome@2.3.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.10", "@biomejs/cli-darwin-x64": "2.3.10", "@biomejs/cli-linux-arm64": "2.3.10", "@biomejs/cli-linux-arm64-musl": "2.3.10", "@biomejs/cli-linux-x64": "2.3.10", "@biomejs/cli-linux-x64-musl": "2.3.10", "@biomejs/cli-win32-arm64": "2.3.10", "@biomejs/cli-win32-x64": "2.3.10" }, "bin": { "biome": "bin/biome" } }, "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ=="],
-
- "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w=="],
-
- "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg=="],
-
- "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA=="],
-
- "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A=="],
-
- "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw=="],
-
- "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g=="],
-
- "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ=="],
-
- "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.10", "", { "os": "win32", "cpu": "x64" }, "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ=="],
-
- "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
-
- "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
-
- "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
-
- "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
-
- "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
-
- "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
-
- "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
-
- "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
-
- "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
-
- "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
-
- "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
-
- "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
-
- "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
-
- "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
-
- "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
-
- "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
-
- "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
-
- "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
-
- "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
-
- "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
-
- "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
-
- "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
-
- "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
-
- "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
-
- "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
-
- "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
-
- "@floating-ui/core": ["@floating-ui/core@1.6.8", "", { "dependencies": { "@floating-ui/utils": "^0.2.8" } }, "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA=="],
-
- "@floating-ui/dom": ["@floating-ui/dom@1.6.11", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.8" } }, "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ=="],
-
- "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="],
-
- "@floating-ui/utils": ["@floating-ui/utils@0.2.8", "", {}, "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="],
-
- "@fontsource/inter": ["@fontsource/inter@5.1.1", "", {}, "sha512-weN3E+rq0Xb3Z93VHJ+Rc7WOQX9ETJPTAJ+gDcaMHtjft67L58sfS65rAjC5tZUXQ2FdZ/V1/sSzCwZ6v05kJw=="],
-
- "@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="],
-
- "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.5", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg=="],
-
- "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
-
- "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
-
- "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
-
- "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
-
- "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
-
- "@radix-ui/number": ["@radix-ui/number@1.1.0", "", {}, "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ=="],
-
- "@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
-
- "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collapsible": "1.1.3", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-RIQ15mrcvqIkDARJeERSuXSry2N8uYnxkdDetpfmalT/+0ntOXLkFOsh9iwlAsCv+qcmhZjbdJogIm6WBa6c4A=="],
-
- "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
-
- "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw=="],
-
- "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw=="],
-
- "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw=="],
-
- "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
-
- "@radix-ui/react-context": ["@radix-ui/react-context@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q=="],
-
- "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
-
- "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg=="],
-
- "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
-
- "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-menu": "2.1.6", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA=="],
-
- "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
-
- "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
-
- "@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="],
-
- "@radix-ui/react-label": ["@radix-ui/react-label@2.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw=="],
-
- "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-roving-focus": "1.1.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-callback-ref": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg=="],
-
- "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg=="],
-
- "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.2", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA=="],
-
- "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
-
- "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="],
-
- "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="],
-
- "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.2", "", { "dependencies": { "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA=="],
-
- "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw=="],
-
- "@radix-ui/react-select": ["@radix-ui/react-select@2.1.6", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg=="],
-
- "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ=="],
-
- "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
-
- "@radix-ui/react-switch": ["@radix-ui/react-switch@1.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ=="],
-
- "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="],
-
- "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="],
-
- "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="],
-
- "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
-
- "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
-
- "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="],
-
- "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og=="],
-
- "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.0", "", { "dependencies": { "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ=="],
-
- "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw=="],
-
- "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q=="],
-
- "@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="],
-
- "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
-
- "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
-
- "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
-
- "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
-
- "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="],
-
- "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="],
-
- "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="],
-
- "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="],
-
- "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="],
-
- "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="],
-
- "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="],
-
- "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="],
-
- "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="],
-
- "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="],
-
- "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="],
-
- "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="],
-
- "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="],
-
- "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="],
-
- "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="],
-
- "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="],
-
- "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="],
-
- "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="],
-
- "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="],
-
- "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="],
-
- "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
-
- "@swc/core": ["@swc/core@1.10.15", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.17" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.10.15", "@swc/core-darwin-x64": "1.10.15", "@swc/core-linux-arm-gnueabihf": "1.10.15", "@swc/core-linux-arm64-gnu": "1.10.15", "@swc/core-linux-arm64-musl": "1.10.15", "@swc/core-linux-x64-gnu": "1.10.15", "@swc/core-linux-x64-musl": "1.10.15", "@swc/core-win32-arm64-msvc": "1.10.15", "@swc/core-win32-ia32-msvc": "1.10.15", "@swc/core-win32-x64-msvc": "1.10.15" }, "peerDependencies": { "@swc/helpers": "*" }, "optionalPeers": ["@swc/helpers"] }, "sha512-/iFeQuNaGdK7mfJbQcObhAhsMqLT7qgMYl7jX2GEIO+VDTejESpzAyKwaMeYXExN8D6e5BRHBCe7M5YlsuzjDA=="],
-
- "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.10.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zFdZ6/yHqMCPk7OhLFqHy/MQ1EqJhcZMpNHd1gXYT7VRU3FaqvvKETrUlG3VYl65McPC7AhMRfXPyJ0JO/jARQ=="],
-
- "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.10.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-8g4yiQwbr8fxOOjKXdot0dEkE5zgE8uNZudLy/ZyAhiwiZ8pbJ8/wVrDOu6dqbX7FBXAoDnvZ7fwN1jk4C8jdA=="],
-
- "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.10.15", "", { "os": "linux", "cpu": "arm" }, "sha512-rl+eVOltl2+7WXOnvmWBpMgh6aO13G5x0U0g8hjwlmD6ku3Y9iRcThpOhm7IytMEarUp5pQxItNoPq+VUGjVHg=="],
-
- "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.10.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-qxWEQeyAJMWJqjaN4hi58WMpPdt3Tn0biSK9CYRegQtvZWCbewr6v2agtSu5AZ2rudeH6OfCWAMDQQeSgn6PJQ=="],
-
- "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.10.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-QcELd9/+HjZx0WCxRrKcyKGWTiQ0485kFb5w8waxcSNd0d9Lgk4EFfWWVyvIb5gIHpDQmhrgzI/yRaWQX4YSZQ=="],
-
- "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.10.15", "", { "os": "linux", "cpu": "x64" }, "sha512-S1+ZEEn3+a/MiMeQqQypbwTGoBG8/sPoCvpNbk+uValyygT+jSn3U0xVr45FbukpmMB+NhBMqfedMLqKA0QnJA=="],
-
- "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.10.15", "", { "os": "linux", "cpu": "x64" }, "sha512-qW+H9g/2zTJ4jP7NDw4VAALY0ZlNEKzYsEoSj/HKi7k3tYEHjMzsxjfsY9I8WZCft23bBdV3RTCPoxCshaj1CQ=="],
-
- "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.10.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-AhRB11aA6LxjIqut+mg7qsu/7soQDmbK6MKR9nP3hgBszpqtXbRba58lr24xIbBCMr+dpo6kgEapWt+t5Po6Zg=="],
-
- "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.10.15", "", { "os": "win32", "cpu": "ia32" }, "sha512-UGdh430TQwbDn6KjgvRTg1fO022sbQ4yCCHUev0+5B8uoBwi9a89qAz3emy2m56C8TXxUoihW9Y9OMfaRwPXUw=="],
-
- "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.10.15", "", { "os": "win32", "cpu": "x64" }, "sha512-XJzBCqO1m929qbJsOG7FZXQWX26TnEoMctS3QjuCoyBmkHxxQmZsy78KjMes1aomTcKHCyFYgrRGWgVmk7tT4Q=="],
-
- "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
-
- "@swc/types": ["@swc/types@0.1.17", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ=="],
-
- "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
-
- "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
-
- "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
-
- "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
-
- "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
-
- "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
-
- "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
-
- "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
-
- "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
-
- "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
-
- "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
-
- "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
-
- "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
-
- "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
-
- "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="],
-
- "@tanstack/query-core": ["@tanstack/query-core@5.66.4", "", {}, "sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA=="],
-
- "@tanstack/query-devtools": ["@tanstack/query-devtools@5.65.0", "", {}, "sha512-g5y7zc07U9D3esMdqUfTEVu9kMHoIaVBsD0+M3LPdAdD710RpTcLiNvJY1JkYXqkq9+NV+CQoemVNpQPBXVsJg=="],
-
- "@tanstack/react-query": ["@tanstack/react-query@5.66.7", "", { "dependencies": { "@tanstack/query-core": "5.66.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-qd3q/tUpF2K1xItfPZddk1k/8pSXnovg41XyCqJgPoyYEirMBtB0sVEVVQ/CsAOngzgWtBPXimVf4q4kM9uO6A=="],
-
- "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.66.7", "", { "dependencies": { "@tanstack/query-devtools": "5.65.0" }, "peerDependencies": { "@tanstack/react-query": "^5.66.7", "react": "^18 || ^19" } }, "sha512-40z4PPkz06tYIF0vwLZZIZfZxKUH4OAaBOR14blCFyYm6hlU6qc+M82mkZ+D00HcEMhV7P4XeJiEuDhFq0q9Qw=="],
-
- "@tanstack/react-table": ["@tanstack/react-table@8.21.2", "", { "dependencies": { "@tanstack/table-core": "8.21.2" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg=="],
-
- "@tanstack/table-core": ["@tanstack/table-core@8.21.2", "", {}, "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA=="],
-
- "@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
-
- "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
-
- "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
-
- "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
-
- "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
-
- "@types/d3-path": ["@types/d3-path@3.1.0", "", {}, "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="],
-
- "@types/d3-scale": ["@types/d3-scale@4.0.8", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ=="],
-
- "@types/d3-shape": ["@types/d3-shape@3.1.6", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA=="],
-
- "@types/d3-time": ["@types/d3-time@3.0.3", "", {}, "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw=="],
-
- "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
-
- "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
-
- "@types/geojson": ["@types/geojson@7946.0.14", "", {}, "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg=="],
-
- "@types/luxon": ["@types/luxon@3.4.2", "", {}, "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA=="],
-
- "@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="],
-
- "@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="],
-
- "@types/react-dom": ["@types/react-dom@19.0.4", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="],
-
- "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.8.0", "", { "dependencies": { "@swc/core": "^1.10.15" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw=="],
-
- "aria-hidden": ["aria-hidden@1.2.4", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A=="],
-
- "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
-
- "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
-
- "cmdk": ["cmdk@1.1.1", "", { "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" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
-
- "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
-
- "country-flag-icons": ["country-flag-icons@1.5.18", "", {}, "sha512-z+Uzesi8u8IdkViqqbzzbkf3+a7WJpcET5B7sPwTg7GXqPYpVEgNlZ/FC3l8KO4mEf+mNkmzKLppKTN4PlCJEQ=="],
-
- "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
-
- "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
-
- "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
-
- "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
-
- "d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="],
-
- "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
-
- "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
-
- "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
-
- "d3-scale": ["d3-scale@4.0.2", "", { "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" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
-
- "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
-
- "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
-
- "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
-
- "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
-
- "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="],
-
- "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
-
- "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
-
- "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
-
- "diacritics": ["diacritics@1.3.0", "", {}, "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA=="],
-
- "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
-
- "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
-
- "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
-
- "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
-
- "fast-equals": ["fast-equals@5.0.1", "", {}, "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ=="],
-
- "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
-
- "framer-motion": ["framer-motion@12.23.26", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA=="],
-
- "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
-
- "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
-
- "globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
-
- "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
-
- "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
-
- "i18n-iso-countries": ["i18n-iso-countries@7.14.0", "", { "dependencies": { "diacritics": "1.3.0" } }, "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg=="],
-
- "i18next": ["i18next@24.2.2", "", { "dependencies": { "@babel/runtime": "^7.23.2" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ=="],
-
- "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
-
- "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
-
- "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
-
- "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
-
- "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
-
- "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
-
- "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
-
- "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
-
- "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
-
- "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
-
- "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
-
- "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
-
- "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
-
- "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
-
- "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
-
- "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
-
- "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
-
- "lucide-react": ["lucide-react@0.460.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg=="],
-
- "luxon": ["luxon@3.5.0", "", {}, "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ=="],
-
- "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
-
- "motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="],
-
- "motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
-
- "nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
-
- "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
-
- "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
-
- "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
-
- "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
-
- "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
-
- "react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
-
- "react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
-
- "react-i18next": ["react-i18next@15.4.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw=="],
-
- "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
-
- "react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "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" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="],
-
- "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
-
- "react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="],
-
- "react-router-dom": ["react-router-dom@7.13.0", "", { "dependencies": { "react-router": "7.13.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g=="],
-
- "react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="],
-
- "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
-
- "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
-
- "recharts": ["recharts@2.15.1", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q=="],
-
- "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="],
-
- "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
-
- "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
-
- "scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="],
-
- "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
-
- "sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="],
-
- "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
-
- "tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="],
-
- "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
-
- "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
-
- "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
-
- "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
-
- "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
-
- "tslib": ["tslib@2.8.0", "", {}, "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="],
-
- "typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
-
- "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
-
- "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
-
- "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
-
- "victory-vendor": ["victory-vendor@36.9.2", "", { "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" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
-
- "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
-
- "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
-
- "@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
-
- "@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
-
- "@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
-
- "@radix-ui/react-dialog/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
-
- "@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
- "@radix-ui/react-dialog/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
-
- "@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
-
- "@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
- "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
- "@radix-ui/react-dialog/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
-
- "@radix-ui/react-dismissable-layer/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
-
- "@radix-ui/react-dismissable-layer/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
-
- "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
- "@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
-
- "@radix-ui/react-focus-scope/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
-
- "@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
- "@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
-
- "@radix-ui/react-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
-
- "@radix-ui/react-menu/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
-
- "@radix-ui/react-menu/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA=="],
-
- "@radix-ui/react-menu/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
-
- "@radix-ui/react-popover/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
-
- "@radix-ui/react-popover/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
-
- "@radix-ui/react-popover/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA=="],
-
- "@radix-ui/react-popover/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
-
- "@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
- "@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
-
- "@radix-ui/react-select/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
-
- "@radix-ui/react-select/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
-
- "@radix-ui/react-select/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA=="],
-
- "@radix-ui/react-select/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
-
- "@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
-
- "@radix-ui/react-tooltip/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
-
- "@radix-ui/react-use-effect-event/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
-
- "@radix-ui/react-use-escape-keydown/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
-
- "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
-
- "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
-
- "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
-
- "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="],
-
- "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
-
- "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.0", "", { "bundled": true }, "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="],
-
- "cmdk/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw=="],
-
- "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
-
- "@radix-ui/react-dialog/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
-
- "@radix-ui/react-dialog/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
-
- "@radix-ui/react-dialog/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
-
- "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
- "@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
- "@radix-ui/react-menu/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="],
-
- "@radix-ui/react-popover/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="],
-
- "@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
- "@radix-ui/react-select/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="],
-
- "@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="],
-
- "cmdk/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
-
- "cmdk/@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
-
- "cmdk/@radix-ui/react-dialog/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA=="],
-
- "cmdk/@radix-ui/react-dialog/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="],
-
- "@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
-
- "cmdk/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="],
- }
-}
diff --git a/index.html b/index.html
index 34e9f65..ec0ec58 100644
--- a/index.html
+++ b/index.html
@@ -1,95 +1,50 @@
-
+
-
-
"),
+ );
+
+ renderApp();
+
+ await waitFor(() => {
+ expect(appMocks.injectContext).toHaveBeenCalledWith(
+ "",
+ );
+ });
+ expect(await screen.findByText("server-page")).toBeInTheDocument();
+ });
+
+ it("renders the app shell when initial settings fetch fails", async () => {
+ appMocks.fetchSetting.mockRejectedValue(new Error("settings failed"));
+
+ renderApp();
+
+ expect(await screen.findByText("server-page")).toBeInTheDocument();
+ expect(screen.getByText("refresh-toast")).toBeInTheDocument();
+ expect(screen.getByText("header")).toBeInTheDocument();
+ expect(screen.getByText("dash-command")).toBeInTheDocument();
+ expect(screen.getByText("footer")).toBeInTheDocument();
+ expect(await screen.findByText("settings failed")).toBeInTheDocument();
+ });
+
+ it("keeps rendering with stale settings when a later settings refetch fails", async () => {
+ let requestCount = 0;
+ appMocks.fetchSetting.mockImplementation(() => {
+ requestCount += 1;
+ return requestCount === 1
+ ? Promise.resolve(settingResponse())
+ : Promise.reject(new Error("settings failed"));
+ });
+
+ const { queryClient } = renderApp();
+
+ expect(await screen.findByText("server-page")).toBeInTheDocument();
+
+ await queryClient.refetchQueries({ queryKey: ["setting"] });
+
+ await waitFor(() => {
+ expect(appMocks.fetchSetting.mock.calls.length).toBeGreaterThanOrEqual(2);
+ });
+ expect(screen.getByText("server-page")).toBeInTheDocument();
+ expect(screen.queryByText("settings failed")).not.toBeInTheDocument();
+ });
+
+ it("routes server detail paths through the app router", async () => {
+ renderApp("/server/42");
+
+ expect(await screen.findByText("server-detail-page")).toBeInTheDocument();
+ });
+});
diff --git a/src/test/components/RemainPercentBar.test.tsx b/src/test/components/RemainPercentBar.test.tsx
new file mode 100644
index 0000000..3c04610
--- /dev/null
+++ b/src/test/components/RemainPercentBar.test.tsx
@@ -0,0 +1,22 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import RemainPercentBar from "@/components/RemainPercentBar";
+
+describe("RemainPercentBar", () => {
+ it("uses status colors for low, medium, and healthy remaining percentages", () => {
+ const { rerender } = render();
+ const progress = screen.getByRole("progressbar");
+ const indicator = progress.firstElementChild;
+
+ expect(progress).toHaveClass("w-[70px]");
+ expect(indicator).toHaveClass("bg-red-500");
+ expect(indicator).toHaveStyle({ transform: "translateX(-71%)" });
+
+ rerender();
+ expect(progress.firstElementChild).toHaveClass("bg-orange-400");
+
+ rerender();
+ expect(progress.firstElementChild).toHaveClass("bg-green-500");
+ });
+});
diff --git a/src/test/components/ThemeProvider.test.tsx b/src/test/components/ThemeProvider.test.tsx
new file mode 100644
index 0000000..3fbb659
--- /dev/null
+++ b/src/test/components/ThemeProvider.test.tsx
@@ -0,0 +1,144 @@
+import { act, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi } from "vitest";
+import { ThemeColorManager } from "@/components/ThemeColorManager";
+import {
+ ThemeProvider,
+ ThemeProviderContext,
+} from "@/components/ThemeProvider";
+import { useTheme } from "@/hooks/use-theme";
+
+function ThemeProbe() {
+ const { setTheme, theme } = useTheme();
+
+ return (
+
+
{theme}
+
+
+
+ );
+}
+
+function InvalidThemeProbe() {
+ useTheme();
+ return null;
+}
+
+describe("ThemeProvider", () => {
+ it("applies theme classes, meta color, and localStorage state", async () => {
+ const user = userEvent.setup();
+ const meta = document.createElement("meta");
+ meta.name = "theme-color";
+ document.head.appendChild(meta);
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId("theme-state")).toHaveTextContent("system");
+ expect(document.documentElement).toHaveClass("light");
+
+ await user.click(screen.getByRole("button", { name: "dark" }));
+
+ expect(screen.getByTestId("theme-state")).toHaveTextContent("dark");
+ expect(localStorage.getItem("theme-test")).toBe("dark");
+ expect(document.documentElement).toHaveClass("dark");
+ expect(document.documentElement.style.colorScheme).toBe("dark");
+ expect(document.querySelector('meta[name="theme-color"]')).toHaveAttribute(
+ "content",
+ "hsl(30 15% 8%)",
+ );
+
+ await user.click(screen.getByRole("button", { name: "light" }));
+ expect(document.documentElement).toHaveClass("light");
+ });
+
+ it("uses stored theme value as the initial theme", () => {
+ localStorage.setItem("theme-test", "dark");
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId("theme-state")).toHaveTextContent("dark");
+ expect(document.documentElement).toHaveClass("dark");
+ });
+
+ it("follows system color scheme changes and cleans up its listener", () => {
+ let systemDark = true;
+ let changeListener: (() => void) | undefined;
+ const addEventListener = vi.fn((_event: string, listener: () => void) => {
+ changeListener = listener;
+ });
+ const removeEventListener = vi.fn();
+
+ vi.mocked(window.matchMedia).mockReturnValue({
+ get matches() {
+ return systemDark;
+ },
+ media: "(prefers-color-scheme: dark)",
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener,
+ removeEventListener,
+ dispatchEvent: vi.fn(),
+ } as unknown as MediaQueryList);
+
+ const { unmount } = render(
+
+
+ ,
+ );
+
+ expect(document.documentElement).toHaveClass("dark");
+
+ act(() => {
+ systemDark = false;
+ changeListener?.();
+ });
+
+ expect(document.documentElement).toHaveClass("light");
+
+ unmount();
+
+ expect(removeEventListener).toHaveBeenCalledWith("change", changeListener);
+ });
+
+ it("keeps theme-color meta in sync through ThemeColorManager", () => {
+ render(
+
+
+
+ ,
+ );
+
+ act(() => {
+ screen.getByRole("button", { name: "dark" }).click();
+ });
+
+ expect(document.querySelector('meta[name="theme-color"]')).toHaveAttribute(
+ "content",
+ "hsl(30 15% 8%)",
+ );
+ });
+
+ it("throws when the theme context is explicitly unavailable", () => {
+ expect(() =>
+ render(
+
+
+ ,
+ ),
+ ).toThrow("useTheme must be used within a ThemeProvider");
+ });
+});
diff --git a/src/test/components/basic-components.test.tsx b/src/test/components/basic-components.test.tsx
new file mode 100644
index 0000000..f1f9255
--- /dev/null
+++ b/src/test/components/basic-components.test.tsx
@@ -0,0 +1,185 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it } from "vitest";
+import AnimateCountClient, { AnimateCount } from "@/components/AnimatedCount";
+import ErrorBoundary from "@/components/ErrorBoundary";
+import ChartSkeleton from "@/components/loading/ChartSkeleton";
+import { Loader, LoadingSpinner } from "@/components/loading/Loader";
+import NetworkChartLoading from "@/components/NetworkChartLoading";
+import { SearchButton } from "@/components/SearchButton";
+import ServerOverview from "@/components/ServerOverview";
+import ServerUsageBar from "@/components/ServerUsageBar";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Separator } from "@/components/ui/separator";
+import { Skeleton } from "@/components/ui/skeleton";
+
+import { CommandProvider } from "@/context/command-provider";
+import { StatusProvider } from "@/context/status-provider";
+import { useCommand } from "@/hooks/use-command";
+import { useStatus } from "@/hooks/use-status";
+
+function BrokenComponent(): never {
+ throw new Error("render failed");
+}
+
+function CommandReadout() {
+ const { isOpen } = useCommand();
+ return {isOpen ? "command-open" : "command-closed"}
;
+}
+
+function StatusReadout() {
+ const { status } = useStatus();
+ return {`status:${status}`}
;
+}
+
+describe("basic display components", () => {
+ it("renders animated counts with padded digits", () => {
+ render({7});
+
+ expect(screen.getAllByText("0")).toHaveLength(4);
+ expect(screen.getAllByText("7")).toHaveLength(1);
+ });
+
+ it("keeps the animated count client stable across count changes", () => {
+ const { rerender } = render(
+ ,
+ );
+
+ expect(screen.getAllByText("1")).toHaveLength(2);
+ rerender();
+ expect(screen.getByText("2")).toBeInTheDocument();
+ });
+
+ it("renders server usage colors for healthy, warning, and critical values", () => {
+ const { rerender } = render();
+ expect(screen.getByRole("progressbar").firstElementChild).toHaveClass(
+ "bg-green-500",
+ );
+
+ rerender();
+ expect(screen.getByRole("progressbar").firstElementChild).toHaveClass(
+ "bg-orange-400",
+ );
+
+ rerender();
+ expect(screen.getByRole("progressbar").firstElementChild).toHaveClass(
+ "bg-red-500",
+ );
+ });
+
+ it("renders loading primitives", () => {
+ const { container } = render(
+ <>
+
+
+
+
+ >,
+ );
+
+ expect(
+ container.querySelector("[data-visible='true']"),
+ ).toBeInTheDocument();
+ expect(container.querySelectorAll(".hamster-loading-bar")).toHaveLength(16);
+ expect(container.querySelector(".animate-spin")).toBeInTheDocument();
+ expect(container.querySelector(".h-\\[250px\\]")).toBeInTheDocument();
+ });
+
+ it("catches render errors with ErrorBoundary", () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByText("500")).toBeInTheDocument();
+ expect(screen.getByText("render failed")).toBeInTheDocument();
+ });
+});
+
+describe("interactive app controls", () => {
+ it("opens command search from the search button", async () => {
+ const user = userEvent.setup();
+ render(
+
+
+
+ ,
+ );
+
+ expect(screen.getByText("command-closed")).toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "Search" }));
+ expect(screen.getByText("command-open")).toBeInTheDocument();
+ });
+
+ it("lets overview cards update the global status filter", () => {
+ render(
+
+ {}}
+ activeView="servers"
+ />
+
+
+
+ ,
+ );
+
+ expect(screen.getByText("status:all")).toBeInTheDocument();
+ fireEvent.click(screen.getByText("serverOverview.onlineServers"));
+ expect(screen.getByText("status:online")).toBeInTheDocument();
+ fireEvent.click(screen.getByText("serverOverview.offlineServers"));
+ expect(screen.getByText("status:offline")).toBeInTheDocument();
+ fireEvent.click(screen.getByText("serverOverview.totalServers"));
+ expect(screen.getByText("status:all")).toBeInTheDocument();
+ });
+});
+
+describe("ui primitives", () => {
+ it("forwards classes and events through Button and Input", async () => {
+ const user = userEvent.setup();
+ const onClick = vi.fn();
+ const onChange = vi.fn();
+ render(
+ <>
+
+
+ >,
+ );
+
+ await user.click(screen.getByRole("button", { name: "Save" }));
+ await user.type(screen.getByPlaceholderText("Name"), "edge");
+
+ expect(onClick).toHaveBeenCalledOnce();
+ expect(onChange).toHaveBeenCalled();
+ expect(screen.getByRole("button", { name: "Save" })).toHaveClass(
+ "bg-secondary",
+ );
+ });
+
+ it("renders skeletons and separators with expected orientation classes", () => {
+ const { container } = render(
+ <>
+
+
+
+ >,
+ );
+
+ expect(screen.getByTestId("skeleton")).toHaveClass("animate-pulse", "h-4");
+ expect(screen.getByTestId("horizontal")).toHaveClass("h-px", "w-full");
+ expect(screen.getByTestId("vertical")).toHaveClass("h-full", "w-px");
+ expect(container.querySelectorAll("[data-orientation]")).toHaveLength(2);
+ });
+});
diff --git a/src/test/components/chart-primitives.test.tsx b/src/test/components/chart-primitives.test.tsx
new file mode 100644
index 0000000..f7c85db
--- /dev/null
+++ b/src/test/components/chart-primitives.test.tsx
@@ -0,0 +1,136 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import {
+ ChartContainer,
+ ChartLegendContent,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
+
+function CpuIcon() {
+ return icon;
+}
+
+const chartConfig = {
+ cpu: {
+ label: "CPU Usage",
+ icon: CpuIcon,
+ },
+ mem: {
+ label: "Memory",
+ color: "#00ff00",
+ },
+ themed: {
+ label: "Themed",
+ theme: {
+ light: "#ffffff",
+ dark: "#000000",
+ },
+ },
+};
+
+describe("chart primitives", () => {
+ it("renders tooltip rows with config labels, icons, and formatted values", () => {
+ const formatter = vi.fn((value: number, name: string) => (
+ {`${name}:${value.toFixed(1)}`}
+ ));
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByText("CPU Usage")).toBeInTheDocument();
+ expect(screen.getByText("cpu:12.3")).toBeInTheDocument();
+ expect(screen.getByText("mem:42.0")).toBeInTheDocument();
+ expect(formatter).toHaveBeenCalled();
+ });
+
+ it("renders nested labels for single line indicators and hides inactive tooltips", () => {
+ const { container, rerender } = render(
+
+
+ ,
+ );
+
+ expect(screen.getAllByText("CPU Usage")).toHaveLength(2);
+ expect(screen.getByText("12.00")).toBeInTheDocument();
+
+ rerender(
+
+
+ ,
+ );
+
+ expect(container).not.toHaveTextContent("CPU Usage");
+ });
+
+ it("renders legend payloads and chart theme style blocks", () => {
+ const { container, rerender } = render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId("cpu-icon")).toBeInTheDocument();
+ expect(screen.getByText("mem")).toBeInTheDocument();
+ expect(container.querySelector("style")?.textContent).toContain(
+ "--color-themed",
+ );
+
+ rerender(
+
+
+ ,
+ );
+
+ expect(screen.queryByText("mem")).not.toBeInTheDocument();
+ });
+});
diff --git a/src/test/components/dash-command.test.tsx b/src/test/components/dash-command.test.tsx
new file mode 100644
index 0000000..0cf7f6a
--- /dev/null
+++ b/src/test/components/dash-command.test.tsx
@@ -0,0 +1,118 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { DashCommand } from "@/components/DashCommand";
+import { createServer } from "@/test/fixtures";
+
+const dashMocks = vi.hoisted(() => ({
+ closeCommand: vi.fn(),
+ isOpen: true,
+ lastData: null as {
+ now: number;
+ servers: ReturnType[];
+ } | null,
+ navigate: vi.fn(),
+ setTheme: vi.fn(),
+ toggleCommand: vi.fn(),
+ connected: true,
+}));
+
+vi.mock("@/hooks/use-command", () => ({
+ useCommand: () => ({
+ closeCommand: dashMocks.closeCommand,
+ isOpen: dashMocks.isOpen,
+ toggleCommand: dashMocks.toggleCommand,
+ }),
+}));
+
+vi.mock("@/hooks/use-theme", () => ({
+ useTheme: () => ({
+ setTheme: dashMocks.setTheme,
+ }),
+}));
+
+vi.mock("@/hooks/use-websocket-context", () => ({
+ useWebSocketContext: () => ({
+ connected: dashMocks.connected,
+ lastData: dashMocks.lastData,
+ }),
+}));
+
+vi.mock("react-router-dom", async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ useNavigate: () => dashMocks.navigate,
+ };
+});
+
+function seedWebSocketData() {
+ dashMocks.connected = true;
+ dashMocks.lastData = {
+ now: Date.parse("2025-01-01T00:00:20.000Z"),
+ servers: [
+ createServer({
+ id: 7,
+ name: "edge-7",
+ last_active: "2025-01-01T00:00:00.000Z",
+ }),
+ ],
+ };
+}
+
+describe("DashCommand", () => {
+ beforeEach(() => {
+ dashMocks.closeCommand.mockClear();
+ dashMocks.navigate.mockClear();
+ dashMocks.setTheme.mockClear();
+ dashMocks.toggleCommand.mockClear();
+ dashMocks.isOpen = true;
+ dashMocks.connected = true;
+ dashMocks.lastData = null;
+ });
+
+ it("does not render until websocket data is available", () => {
+ dashMocks.connected = false;
+ dashMocks.lastData = null;
+
+ const { container } = render();
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("does not mount server command items while the dialog is closed", () => {
+ seedWebSocketData();
+ dashMocks.isOpen = false;
+
+ render();
+
+ expect(screen.queryByText("edge-7")).not.toBeInTheDocument();
+ });
+
+ it("renders server and shortcut commands, handles selection, and listens for keyboard toggle", async () => {
+ const user = userEvent.setup();
+ seedWebSocketData();
+
+ render();
+
+ fireEvent.keyDown(document, { key: "k", metaKey: true });
+ expect(dashMocks.toggleCommand).toHaveBeenCalledOnce();
+
+ expect(screen.getByPlaceholderText("TypeCommand")).toBeInTheDocument();
+ expect(screen.getByText("edge-7")).toBeInTheDocument();
+ expect(screen.getByText("ToggleDarkMode")).toBeInTheDocument();
+
+ Object.defineProperty(window, "scrollY", {
+ configurable: true,
+ value: 256,
+ });
+ await user.click(screen.getByText("edge-7"));
+ expect(dashMocks.navigate).toHaveBeenCalledWith("/server/7");
+ expect(sessionStorage.getItem("fromMainPage")).toBe("true");
+ expect(sessionStorage.getItem("scrollPosition")).toBe("256");
+ expect(dashMocks.closeCommand).toHaveBeenCalled();
+
+ await user.click(screen.getByText("ToggleDarkMode"));
+ expect(dashMocks.setTheme).toHaveBeenCalledWith("dark");
+ });
+});
diff --git a/src/test/components/global-map.test.tsx b/src/test/components/global-map.test.tsx
new file mode 100644
index 0000000..73b039a
--- /dev/null
+++ b/src/test/components/global-map.test.tsx
@@ -0,0 +1,176 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { MemoryRouter, useLocation } from "react-router-dom";
+import { describe, expect, it } from "vitest";
+import GlobalMap, { InteractiveMap } from "@/components/GlobalMap";
+import { TooltipProvider } from "@/context/tooltip-provider";
+import { createServer } from "@/test/fixtures";
+
+type InteractiveMapProps = Parameters[0];
+
+const now = Date.parse("2025-01-01T00:00:20.000Z");
+
+function LocationProbe() {
+ const location = useLocation();
+ return {location.pathname}
;
+}
+
+function renderMap(ui: React.ReactElement) {
+ return render(
+
+
+ {ui}
+
+
+ ,
+ );
+}
+
+function mapFeature(code: string, name: string) {
+ return {
+ type: "Feature",
+ properties: {
+ iso_a2_eh: code,
+ iso_a3_eh: `${code}A`,
+ name,
+ },
+ geometry: {
+ type: "Polygon",
+ coordinates: [
+ [
+ [-100, 30],
+ [-90, 30],
+ [-90, 40],
+ [-100, 40],
+ [-100, 30],
+ ],
+ ],
+ },
+ };
+}
+
+describe("GlobalMap", () => {
+ it("counts unique countries and ignores servers without country codes", () => {
+ Object.assign(window, { CustomBackgroundImage: "/background.png" });
+
+ const { container } = renderMap(
+ ,
+ );
+
+ expect(
+ screen.getByText(/map\.Distributions 2 map\.Regions/),
+ ).toBeInTheDocument();
+ expect(container.querySelector("section")).toHaveClass("bg-card/70");
+ });
+});
+
+describe("InteractiveMap", () => {
+ it("shows country tooltips, navigates from servers, and clears tooltip state", async () => {
+ const user = userEvent.setup();
+ const filteredFeatures = [
+ mapFeature("US", "United States"),
+ mapFeature("DE", "Germany"),
+ ] as unknown as InteractiveMapProps["filteredFeatures"];
+
+ const { container } = renderMap(
+ ,
+ );
+
+ const highlightedCountry = container.querySelector("path.fill-green-700");
+ expect(highlightedCountry).toBeInTheDocument();
+
+ fireEvent.mouseEnter(highlightedCountry as SVGPathElement);
+
+ expect(await screen.findByText("United States")).toBeInTheDocument();
+ expect(screen.getByText("2 map.Servers")).toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: /us-offline/ }));
+
+ expect(sessionStorage.getItem("fromMainPage")).toBe("true");
+ expect(screen.getByText("/server/12")).toBeInTheDocument();
+
+ const fallbackMarker = container.querySelector("circle.fill-sky-700");
+ expect(fallbackMarker).toBeInTheDocument();
+
+ fireEvent.mouseEnter(fallbackMarker as SVGCircleElement);
+
+ expect(await screen.findByText("Singapore")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /sg-edge/ })).toBeInTheDocument();
+
+ fireEvent.mouseLeave(container.querySelector(".relative") as HTMLElement);
+
+ expect(screen.queryByText("Singapore")).not.toBeInTheDocument();
+ });
+
+ it("clears tooltip state for non-highlighted countries and empty map areas", () => {
+ const filteredFeatures = [
+ mapFeature("US", "United States"),
+ mapFeature("DE", "Germany"),
+ ] as unknown as InteractiveMapProps["filteredFeatures"];
+
+ const { container } = renderMap(
+ ,
+ );
+
+ const [highlightedCountry, neutralCountry] = Array.from(
+ container.querySelectorAll("path"),
+ );
+ fireEvent.mouseEnter(highlightedCountry as SVGPathElement);
+ expect(screen.getByText("United States")).toBeInTheDocument();
+
+ fireEvent.mouseEnter(neutralCountry as SVGPathElement);
+ expect(screen.queryByText("United States")).not.toBeInTheDocument();
+
+ fireEvent.mouseEnter(container.querySelector("rect") as SVGRectElement);
+ expect(screen.queryByText("United States")).not.toBeInTheDocument();
+ });
+});
diff --git a/src/test/components/header.test.tsx b/src/test/components/header.test.tsx
new file mode 100644
index 0000000..ce3ccf2
--- /dev/null
+++ b/src/test/components/header.test.tsx
@@ -0,0 +1,252 @@
+import { QueryClientProvider } from "@tanstack/react-query";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import type { ReactElement } from "react";
+import { MemoryRouter, useLocation } from "react-router-dom";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import Header, { RefreshToast } from "@/components/Header";
+import { ThemeProvider } from "@/components/ThemeProvider";
+import { CommandProvider } from "@/context/command-provider";
+import { createSettingResponse } from "@/test/fixtures";
+import { createTestQueryClient } from "@/test/utils";
+
+const headerMocks = vi.hoisted(() => ({
+ backgroundImage: undefined as string | undefined,
+ connected: true,
+ fetchLoginUser: vi.fn(),
+ fetchSetting: vi.fn(),
+ lastData: null as { now: number; online?: number; servers: [] } | null,
+ needReconnect: false,
+ setNeedReconnect: vi.fn(),
+ updateBackground: vi.fn(),
+}));
+
+vi.mock("@/hooks/use-background", () => ({
+ useBackground: () => ({
+ backgroundImage: headerMocks.backgroundImage,
+ updateBackground: headerMocks.updateBackground,
+ }),
+}));
+
+vi.mock("@/hooks/use-websocket-context", () => ({
+ useWebSocketContext: () => ({
+ connected: headerMocks.connected,
+ lastData: headerMocks.lastData,
+ needReconnect: headerMocks.needReconnect,
+ setNeedReconnect: headerMocks.setNeedReconnect,
+ }),
+}));
+
+vi.mock("@/lib/nezha-api", () => ({
+ fetchLoginUser: headerMocks.fetchLoginUser,
+ fetchSetting: headerMocks.fetchSetting,
+}));
+
+function settingResponse(siteName = "Nezha") {
+ return {
+ ...createSettingResponse(),
+ data: {
+ ...createSettingResponse().data,
+ config: {
+ ...createSettingResponse().data.config,
+ site_name: siteName,
+ },
+ },
+ };
+}
+
+function loginResponse() {
+ return {
+ success: true,
+ data: {
+ id: 1,
+ username: "admin",
+ password: "",
+ created_at: "2025-01-01T00:00:00.000Z",
+ updated_at: "2025-01-01T00:00:00.000Z",
+ },
+ };
+}
+
+function LocationProbe() {
+ const location = useLocation();
+ return {location.pathname}
;
+}
+
+function renderHeader(route = "/server/1") {
+ return render(
+
+
+
+
+
+
+
+
+
+ ,
+ );
+}
+
+function renderInRouter(ui: ReactElement) {
+ return render({ui});
+}
+
+describe("Header", () => {
+ beforeEach(() => {
+ headerMocks.backgroundImage = undefined;
+ headerMocks.connected = true;
+ headerMocks.fetchLoginUser.mockReset();
+ headerMocks.fetchSetting.mockReset();
+ headerMocks.lastData = {
+ now: Date.parse("2025-01-01T00:00:20.000Z"),
+ online: 4,
+ servers: [],
+ };
+ headerMocks.needReconnect = false;
+ headerMocks.setNeedReconnect.mockReset();
+ headerMocks.updateBackground.mockReset();
+ Object.assign(window, {
+ CustomBackgroundImage: "",
+ CustomDesc: "",
+ CustomLinks: "",
+ CustomLogo: "",
+ CustomMobileBackgroundImage: "",
+ });
+ headerMocks.fetchSetting.mockResolvedValue(settingResponse());
+ headerMocks.fetchLoginUser.mockRejectedValue(new Error("anonymous"));
+ });
+
+ it("renders configured site identity, custom links, online count, and dashboard state", async () => {
+ const user = userEvent.setup();
+ Object.assign(window, {
+ CustomDesc: "edge status",
+ CustomLinks: JSON.stringify([
+ { link: "https://example.test", name: "Docs" },
+ ]),
+ CustomLogo: "/logo.png",
+ });
+ Object.defineProperty(document, "cookie", {
+ configurable: true,
+ value: "session=1",
+ });
+ sessionStorage.setItem("selectedGroup", "Edge");
+ headerMocks.fetchSetting.mockResolvedValue(settingResponse("Status Hub"));
+ headerMocks.fetchLoginUser.mockResolvedValue(loginResponse());
+
+ renderHeader();
+
+ const siteName = await screen.findByText("Status Hub");
+ expect(screen.getByText("edge status")).toBeInTheDocument();
+ expect(screen.getByAltText("apple-touch-icon")).toHaveAttribute(
+ "src",
+ "/logo.png",
+ );
+ expect(screen.getAllByRole("link", { name: "Docs" })).toHaveLength(2);
+ expect(await screen.findAllByText("dashboard")).toHaveLength(2);
+ expect(screen.getByText("online").closest("button")).toHaveTextContent("4");
+ expect(screen.getByText("online")).toBeInTheDocument();
+
+ await waitFor(() => {
+ expect(document.title).toBe("Status Hub");
+ });
+ expect(document.querySelector("link[rel='shortcut icon']")).toHaveAttribute(
+ "href",
+ "/logo.png",
+ );
+
+ await user.click(siteName);
+
+ expect(sessionStorage.getItem("selectedGroup")).toBeNull();
+ expect(screen.getByText("/")).toBeInTheDocument();
+ });
+
+ it("uses the offline display and login links when websocket and auth are unavailable", async () => {
+ headerMocks.connected = false;
+
+ const { container } = renderHeader();
+
+ expect(await screen.findAllByText("login")).toHaveLength(2);
+ expect(screen.getByText("offline")).toBeInTheDocument();
+ expect(
+ container.querySelector("[data-visible='true']"),
+ ).toBeInTheDocument();
+ });
+
+ it("ignores invalid custom links instead of crashing", async () => {
+ Object.assign(window, {
+ CustomLinks: "{bad-json",
+ });
+
+ renderHeader();
+
+ expect(await screen.findByText("Nezha")).toBeInTheDocument();
+ expect(
+ screen.queryByRole("link", { name: "Docs" }),
+ ).not.toBeInTheDocument();
+ });
+
+ it("stores and removes the active custom background", async () => {
+ const user = userEvent.setup();
+ headerMocks.backgroundImage = "/desktop.png";
+ Object.assign(window, {
+ CustomBackgroundImage: "/desktop.png",
+ CustomMobileBackgroundImage: "/mobile.png",
+ });
+
+ const { container } = renderHeader();
+ await screen.findByText("Nezha");
+
+ const toggleButton = container
+ .querySelector(".lucide-image-minus")
+ ?.closest("button");
+ expect(toggleButton).toBeInTheDocument();
+
+ await user.click(toggleButton as HTMLButtonElement);
+
+ expect(sessionStorage.getItem("savedBackgroundImage")).toBe("/desktop.png");
+ expect(headerMocks.updateBackground).toHaveBeenCalledWith(undefined);
+ });
+
+ it("restores the saved custom background", async () => {
+ const user = userEvent.setup();
+ sessionStorage.setItem("savedBackgroundImage", "/saved.png");
+
+ const { container } = renderHeader();
+ await screen.findByText("Nezha");
+
+ const toggleButton = container
+ .querySelector(".lucide-image-minus")
+ ?.closest("button");
+ expect(toggleButton).toBeInTheDocument();
+
+ await user.click(toggleButton as HTMLButtonElement);
+
+ expect(headerMocks.updateBackground).toHaveBeenCalledWith("/saved.png");
+ });
+});
+
+describe("RefreshToast", () => {
+ beforeEach(() => {
+ headerMocks.needReconnect = false;
+ });
+
+ it("renders only while reconnect refresh is needed", () => {
+ vi.useFakeTimers();
+ sessionStorage.setItem("needRefresh", "true");
+
+ const { container, rerender } = renderInRouter();
+
+ expect(container).toBeEmptyDOMElement();
+
+ headerMocks.needReconnect = true;
+ rerender(
+
+
+ ,
+ );
+
+ expect(screen.getByText("refreshing...")).toBeInTheDocument();
+ expect(sessionStorage.getItem("needRefresh")).toBeNull();
+ });
+});
diff --git a/src/test/components/info-components.test.tsx b/src/test/components/info-components.test.tsx
new file mode 100644
index 0000000..886870b
--- /dev/null
+++ b/src/test/components/info-components.test.tsx
@@ -0,0 +1,232 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { useEffect } from "react";
+import { MemoryRouter, useLocation } from "react-router-dom";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import BillingInfo from "@/components/billingInfo";
+import MapTooltip from "@/components/MapTooltip";
+import PlanInfo from "@/components/PlanInfo";
+import ServerFlag from "@/components/ServerFlag";
+import { TooltipProvider } from "@/context/tooltip-provider";
+import { useTooltip } from "@/hooks/use-tooltip";
+import type { PublicNoteData } from "@/lib/utils";
+
+const planData: PublicNoteData = {
+ planDataMod: {
+ bandwidth: "1Gbps",
+ trafficVol: "2TB",
+ trafficType: "monthly",
+ IPv4: "1",
+ IPv6: "1",
+ networkRoute: "CN2,CMI",
+ extra: "Premium,Backup",
+ },
+};
+
+function TooltipSeeder() {
+ const { setTooltipData } = useTooltip();
+
+ useEffect(() => {
+ setTooltipData({
+ centroid: [12, 34],
+ country: "China",
+ count: 2,
+ servers: [
+ { id: 1, name: "edge-1", status: true },
+ { id: 2, name: "edge-2", status: false },
+ ],
+ });
+ }, [setTooltipData]);
+
+ return null;
+}
+
+function LocationProbe() {
+ const location = useLocation();
+ return {location.pathname}
;
+}
+
+describe("PlanInfo and BillingInfo", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2025-01-15T00:00:00.000Z"));
+ });
+
+ it("renders plan badges for every supported public note field", () => {
+ render();
+
+ expect(screen.getByText("1Gbps")).toBeInTheDocument();
+ expect(screen.getByText("2TB")).toBeInTheDocument();
+ expect(screen.getByText("IPv4")).toBeInTheDocument();
+ expect(screen.getByText("IPv6")).toBeInTheDocument();
+ expect(screen.getByText("CN2|CMI")).toBeInTheDocument();
+ expect(screen.getByText("Premium")).toBeInTheDocument();
+ expect(screen.getByText("Backup")).toBeInTheDocument();
+ });
+
+ it("renders active, free, usage-based, indefinite, and expired billing states", () => {
+ const { rerender } = render(
+ ,
+ );
+
+ expect(
+ screen.getByText("billingInfo.price: 10/monthly"),
+ ).toBeInTheDocument();
+ expect(screen.getByText(/billingInfo.remaining: 16/)).toBeInTheDocument();
+ expect(screen.getByRole("progressbar")).toBeInTheDocument();
+
+ rerender(
+ ,
+ );
+ expect(screen.getByText("billingInfo.free")).toBeInTheDocument();
+
+ rerender(
+ ,
+ );
+ expect(screen.getByText("billingInfo.usage-baseed")).toBeInTheDocument();
+
+ rerender(
+ ,
+ );
+ expect(screen.getByText(/billingInfo.indefinite/)).toBeInTheDocument();
+
+ rerender(
+ ,
+ );
+ expect(screen.getByText(/billingInfo.expired/)).toBeInTheDocument();
+ });
+});
+
+describe("MapTooltip", () => {
+ it("renders tooltip data and navigates to selected servers", async () => {
+ const user = userEvent.setup();
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+ expect(await screen.findByText("Mainland China")).toBeInTheDocument();
+ expect(screen.getByTestId("map-tooltip")).toHaveStyle({
+ transform: "translate(20%, 8px)",
+ });
+ expect(screen.getByText("2 map.Servers")).toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: /edge-2/ }));
+
+ expect(sessionStorage.getItem("fromMainPage")).toBe("true");
+ expect(screen.getByText("/server/2")).toBeInTheDocument();
+ });
+});
+
+describe("ServerFlag", () => {
+ it("uses SVG flag classes when emoji flags are forced off", () => {
+ Object.assign(window, { ForceUseSvgFlag: true });
+ const { container } = render();
+
+ expect(container.querySelector(".fi-us")).toBeInTheDocument();
+ });
+
+ it("normalizes SVG flag classes and ignores invalid country codes", () => {
+ Object.assign(window, { ForceUseSvgFlag: true });
+ const { container, rerender } = render();
+
+ expect(container.querySelector(".fi-us")).toBeInTheDocument();
+
+ rerender();
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("uses emoji flags when the canvas probe detects support", async () => {
+ Object.assign(window, { ForceUseSvgFlag: false });
+ const originalCreateElement = document.createElement.bind(document);
+ const canvasContext = {
+ fillStyle: "",
+ textBaseline: "",
+ font: "",
+ fillText: vi.fn(),
+ getImageData: vi.fn(() => ({
+ data: new Uint8ClampedArray([0, 0, 0, 255]),
+ })),
+ } as unknown as CanvasRenderingContext2D;
+ vi.spyOn(document, "createElement").mockImplementation(
+ (tagName, options) => {
+ if (tagName === "canvas") {
+ return {
+ getContext: vi.fn(() => canvasContext),
+ } as unknown as HTMLCanvasElement;
+ }
+
+ return originalCreateElement(tagName, options);
+ },
+ );
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(container).toHaveTextContent("🇺🇸");
+ });
+ expect(container.querySelector(".fi-US")).not.toBeInTheDocument();
+ });
+
+ it("renders nothing for missing country codes", () => {
+ Object.assign(window, { ForceUseSvgFlag: true });
+ const { container } = render();
+
+ expect(container).toBeEmptyDOMElement();
+ });
+});
diff --git a/src/test/components/navigation-and-footer.test.tsx b/src/test/components/navigation-and-footer.test.tsx
new file mode 100644
index 0000000..86c32eb
--- /dev/null
+++ b/src/test/components/navigation-and-footer.test.tsx
@@ -0,0 +1,160 @@
+import { QueryClientProvider } from "@tanstack/react-query";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { MemoryRouter, useLocation } from "react-router-dom";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import Footer from "@/components/Footer";
+import GroupSwitch from "@/components/GroupSwitch";
+import {
+ ServerDetailChartLoading,
+ ServerDetailLoading,
+} from "@/components/loading/ServerDetailLoading";
+import TabSwitch from "@/components/TabSwitch";
+import { createTestQueryClient, renderWithProviders } from "@/test/utils";
+
+const apiMocks = vi.hoisted(() => ({
+ fetchSetting: vi.fn(),
+}));
+
+vi.mock("@/lib/nezha-api", () => apiMocks);
+
+function LocationProbe() {
+ const location = useLocation();
+ return {location.pathname}
;
+}
+
+describe("Footer", () => {
+ beforeEach(() => {
+ apiMocks.fetchSetting.mockResolvedValue({
+ success: true,
+ data: {
+ config: {
+ debug: false,
+ language: "en-US",
+ site_name: "Nezha",
+ user_template: "",
+ admin_template: "",
+ custom_code: "",
+ },
+ version: "9.9.9",
+ },
+ });
+ });
+
+ it("renders setting version and project attribution", async () => {
+ renderWithProviders();
+
+ expect(screen.getByText("footer.themeBy")).toBeInTheDocument();
+ expect(screen.getByText("nezha-dash")).toHaveAttribute(
+ "href",
+ "https://github.com/hamster1963/nezha-dash",
+ );
+ expect(await screen.findByText("9.9.9")).toBeInTheDocument();
+ });
+});
+
+describe("Tab and group switches", () => {
+ it("switches tabs and marks the active tab", async () => {
+ const user = userEvent.setup();
+ const setCurrentTab = vi.fn();
+
+ render(
+ ,
+ );
+
+ await user.click(screen.getByText("tabSwitch.Network"));
+
+ expect(setCurrentTab).toHaveBeenCalledWith("Network");
+ expect(
+ screen.getByText("tabSwitch.Detail").parentElement?.parentElement,
+ ).toHaveClass("text-black");
+ });
+
+ it("restores saved groups and hides itself when only All exists", async () => {
+ const setCurrentTab = vi.fn();
+ sessionStorage.setItem("selectedGroup", "Edge");
+
+ const { container, rerender } = render(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(setCurrentTab).toHaveBeenCalledWith("Edge");
+ });
+
+ rerender(
+ ,
+ );
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("scrolls overflowing group tabs and applies custom background styling", async () => {
+ const user = userEvent.setup();
+ const setCurrentTab = vi.fn();
+ const scrollIntoView = vi.spyOn(HTMLElement.prototype, "scrollIntoView");
+ vi.spyOn(HTMLElement.prototype, "scrollWidth", "get").mockReturnValue(320);
+ vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(120);
+ window.CustomBackgroundImage = "/background.jpg";
+
+ const { container } = render(
+ ,
+ );
+
+ const scrollContainer = container.querySelector(
+ ".scrollbar-hidden",
+ ) as HTMLElement;
+
+ fireEvent.wheel(scrollContainer, { deltaY: 42 });
+ await user.click(screen.getByText("Asia"));
+
+ expect(scrollContainer.scrollLeft).toBe(42);
+ expect(setCurrentTab).toHaveBeenCalledWith("Asia");
+ expect(scrollIntoView).not.toHaveBeenCalled();
+ expect(
+ container.querySelector(".relative.flex.items-center")?.className,
+ ).toContain("bg-stone-100/70");
+ });
+});
+
+describe("server detail loading states", () => {
+ it("renders chart skeleton cards", () => {
+ const { container } = render();
+
+ expect(container.querySelectorAll(".h-\\[182px\\]")).toHaveLength(6);
+ });
+
+ it("navigates home from the detail loading back affordance", async () => {
+ const user = userEvent.setup();
+ const queryClient = createTestQueryClient();
+
+ render(
+
+
+
+
+
+ ,
+ );
+
+ await user.click(screen.getAllByAltText("BackIcon")[0]);
+
+ expect(screen.getByText("/")).toBeInTheDocument();
+ });
+});
diff --git a/src/test/components/network-chart.test.tsx b/src/test/components/network-chart.test.tsx
new file mode 100644
index 0000000..7c3c677
--- /dev/null
+++ b/src/test/components/network-chart.test.tsx
@@ -0,0 +1,287 @@
+import { QueryClientProvider } from "@tanstack/react-query";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import type { ReactElement, ReactNode } from "react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { NetworkChart, NetworkChartClient } from "@/components/NetworkChart";
+import type { ChartConfig } from "@/components/ui/chart";
+import { createTestQueryClient } from "@/test/utils";
+import type { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api";
+
+const apiMocks = vi.hoisted(() => ({
+ fetchLoginUser: vi.fn(),
+ fetchMonitor: vi.fn(),
+}));
+
+vi.mock("@/lib/nezha-api", () => ({
+ fetchLoginUser: apiMocks.fetchLoginUser,
+ fetchMonitor: apiMocks.fetchMonitor,
+}));
+
+vi.mock("recharts", () => {
+ const createElement =
+ (testId: string) =>
+ ({
+ children,
+ data,
+ dataKey,
+ }: {
+ children?: ReactNode;
+ data?: unknown[];
+ dataKey?: string;
+ }) => (
+
+ {children}
+
+ );
+
+ const ComposedChart = createElement("composed-chart");
+ const genericChart = createElement("generic-chart");
+
+ return {
+ Area: createElement("area"),
+ AreaChart: genericChart,
+ BarChart: genericChart,
+ CartesianGrid: createElement("grid"),
+ ComposedChart,
+ FunnelChart: genericChart,
+ Legend: ({ content }: { content?: ReactNode }) => (
+ {content}
+ ),
+ Line: createElement("line"),
+ LineChart: genericChart,
+ PieChart: genericChart,
+ RadarChart: genericChart,
+ RadialBarChart: genericChart,
+ ResponsiveContainer: ({ children }: { children?: ReactNode }) => (
+ {children}
+ ),
+ Sankey: genericChart,
+ ScatterChart: genericChart,
+ Tooltip: ({ content }: { content?: ReactNode }) => (
+ {content}
+ ),
+ Treemap: genericChart,
+ XAxis: createElement("x-axis"),
+ YAxis: createElement("y-axis"),
+ };
+});
+
+const times = Array.from(
+ { length: 12 },
+ (_, index) => Date.parse("2025-01-01T00:00:00.000Z") + index * 60 * 60 * 1000,
+);
+
+const monitorData: NezhaMonitor[] = [
+ {
+ monitor_id: 2,
+ monitor_name: "Beta",
+ display_index: 1,
+ server_id: 7,
+ server_name: "edge-chart",
+ created_at: times,
+ avg_delay: [15, 18, 0, 45, 48, 52, 4000, 65, 70, 75, 80, 85],
+ },
+ {
+ monitor_id: 1,
+ monitor_name: "Alpha",
+ display_index: 3,
+ server_id: 7,
+ server_name: "edge-chart",
+ created_at: times,
+ avg_delay: [30, 32, 35, 36, 38, 40, 42, 44, 46, 48, 50, 52],
+ packet_loss: [0, 0, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5],
+ },
+];
+
+const clientChartData: ServerMonitorChart = {
+ Alpha: times.map((created_at, index) => ({
+ created_at,
+ avg_delay: 30 + index,
+ packet_loss: index,
+ })),
+ Beta: times.map((created_at, index) => ({
+ created_at,
+ avg_delay: 60 + index,
+ packet_loss: index % 2,
+ })),
+};
+
+const clientFormattedData = times.map((created_at, index) => ({
+ created_at,
+ Alpha: 30 + index,
+ Alpha_packet_loss: index,
+ Beta: 60 + index,
+ Beta_packet_loss: index % 2,
+}));
+
+const chartConfig = {
+ avg_delay: { label: "monitor.avgDelay" },
+ Alpha: { label: "Alpha" },
+ Beta: { label: "Beta" },
+} satisfies ChartConfig;
+
+function loginResponse() {
+ return {
+ success: true,
+ data: {
+ id: 1,
+ username: "admin",
+ password: "",
+ created_at: "2025-01-01T00:00:00.000Z",
+ updated_at: "2025-01-01T00:00:00.000Z",
+ },
+ };
+}
+
+function renderWithQuery(ui: ReactElement) {
+ return render(
+
+ {ui}
+ ,
+ );
+}
+
+describe("NetworkChart", () => {
+ beforeEach(() => {
+ apiMocks.fetchLoginUser.mockReset();
+ apiMocks.fetchMonitor.mockReset();
+ apiMocks.fetchLoginUser.mockRejectedValue(new Error("anonymous"));
+ Object.defineProperty(document, "cookie", {
+ configurable: true,
+ value: "",
+ });
+ });
+
+ it("renders the loading state while monitor data is unavailable", () => {
+ apiMocks.fetchMonitor.mockReturnValue(new Promise(() => undefined));
+
+ const { container } = renderWithQuery(
+ ,
+ );
+
+ expect(container.querySelector(".h-\\[250px\\]")).toBeInTheDocument();
+ expect(apiMocks.fetchMonitor).not.toHaveBeenCalled();
+ });
+
+ it("renders the no-data state from the monitor API", async () => {
+ apiMocks.fetchMonitor.mockResolvedValue({
+ success: true,
+ data: null,
+ });
+
+ renderWithQuery();
+
+ expect(await screen.findByText("monitor.noData")).toBeInTheDocument();
+ });
+
+ it("fetches monitor data, transforms chart series, and allows logged-in period changes", async () => {
+ const user = userEvent.setup();
+ Object.defineProperty(document, "cookie", {
+ configurable: true,
+ value: "session=1",
+ });
+ apiMocks.fetchLoginUser.mockResolvedValue(loginResponse());
+ apiMocks.fetchMonitor.mockResolvedValue({
+ success: true,
+ data: monitorData,
+ });
+
+ renderWithQuery();
+
+ expect(await screen.findByText("edge-chart")).toBeInTheDocument();
+ expect(apiMocks.fetchMonitor).toHaveBeenCalledWith(7, "1d");
+ expect(screen.getByText("2 monitor.monitorCount")).toBeInTheDocument();
+ expect(screen.getByText("Alpha")).toBeInTheDocument();
+ expect(screen.getByText("Beta")).toBeInTheDocument();
+ expect(screen.getByTestId("composed-chart")).toHaveAttribute(
+ "data-points",
+ "12",
+ );
+
+ await user.click(screen.getByText("monitor.period7d"));
+
+ await waitFor(() => {
+ expect(apiMocks.fetchMonitor).toHaveBeenCalledWith(7, "7d");
+ });
+ });
+});
+
+describe("NetworkChartClient", () => {
+ it("locks longer periods for anonymous users and manages chart selection state", async () => {
+ const user = userEvent.setup();
+ const onPeriodChange = vi.fn();
+
+ render(
+ ,
+ );
+
+ expect(screen.getByText("edge-client")).toBeInTheDocument();
+ expect(screen.getByText("2 monitor.monitorCount")).toBeInTheDocument();
+
+ await user.click(screen.getByText("monitor.period7d"));
+ expect(onPeriodChange).not.toHaveBeenCalled();
+
+ await user.click(screen.getByText("Alpha"));
+ expect(
+ screen.getByRole("button", { name: /monitor.clearSelections/ }),
+ ).toBeInTheDocument();
+ expect(screen.getByTestId("area")).toHaveAttribute(
+ "data-key",
+ "packet_loss",
+ );
+
+ await user.click(screen.getByText("Beta"));
+ expect(screen.queryByTestId("area")).not.toBeInTheDocument();
+
+ await user.click(
+ screen.getByRole("button", { name: /monitor.clearSelections/ }),
+ );
+ expect(
+ screen.queryByRole("button", { name: /monitor.clearSelections/ }),
+ ).not.toBeInTheDocument();
+ });
+
+ it("shows period loading and honors the forced peak-cut global", async () => {
+ const user = userEvent.setup();
+ Object.assign(window, { ForcePeakCutEnabled: true });
+ const onPeriodChange = vi.fn();
+
+ const { container } = render(
+ ,
+ );
+
+ expect(container.querySelector(".opacity-60")).toBeInTheDocument();
+ expect(
+ screen.getByRole("switch", { name: "monitor.peakCut" }),
+ ).toHaveAttribute("data-state", "checked");
+
+ await user.click(screen.getByText("monitor.period30d"));
+ expect(onPeriodChange).toHaveBeenCalledWith("30d");
+
+ await user.click(screen.getByRole("switch", { name: "monitor.peakCut" }));
+ expect(
+ screen.getByRole("switch", { name: "monitor.peakCut" }),
+ ).toHaveAttribute("data-state", "unchecked");
+ });
+});
diff --git a/src/test/components/server-cards.test.tsx b/src/test/components/server-cards.test.tsx
new file mode 100644
index 0000000..caafb63
--- /dev/null
+++ b/src/test/components/server-cards.test.tsx
@@ -0,0 +1,153 @@
+import { fireEvent, screen } from "@testing-library/react";
+import { useLocation } from "react-router-dom";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import ServerCard from "@/components/ServerCard";
+import ServerCardInline from "@/components/ServerCardInline";
+import { createServer } from "@/test/fixtures";
+import { renderWithProviders } from "@/test/utils";
+
+const publicNote = JSON.stringify({
+ billingDataMod: {
+ startDate: "2025-01-01T00:00:00.000Z",
+ endDate: "2025-01-31T00:00:00.000Z",
+ autoRenewal: "0",
+ cycle: "monthly",
+ amount: "10",
+ },
+ planDataMod: {
+ bandwidth: "1Gbps",
+ trafficVol: "2TB",
+ trafficType: "monthly",
+ IPv4: "1",
+ IPv6: "1",
+ networkRoute: "CN2,CMI",
+ extra: "Premium",
+ },
+});
+
+function LocationProbe() {
+ const location = useLocation();
+ return {location.pathname}
;
+}
+
+describe("ServerCard", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2025-01-15T00:00:00.000Z"));
+ Object.assign(window, {
+ ForceUseSvgFlag: true,
+ FixedTopServerName: true,
+ ShowNetTransfer: true,
+ });
+ });
+
+ it("renders online server metrics, billing, plan data, and navigates on click", async () => {
+ const server = createServer({
+ id: 7,
+ name: "edge-online",
+ public_note: publicNote,
+ host: { platform: "Windows Server" },
+ });
+
+ renderWithProviders(
+ <>
+
+
+ >,
+ );
+
+ expect(screen.getByText("edge-online")).toBeInTheDocument();
+ expect(screen.getByText("Windows")).toBeInTheDocument();
+ expect(screen.getByText("12.00%")).toBeInTheDocument();
+ expect(screen.getAllByText("25.00%")).toHaveLength(2);
+ expect(screen.getByText("serverCard.upload:2.00 GiB")).toBeInTheDocument();
+ expect(
+ screen.getByText("serverCard.download:1.00 GiB"),
+ ).toBeInTheDocument();
+ expect(screen.getByText("1Gbps")).toBeInTheDocument();
+ expect(
+ screen.getAllByText(/billingInfo.remaining: 16/).length,
+ ).toBeGreaterThan(0);
+
+ Object.defineProperty(window, "scrollY", {
+ configurable: true,
+ value: 432,
+ });
+ fireEvent.click(screen.getByText("edge-online"));
+
+ expect(sessionStorage.getItem("fromMainPage")).toBe("true");
+ expect(sessionStorage.getItem("scrollPosition")).toBe("432");
+ expect(screen.getByText("/server/7")).toBeInTheDocument();
+ });
+
+ it("renders a compact offline card without live metric blocks", () => {
+ const server = createServer({
+ id: 8,
+ name: "edge-offline",
+ public_note: publicNote,
+ last_active: "2024-12-31T23:00:00.000Z",
+ });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("edge-offline")).toBeInTheDocument();
+ expect(screen.getByText("1Gbps")).toBeInTheDocument();
+ expect(screen.queryByText("CPU")).not.toBeInTheDocument();
+ });
+});
+
+describe("ServerCardInline", () => {
+ beforeEach(() => {
+ Object.assign(window, { ForceUseSvgFlag: true });
+ });
+
+ it("renders online inline server detail columns", () => {
+ const server = createServer({
+ id: 9,
+ name: "edge-inline",
+ public_note: publicNote,
+ state: { uptime: 2 * 86_400 },
+ });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("edge-inline")).toBeInTheDocument();
+ expect(screen.getByText("serverCard.system")).toBeInTheDocument();
+ expect(screen.getByText("2 serverCard.days")).toBeInTheDocument();
+ expect(screen.getByText("2.00 GiB")).toBeInTheDocument();
+ expect(screen.getByText("1.00 GiB")).toBeInTheDocument();
+ });
+
+ it("renders offline inline server cards with saved plan data", () => {
+ const server = createServer({
+ id: 10,
+ name: "edge-inline-offline",
+ public_note: publicNote,
+ last_active: "2024-12-31T23:00:00.000Z",
+ });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("edge-inline-offline")).toBeInTheDocument();
+ expect(screen.getByText("1Gbps")).toBeInTheDocument();
+ expect(screen.queryByText("serverCard.system")).not.toBeInTheDocument();
+ });
+});
diff --git a/src/test/components/server-detail-chart.test.tsx b/src/test/components/server-detail-chart.test.tsx
new file mode 100644
index 0000000..0e5bd47
--- /dev/null
+++ b/src/test/components/server-detail-chart.test.tsx
@@ -0,0 +1,288 @@
+import { QueryClientProvider } from "@tanstack/react-query";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import type { ReactNode } from "react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import ServerDetailChart from "@/components/ServerDetailChart";
+import { createServer, createSettingResponse } from "@/test/fixtures";
+import { createTestQueryClient } from "@/test/utils";
+import type { NezhaServer, NezhaWebsocketResponse } from "@/types/nezha-api";
+
+const detailChartMocks = vi.hoisted(() => ({
+ connected: true,
+ fetchLoginUser: vi.fn(),
+ fetchServerMetrics: vi.fn(),
+ fetchSetting: vi.fn(),
+ lastData: null as NezhaWebsocketResponse | null,
+ messageHistory: [] as NezhaWebsocketResponse[],
+}));
+
+vi.mock("recharts", () => {
+ const createElement =
+ (testId: string) =>
+ ({
+ children,
+ data,
+ dataKey,
+ }: {
+ children?: ReactNode;
+ data?: unknown[];
+ dataKey?: string;
+ }) => (
+
+ {children}
+
+ );
+
+ const AreaChart = createElement("area-chart");
+ const LineChart = createElement("line-chart");
+ const genericChart = createElement("generic-chart");
+
+ return {
+ Area: createElement("area"),
+ AreaChart,
+ BarChart: genericChart,
+ CartesianGrid: createElement("grid"),
+ ComposedChart: genericChart,
+ FunnelChart: genericChart,
+ Legend: ({ content }: { content?: ReactNode }) => (
+ {content}
+ ),
+ Line: createElement("line"),
+ LineChart,
+ PieChart: genericChart,
+ RadarChart: genericChart,
+ RadialBarChart: genericChart,
+ ResponsiveContainer: ({ children }: { children?: ReactNode }) => (
+ {children}
+ ),
+ Sankey: genericChart,
+ ScatterChart: genericChart,
+ Tooltip: ({ content }: { content?: ReactNode }) => (
+ {content}
+ ),
+ Treemap: genericChart,
+ XAxis: createElement("x-axis"),
+ YAxis: createElement("y-axis"),
+ };
+});
+
+vi.mock("@/hooks/use-websocket-context", () => ({
+ useWebSocketContext: () => ({
+ connected: detailChartMocks.connected,
+ lastData: detailChartMocks.lastData,
+ messageHistory: detailChartMocks.messageHistory,
+ }),
+}));
+
+vi.mock("@/lib/nezha-api", () => ({
+ fetchLoginUser: detailChartMocks.fetchLoginUser,
+ fetchServerMetrics: detailChartMocks.fetchServerMetrics,
+ fetchSetting: detailChartMocks.fetchSetting,
+}));
+
+function settingResponse(tsdbEnabled = true) {
+ return {
+ ...createSettingResponse(),
+ data: {
+ ...createSettingResponse().data,
+ tsdb_enabled: tsdbEnabled,
+ },
+ };
+}
+
+function loginResponse() {
+ return {
+ success: true,
+ data: {
+ id: 1,
+ username: "admin",
+ password: "",
+ created_at: "2025-01-01T00:00:00.000Z",
+ updated_at: "2025-01-01T00:00:00.000Z",
+ },
+ };
+}
+
+function metricsResponse(metric: string) {
+ return {
+ success: true,
+ data: {
+ server_id: 7,
+ server_name: "edge-chart-detail",
+ metric,
+ data_points: [
+ { ts: Date.parse("2025-01-01T00:00:00.000Z"), value: 10 },
+ { ts: Date.parse("2025-01-01T01:00:00.000Z"), value: 20 },
+ { ts: Date.parse("2025-01-01T02:00:00.000Z"), value: 30 },
+ ],
+ },
+ };
+}
+
+function websocketPayload(server: NezhaServer, now: number) {
+ return {
+ now,
+ servers: [server],
+ };
+}
+
+function renderWithQuery(ui: React.ReactElement) {
+ return render(
+
+ {ui}
+ ,
+ );
+}
+
+function seedWebSocketData() {
+ const baseNow = Date.parse("2025-01-01T00:00:20.000Z");
+ const server = createServer({
+ id: 7,
+ name: "edge-chart-detail",
+ host: {
+ gpu: ["NVIDIA T4"],
+ },
+ state: {
+ cpu: 45,
+ disk_used: 180,
+ gpu: [33],
+ mem_used: 80,
+ net_in_speed: 4 * 1024 ** 2,
+ net_out_speed: 3 * 1024 ** 2,
+ process_count: 77,
+ swap_used: 25,
+ tcp_conn_count: 18,
+ udp_conn_count: 9,
+ },
+ });
+
+ detailChartMocks.connected = true;
+ detailChartMocks.lastData = websocketPayload(server, baseNow);
+ detailChartMocks.messageHistory = [0, 1, 2].map((index) =>
+ websocketPayload(
+ createServer({
+ id: 7,
+ host: {
+ gpu: ["NVIDIA T4"],
+ },
+ state: {
+ cpu: 30 + index,
+ disk_used: 100 + index * 10,
+ gpu: [20 + index],
+ mem_used: 50 + index * 5,
+ net_in_speed: (1 + index) * 1024 ** 2,
+ net_out_speed: (2 + index) * 1024 ** 2,
+ process_count: 60 + index,
+ swap_used: 10 + index,
+ tcp_conn_count: 10 + index,
+ udp_conn_count: 5 + index,
+ },
+ }),
+ baseNow - index * 1000,
+ ),
+ );
+}
+
+describe("ServerDetailChart", () => {
+ beforeEach(() => {
+ detailChartMocks.connected = true;
+ detailChartMocks.fetchLoginUser.mockReset();
+ detailChartMocks.fetchServerMetrics.mockReset();
+ detailChartMocks.fetchSetting.mockReset();
+ detailChartMocks.lastData = null;
+ detailChartMocks.messageHistory = [];
+ detailChartMocks.fetchLoginUser.mockRejectedValue(new Error("anonymous"));
+ detailChartMocks.fetchSetting.mockResolvedValue(settingResponse());
+ });
+
+ it("renders the loading grid without websocket data", () => {
+ detailChartMocks.connected = false;
+
+ const { container } = renderWithQuery();
+
+ expect(container.querySelectorAll(".h-\\[182px\\]")).toHaveLength(6);
+ });
+
+ it("renders realtime resource, network, connection, and GPU charts", async () => {
+ const user = userEvent.setup();
+ seedWebSocketData();
+
+ renderWithQuery();
+
+ expect(
+ await screen.findByText("serverDetailChart.realtime"),
+ ).toBeInTheDocument();
+ expect(screen.getByText("serverDetailChart.period1d")).toBeInTheDocument();
+ expect(screen.getByText("serverDetailChart.period7d")).toBeInTheDocument();
+ expect(screen.getByText("CPU")).toBeInTheDocument();
+ expect(screen.getByText("GPU: NVIDIA T4")).toBeInTheDocument();
+ expect(screen.getByText("serverDetailChart.mem")).toBeInTheDocument();
+ expect(screen.getByText("serverDetailChart.swap")).toBeInTheDocument();
+ expect(screen.getByText("serverDetailChart.disk")).toBeInTheDocument();
+ expect(screen.getByText("serverDetailChart.process")).toBeInTheDocument();
+ expect(screen.getByText("serverDetailChart.upload")).toBeInTheDocument();
+ expect(screen.getByText("serverDetailChart.download")).toBeInTheDocument();
+ expect(screen.getByText("TCP")).toBeInTheDocument();
+ expect(screen.getByText("UDP")).toBeInTheDocument();
+ expect(screen.getAllByTestId("area-chart").length).toBeGreaterThan(0);
+ expect(screen.getAllByTestId("line-chart").length).toBeGreaterThan(0);
+
+ await user.click(screen.getByText("serverDetailChart.period7d"));
+
+ expect(detailChartMocks.fetchServerMetrics).not.toHaveBeenCalled();
+ });
+
+ it("prevents historical periods when TSDB is disabled", async () => {
+ const user = userEvent.setup();
+ seedWebSocketData();
+ detailChartMocks.fetchSetting.mockResolvedValue(settingResponse(false));
+
+ renderWithQuery();
+
+ await screen.findByText("serverDetailChart.realtime");
+ await user.click(screen.getByText("serverDetailChart.period1d"));
+
+ expect(detailChartMocks.fetchServerMetrics).not.toHaveBeenCalled();
+ });
+
+ it("fetches every historical metric group for the selected period", async () => {
+ const user = userEvent.setup();
+ seedWebSocketData();
+ Object.defineProperty(document, "cookie", {
+ configurable: true,
+ value: "session=1",
+ });
+ detailChartMocks.fetchLoginUser.mockResolvedValue(loginResponse());
+ detailChartMocks.fetchServerMetrics.mockImplementation(
+ (_serverId: number, metric: string) =>
+ Promise.resolve(metricsResponse(metric)),
+ );
+
+ renderWithQuery();
+
+ await screen.findByText("serverDetailChart.realtime");
+ await user.click(screen.getByText("serverDetailChart.period1d"));
+
+ for (const metric of [
+ "cpu",
+ "gpu",
+ "memory",
+ "swap",
+ "disk",
+ "process_count",
+ "net_out_speed",
+ "net_in_speed",
+ "tcp_conn",
+ "udp_conn",
+ ]) {
+ await waitFor(() => {
+ expect(detailChartMocks.fetchServerMetrics).toHaveBeenCalledWith(
+ 7,
+ metric,
+ "1d",
+ );
+ });
+ }
+ });
+});
diff --git a/src/test/components/server-detail-components.test.tsx b/src/test/components/server-detail-components.test.tsx
new file mode 100644
index 0000000..c687be6
--- /dev/null
+++ b/src/test/components/server-detail-components.test.tsx
@@ -0,0 +1,143 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { MemoryRouter, useLocation } from "react-router-dom";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import ServerDetailOverview from "@/components/ServerDetailOverview";
+import ServerDetailSummary from "@/components/ServerDetailSummary";
+import { createServer } from "@/test/fixtures";
+
+const websocketMocks = vi.hoisted(() => ({
+ connected: true,
+ lastData: null as {
+ now: number;
+ servers: ReturnType[];
+ } | null,
+}));
+
+vi.mock("@/hooks/use-websocket-context", () => ({
+ useWebSocketContext: () => websocketMocks,
+}));
+
+function seedWebSocketData({
+ server = createServer(),
+ now = Date.parse("2025-01-01T00:00:20.000Z"),
+} = {}) {
+ websocketMocks.connected = true;
+ websocketMocks.lastData = {
+ now,
+ servers: [server],
+ };
+}
+
+function LocationProbe() {
+ const location = useLocation();
+ return {location.pathname}
;
+}
+
+describe("ServerDetailSummary", () => {
+ beforeEach(() => {
+ websocketMocks.connected = true;
+ websocketMocks.lastData = null;
+ });
+
+ it("renders nothing until websocket data exists", () => {
+ websocketMocks.connected = false;
+
+ const { container } = render();
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("renders resource, network, and connection summaries for the selected server", () => {
+ seedWebSocketData();
+
+ render();
+
+ expect(screen.getByText("12.00%")).toBeInTheDocument();
+ expect(screen.getAllByText("25.00%")).toHaveLength(2);
+ expect(screen.getByText("Process")).toBeInTheDocument();
+ expect(screen.getByText("88")).toBeInTheDocument();
+ expect(screen.getByText("TCP")).toBeInTheDocument();
+ expect(screen.getByText("8")).toBeInTheDocument();
+ expect(screen.getByText("UDP")).toBeInTheDocument();
+ expect(screen.getByText("4")).toBeInTheDocument();
+ expect(screen.getByText("1.00M/s")).toBeInTheDocument();
+ expect(screen.getByText("2.00M/s")).toBeInTheDocument();
+ });
+});
+
+describe("ServerDetailOverview", () => {
+ beforeEach(() => {
+ websocketMocks.connected = true;
+ websocketMocks.lastData = null;
+ Object.assign(window, { ForceUseSvgFlag: true });
+ });
+
+ it("shows the loading shell when websocket data or the selected server is missing", () => {
+ websocketMocks.connected = false;
+ const { rerender } = render(
+
+
+ ,
+ );
+
+ expect(screen.getAllByAltText("BackIcon").length).toBeGreaterThan(0);
+
+ seedWebSocketData();
+ rerender(
+
+
+ ,
+ );
+
+ expect(screen.getAllByAltText("BackIcon").length).toBeGreaterThan(0);
+ });
+
+ it("renders server identity, hardware, traffic, and temperature details", async () => {
+ const user = userEvent.setup();
+ sessionStorage.setItem("fromMainPage", "true");
+ seedWebSocketData({
+ server: createServer({
+ id: 7,
+ name: "edge-detail",
+ country_code: "us",
+ host: {
+ gpu: ["NVIDIA T4"],
+ },
+ state: {
+ temperatures: [{ Name: "CPU Core", Temperature: 55.5 }],
+ },
+ }),
+ });
+
+ const { unmount } = render(
+
+
+
+ ,
+ );
+
+ expect(screen.getByText("edge-detail")).toBeInTheDocument();
+ expect(screen.getByText("serverDetail.online")).toBeInTheDocument();
+ expect(screen.getByText("1.0.0")).toBeInTheDocument();
+ expect(screen.getByText("amd64")).toBeInTheDocument();
+ expect(screen.getByText("US")).toBeInTheDocument();
+ expect(screen.getByText(/linux - 6.8/)).toBeInTheDocument();
+ expect(screen.getByText(/AMD EPYC/)).toBeInTheDocument();
+ expect(screen.getByText("NVIDIA T4")).toBeInTheDocument();
+ expect(screen.getByText("2.00 GiB")).toBeInTheDocument();
+ expect(screen.getByText("1.00 GiB")).toBeInTheDocument();
+
+ await user.click(
+ screen.getByRole("button", { name: /serverDetail.temperature/ }),
+ );
+ expect(screen.getByText("CPU Core")).toBeInTheDocument();
+ expect(screen.getByText(/55.50 °C/)).toBeInTheDocument();
+
+ await user.click(screen.getByText("edge-detail"));
+ expect(screen.getByText("/")).toBeInTheDocument();
+
+ unmount();
+ expect(sessionStorage.getItem("fromMainPage")).toBeNull();
+ });
+});
diff --git a/src/test/components/service-tracker.test.tsx b/src/test/components/service-tracker.test.tsx
new file mode 100644
index 0000000..49098b7
--- /dev/null
+++ b/src/test/components/service-tracker.test.tsx
@@ -0,0 +1,274 @@
+import { QueryClientProvider } from "@tanstack/react-query";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import { CycleTransferStatsCard } from "@/components/CycleTransferStats";
+import CycleTransferStatsClient from "@/components/CycleTransferStatsClient";
+import { ServiceTracker } from "@/components/ServiceTracker";
+import ServiceTrackerClient from "@/components/ServiceTrackerClient";
+import { createServer } from "@/test/fixtures";
+import { createTestQueryClient } from "@/test/utils";
+
+const apiMocks = vi.hoisted(() => ({
+ fetchService: vi.fn(),
+}));
+
+vi.mock("@/lib/nezha-api", () => apiMocks);
+
+function renderWithQuery(ui: React.ReactElement) {
+ return render(
+
+ {ui}
+ ,
+ );
+}
+
+describe("ServiceTracker", () => {
+ it("shows the loading state while service data is pending", () => {
+ apiMocks.fetchService.mockReturnValue(new Promise(() => undefined));
+
+ renderWithQuery();
+
+ expect(screen.getByText("serviceTracker.loading")).toBeInTheDocument();
+ });
+
+ it("shows an empty state when there are no services or cycle stats", async () => {
+ apiMocks.fetchService.mockResolvedValue({
+ success: true,
+ data: {},
+ });
+
+ renderWithQuery();
+
+ expect(
+ await screen.findByText("serviceTracker.noService"),
+ ).toBeInTheDocument();
+ });
+
+ it("renders processed service uptime, delay, and matching cycle transfer stats", async () => {
+ apiMocks.fetchService.mockResolvedValue({
+ success: true,
+ data: {
+ services: {
+ http: {
+ service_name: "HTTP Ping",
+ current_up: 0,
+ current_down: 0,
+ total_up: 5,
+ total_down: 3,
+ delay: [80, 120, 450],
+ up: [3, 2, 0],
+ down: [0, 1, 2],
+ },
+ },
+ cycle_transfer_stats: {
+ monthly: {
+ name: "Monthly",
+ from: "2025-01-01T00:00:00.000Z",
+ to: "2025-02-01T00:00:00.000Z",
+ max: 10 * 1024,
+ min: 0,
+ server_name: {
+ "1": "edge-1",
+ "2": "hidden-server",
+ },
+ transfer: {
+ "1": 5 * 1024,
+ "2": 1024,
+ },
+ next_update: {
+ "1": "2025-01-15T12:00:00.000Z",
+ "2": "2025-01-15T12:00:00.000Z",
+ },
+ },
+ },
+ },
+ });
+
+ renderWithQuery();
+
+ expect(await screen.findByText("HTTP Ping")).toBeInTheDocument();
+ expect(screen.getByText("217ms")).toBeInTheDocument();
+ expect(screen.getByText("62.5% serviceTracker.uptime")).toBeInTheDocument();
+ expect(screen.getByText("edge-1")).toBeInTheDocument();
+ expect(screen.getByText("Monthly")).toBeInTheDocument();
+ expect(screen.queryByText("hidden-server")).not.toBeInTheDocument();
+ });
+
+ it("falls back to zero uptime when a service has no checks", async () => {
+ apiMocks.fetchService.mockResolvedValue({
+ success: true,
+ data: {
+ services: {
+ http: {
+ service_name: "HTTP Ping",
+ current_up: 0,
+ current_down: 0,
+ total_up: 0,
+ total_down: 0,
+ delay: [],
+ up: [0, 0],
+ down: [0, 0],
+ },
+ },
+ },
+ });
+
+ renderWithQuery();
+
+ expect(await screen.findByText("HTTP Ping")).toBeInTheDocument();
+ expect(screen.getByText("0.0% serviceTracker.uptime")).toBeInTheDocument();
+ });
+});
+
+describe("CycleTransferStatsCard", () => {
+ it("renders nothing for empty server lists", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("supports per-server date and max maps while skipping incomplete entries", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("edge-7")).toBeInTheDocument();
+ expect(screen.getByText("Mapped")).toBeInTheDocument();
+ expect(screen.queryByText("edge-8")).not.toBeInTheDocument();
+ expect(screen.queryByText("missing-fields")).not.toBeInTheDocument();
+ });
+});
+
+describe("ServiceTrackerClient and CycleTransferStatsClient", () => {
+ it("uses uptime and delay severity colors for service summaries", () => {
+ const days = [
+ {
+ completed: true,
+ date: new Date("2025-01-01T00:00:00.000Z"),
+ uptime: 99.5,
+ delay: 80,
+ },
+ {
+ completed: false,
+ date: new Date("2025-01-02T00:00:00.000Z"),
+ uptime: 90,
+ delay: 320,
+ },
+ ];
+
+ const { rerender } = render(
+ ,
+ );
+
+ expect(screen.getByText("99.9% serviceTracker.uptime")).toHaveClass(
+ "text-emerald-500",
+ );
+ expect(screen.getByText("80ms")).toHaveClass("text-emerald-500");
+
+ rerender(
+ ,
+ );
+
+ expect(screen.getByText("97.0% serviceTracker.uptime")).toHaveClass(
+ "text-amber-500",
+ );
+ expect(screen.getByText("180ms")).toHaveClass("text-amber-500");
+
+ rerender(
+ ,
+ );
+
+ expect(screen.getByText("94.0% serviceTracker.uptime")).toHaveClass(
+ "text-rose-500",
+ );
+ expect(screen.getByText("320ms")).toHaveClass("text-rose-500");
+ });
+
+ it("caps cycle transfer progress at one hundred percent", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(screen.getByText("200.0%")).toBeInTheDocument();
+ expect(
+ container.querySelector('[style="width: 100%;"]'),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/test/components/switchers-and-primitives.test.tsx b/src/test/components/switchers-and-primitives.test.tsx
new file mode 100644
index 0000000..efeec97
--- /dev/null
+++ b/src/test/components/switchers-and-primitives.test.tsx
@@ -0,0 +1,291 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi } from "vitest";
+import { LanguageSwitcher } from "@/components/LanguageSwitcher";
+import { ThemeProvider } from "@/components/ThemeProvider";
+import { ModeToggle } from "@/components/ThemeSwitcher";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import {
+ DropdownMenu,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Label } from "@/components/ui/label";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Switch } from "@/components/ui/switch";
+import {
+ Table,
+ TableBody,
+ TableCaption,
+ TableCell,
+ TableFooter,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+
+describe("language and theme switchers", () => {
+ it("opens the language menu and marks the active locale", async () => {
+ const user = userEvent.setup();
+
+ render();
+
+ await user.click(screen.getByRole("button", { name: "Change language" }));
+
+ expect(screen.getByText("language.zh-CN")).toBeInTheDocument();
+ expect(screen.getByText("language.en-US")).toHaveClass("font-semibold");
+ });
+
+ it("updates the theme through the mode menu", async () => {
+ const user = userEvent.setup();
+
+ render(
+
+
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: "Toggle theme" }));
+ await user.click(await screen.findByText("theme.dark"));
+
+ expect(document.documentElement).toHaveClass("dark");
+ expect(localStorage.getItem("theme-switcher-test")).toBe("dark");
+ });
+});
+
+describe("Radix UI wrappers", () => {
+ it("opens accordion, popover, tooltip, and dialog content", async () => {
+ const user = userEvent.setup();
+
+ render(
+ <>
+
+
+ Toggle status
+ Status body
+
+
+
+ Open popover
+ Popover body
+
+
+
+ Hover status
+ Tooltip body
+
+
+
+ >,
+ );
+
+ await user.click(screen.getByRole("button", { name: "Toggle status" }));
+ expect(screen.getByText("Status body")).toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "Open popover" }));
+ expect(screen.getByText("Popover body")).toBeInTheDocument();
+
+ await user.hover(screen.getByText("Hover status"));
+ expect(await screen.findByRole("tooltip")).toHaveTextContent(
+ "Tooltip body",
+ );
+
+ await user.click(screen.getByRole("button", { name: "Open dialog" }));
+ expect(screen.getByText("Confirm action")).toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "Done" }));
+ await waitFor(() => {
+ expect(screen.queryByText("Confirm action")).not.toBeInTheDocument();
+ });
+ });
+
+ it("toggles checkbox and switch controls through labels", async () => {
+ const user = userEvent.setup();
+ const onCheckboxChange = vi.fn();
+ const onSwitchChange = vi.fn();
+
+ render(
+ <>
+
+
+
+ >,
+ );
+
+ await user.click(screen.getByText("Alerts"));
+ await user.click(screen.getByRole("switch", { name: "Compact mode" }));
+
+ expect(onCheckboxChange).toHaveBeenCalledWith(true);
+ expect(onSwitchChange).toHaveBeenCalledWith(true);
+ expect(screen.getByRole("checkbox")).toHaveAttribute(
+ "data-state",
+ "checked",
+ );
+ expect(
+ screen.getByRole("switch", { name: "Compact mode" }),
+ ).toHaveAttribute("data-state", "checked");
+ });
+
+ it("renders dropdown labels, indicators, shortcuts, and submenus", async () => {
+ render(
+
+ Open actions
+
+ Actions
+
+
+ Copy
+ Ctrl+C
+
+
+ Show hidden
+
+
+
+ Compact rows
+
+
+
+
+
+ More actions
+
+
+ Archive
+
+
+
+
+ ,
+ );
+
+ expect(await screen.findByText("Actions")).toHaveClass("pl-8");
+ expect(screen.getByText("Copy")).toHaveClass("pl-8");
+ expect(screen.getByText("Ctrl+C")).toHaveClass("tracking-widest");
+ expect(screen.getByText("Show hidden")).toHaveAttribute(
+ "data-state",
+ "checked",
+ );
+ expect(screen.getByText("Compact rows")).toHaveAttribute(
+ "data-state",
+ "checked",
+ );
+ expect(screen.getByText("More actions")).toHaveClass("pl-8");
+ expect(screen.getByText("Archive")).toBeInTheDocument();
+ });
+
+ it("selects values and renders table structure", async () => {
+ const user = userEvent.setup();
+ const onValueChange = vi.fn();
+
+ render(
+ <>
+
+
+ Server summary
+
+
+ Name
+ Status
+
+
+
+
+ edge-1
+ online
+
+
+
+
+ Total
+ 1
+
+
+
+ >,
+ );
+
+ await user.click(screen.getByRole("combobox", { name: "Region" }));
+ await user.click(await screen.findByRole("option", { name: "Asia" }));
+
+ expect(onValueChange).toHaveBeenCalledWith("asia");
+ expect(screen.getByText("Server summary")).toBeInTheDocument();
+ expect(
+ screen.getByRole("columnheader", { name: "Name" }),
+ ).toBeInTheDocument();
+ expect(screen.getByText("edge-1")).toBeInTheDocument();
+ });
+});
diff --git a/src/test/context/providers.test.tsx b/src/test/context/providers.test.tsx
new file mode 100644
index 0000000..4df4e69
--- /dev/null
+++ b/src/test/context/providers.test.tsx
@@ -0,0 +1,359 @@
+import { act, render, renderHook, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import type { ReactNode } from "react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { CommandProvider } from "@/context/command-provider";
+import { SortProvider } from "@/context/sort-provider";
+import { StatusProvider } from "@/context/status-provider";
+import { TooltipProvider } from "@/context/tooltip-provider";
+import { WebSocketProvider } from "@/context/websocket-provider";
+import { useCommand } from "@/hooks/use-command";
+import { useSort } from "@/hooks/use-sort";
+import { useStatus } from "@/hooks/use-status";
+import { useTooltip } from "@/hooks/use-tooltip";
+import { useWebSocketContext } from "@/hooks/use-websocket-context";
+import { createServer } from "@/test/fixtures";
+
+class FakeWebSocket {
+ static readonly CONNECTING = 0;
+ static readonly OPEN = 1;
+ static readonly CLOSING = 2;
+ static readonly CLOSED = 3;
+ static instances: FakeWebSocket[] = [];
+
+ readonly url: string;
+ readyState = FakeWebSocket.CONNECTING;
+ onopen: ((event: Event) => void) | null = null;
+ onclose: ((event: CloseEvent) => void) | null = null;
+ onmessage: ((event: MessageEvent) => void) | null = null;
+ onerror: ((event: Event) => void) | null = null;
+
+ constructor(url: string) {
+ this.url = url;
+ FakeWebSocket.instances.push(this);
+ }
+
+ open() {
+ this.readyState = FakeWebSocket.OPEN;
+ this.onopen?.(new Event("open"));
+ }
+
+ message(data: string) {
+ this.onmessage?.(new MessageEvent("message", { data }));
+ }
+
+ close() {
+ this.readyState = FakeWebSocket.CLOSED;
+ this.onclose?.(new CloseEvent("close"));
+ }
+}
+
+function CommandProbe() {
+ const { closeCommand, isOpen, openCommand, toggleCommand } = useCommand();
+
+ return (
+
+
{isOpen ? "open" : "closed"}
+
+
+
+
+ );
+}
+
+function SortProbe() {
+ const { setSortOrder, setSortType, sortOrder, sortType } = useSort();
+
+ return (
+
+
{`${sortType}:${sortOrder}`}
+
+
+
+ );
+}
+
+function StatusProbe() {
+ const { setStatus, status } = useStatus();
+
+ return (
+
+
{status}
+
+
+ );
+}
+
+function TooltipProbe() {
+ const { setTooltipData, tooltipData } = useTooltip();
+
+ return (
+
+
{tooltipData?.country ?? "empty"}
+
+
+ );
+}
+
+function WebSocketProbe() {
+ const {
+ connected,
+ lastData,
+ messageHistory,
+ needReconnect,
+ setNeedReconnect,
+ } = useWebSocketContext();
+
+ return (
+
+
{connected ? "connected" : "disconnected"}
+
{lastData?.servers[0]?.name ?? "none"}
+
{messageHistory.length}
+
+ {messageHistory.reduce((total, item) => total + item.servers.length, 0)}
+
+
{needReconnect ? "needs-reconnect" : "stable"}
+
+
+ );
+}
+
+describe("state providers", () => {
+ it("manages command palette open state", async () => {
+ const user = userEvent.setup();
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId("command-state")).toHaveTextContent("closed");
+ await user.click(screen.getByRole("button", { name: "open" }));
+ expect(screen.getByTestId("command-state")).toHaveTextContent("open");
+ await user.click(screen.getByRole("button", { name: "toggle" }));
+ expect(screen.getByTestId("command-state")).toHaveTextContent("closed");
+ await user.click(screen.getByRole("button", { name: "open" }));
+ await user.click(screen.getByRole("button", { name: "close" }));
+ expect(screen.getByTestId("command-state")).toHaveTextContent("closed");
+ });
+
+ it("uses forced sort globals when valid and still allows local updates", async () => {
+ window.ForceSortType = "mem";
+ window.ForceSortOrder = "asc";
+ const user = userEvent.setup();
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByText("mem:asc")).toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "cpu" }));
+ expect(screen.getByText("cpu:asc")).toBeInTheDocument();
+ });
+
+ it("falls back to default sort values for invalid forced globals", () => {
+ window.ForceSortType = "invalid";
+ window.ForceSortOrder = "up";
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByText("default:desc")).toBeInTheDocument();
+ });
+
+ it("manages server status filters", async () => {
+ const user = userEvent.setup();
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId("status-state")).toHaveTextContent("all");
+ await user.click(screen.getByRole("button", { name: "online" }));
+ expect(screen.getByTestId("status-state")).toHaveTextContent("online");
+ });
+
+ it("stores map tooltip data", async () => {
+ const user = userEvent.setup();
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByText("empty")).toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "show" }));
+ expect(screen.getByText("China")).toBeInTheDocument();
+ });
+});
+
+describe("context hooks", () => {
+ it("throws helpful errors when strict hooks miss their providers", () => {
+ expect(() => renderHook(() => useCommand())).toThrow(
+ "useCommand must be used within a CommandProvider",
+ );
+ expect(() => renderHook(() => useSort())).toThrow(
+ "useStatus must be used within a SortProvider",
+ );
+ expect(() => renderHook(() => useStatus())).toThrow(
+ "useStatus must be used within a StatusProvider",
+ );
+ expect(() => renderHook(() => useTooltip())).toThrow(
+ "useTooltip must be used within a TooltipProvider",
+ );
+ });
+});
+
+describe("WebSocketProvider", () => {
+ beforeEach(() => {
+ FakeWebSocket.instances = [];
+ vi.stubGlobal("WebSocket", FakeWebSocket);
+ });
+
+ function renderWebSocketProvider(children: ReactNode) {
+ return render(
+ {children},
+ );
+ }
+
+ function websocketPayload(serverName: string) {
+ return JSON.stringify({
+ now: Date.parse("2025-01-01T00:00:20.000Z"),
+ servers: [createServer({ name: serverName })],
+ });
+ }
+
+ it("connects with a ws URL and records incoming messages", () => {
+ renderWebSocketProvider();
+
+ const socket = FakeWebSocket.instances[0];
+ expect(socket.url).toBe("wss://localhost/api/v1/ws/server");
+
+ act(() => {
+ socket.open();
+ });
+ expect(screen.getByText("connected")).toBeInTheDocument();
+
+ act(() => {
+ socket.message(websocketPayload("first"));
+ socket.message(websocketPayload("second"));
+ });
+
+ expect(screen.getByText("second")).toBeInTheDocument();
+ expect(screen.getByTestId("message-count")).toHaveTextContent("2");
+ });
+
+ it("normalizes missing and non-array server collections", () => {
+ renderWebSocketProvider();
+ const socket = FakeWebSocket.instances[0];
+ const now = Date.parse("2025-01-01T00:00:20.000Z");
+
+ act(() => {
+ socket.open();
+ socket.message(JSON.stringify({ now }));
+ socket.message(JSON.stringify({ now, servers: null }));
+ socket.message(JSON.stringify({ now, servers: { invalid: true } }));
+ });
+
+ expect(screen.getByText("none")).toBeInTheDocument();
+ expect(screen.getByTestId("message-count")).toHaveTextContent("3");
+ expect(screen.getByTestId("history-server-count")).toHaveTextContent("0");
+ });
+
+ it("keeps only the latest thirty websocket messages", () => {
+ renderWebSocketProvider();
+ const socket = FakeWebSocket.instances[0];
+
+ act(() => {
+ socket.open();
+ for (let index = 0; index < 31; index += 1) {
+ socket.message(websocketPayload(`message-${index}`));
+ }
+ });
+
+ expect(screen.getByText("message-30")).toBeInTheDocument();
+ expect(screen.getByTestId("message-count")).toHaveTextContent("30");
+ });
+
+ it("ignores malformed websocket messages without disconnecting", () => {
+ const consoleError = vi
+ .spyOn(console, "error")
+ .mockImplementation(() => undefined);
+ renderWebSocketProvider();
+ const socket = FakeWebSocket.instances[0];
+
+ act(() => {
+ socket.open();
+ socket.message("not-json");
+ });
+
+ expect(screen.getByText("connected")).toBeInTheDocument();
+ expect(screen.getByText("none")).toBeInTheDocument();
+ expect(screen.getByTestId("message-count")).toHaveTextContent("0");
+ expect(consoleError).toHaveBeenCalledWith(
+ "Failed to parse WebSocket message:",
+ expect.any(SyntaxError),
+ );
+ });
+
+ it("ignores websocket messages without a valid response shape", () => {
+ const consoleError = vi
+ .spyOn(console, "error")
+ .mockImplementation(() => undefined);
+ renderWebSocketProvider();
+ const socket = FakeWebSocket.instances[0];
+
+ act(() => {
+ socket.open();
+ socket.message("null");
+ socket.message(JSON.stringify({ servers: [] }));
+ });
+
+ expect(screen.getByTestId("message-count")).toHaveTextContent("0");
+ expect(consoleError).toHaveBeenCalledTimes(2);
+ expect(consoleError).toHaveBeenCalledWith(
+ "Failed to parse WebSocket message:",
+ expect.any(TypeError),
+ );
+ });
+
+ it("exposes manual reconnect state separately from socket state", async () => {
+ const user = userEvent.setup();
+ renderWebSocketProvider();
+
+ expect(screen.getByText("stable")).toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "mark" }));
+ expect(screen.getByText("needs-reconnect")).toBeInTheDocument();
+ });
+});
diff --git a/src/test/fixtures.ts b/src/test/fixtures.ts
new file mode 100644
index 0000000..ca5e125
--- /dev/null
+++ b/src/test/fixtures.ts
@@ -0,0 +1,77 @@
+import type { NezhaServer } from "@/types/nezha-api";
+
+type NezhaServerOverrides = Omit, "host" | "state"> & {
+ host?: Partial;
+ state?: Partial;
+};
+
+const baseHost: NezhaServer["host"] = {
+ platform: "linux",
+ platform_version: "6.8",
+ cpu: ["AMD EPYC"],
+ gpu: [],
+ mem_total: 200,
+ disk_total: 400,
+ swap_total: 100,
+ arch: "amd64",
+ boot_time: 1_735_603_200,
+ version: "1.0.0",
+};
+
+const baseState: NezhaServer["state"] = {
+ cpu: 12,
+ mem_used: 50,
+ swap_used: 10,
+ disk_used: 100,
+ net_in_transfer: 1024 ** 3,
+ net_out_transfer: 2 * 1024 ** 3,
+ net_in_speed: 1024 ** 2,
+ net_out_speed: 2 * 1024 ** 2,
+ uptime: 86_400,
+ load_1: 0.12,
+ load_5: 0.45,
+ load_15: 0.78,
+ tcp_conn_count: 8,
+ udp_conn_count: 4,
+ process_count: 88,
+ temperatures: [],
+ gpu: [],
+};
+
+export function createServer(
+ overrides: NezhaServerOverrides = {},
+): NezhaServer {
+ return {
+ id: 1,
+ name: "edge-1",
+ public_note: "",
+ last_active: "2025-01-01T00:00:00.000Z",
+ country_code: "us",
+ ...overrides,
+ host: {
+ ...baseHost,
+ ...overrides.host,
+ },
+ state: {
+ ...baseState,
+ ...overrides.state,
+ },
+ };
+}
+
+export function createSettingResponse() {
+ return {
+ success: true,
+ data: {
+ config: {
+ debug: false,
+ language: "en-US",
+ site_name: "Nezha",
+ user_template: "",
+ admin_template: "",
+ custom_code: "",
+ },
+ version: "1.0.0",
+ },
+ };
+}
diff --git a/src/test/hooks/hooks.test.tsx b/src/test/hooks/hooks.test.tsx
new file mode 100644
index 0000000..bc9225a
--- /dev/null
+++ b/src/test/hooks/hooks.test.tsx
@@ -0,0 +1,129 @@
+import { act, render, renderHook, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi } from "vitest";
+
+import { useActiveIndicator } from "@/hooks/use-active-indicator";
+import { useBackground } from "@/hooks/use-background";
+import { useChartHistory } from "@/hooks/use-chart-history";
+
+function BackgroundProbe() {
+ const { backgroundImage, updateBackground } = useBackground();
+
+ return (
+
+
{backgroundImage ?? "empty"}
+
+
+
+ );
+}
+
+function ActiveIndicatorProbe({
+ active,
+ items,
+}: {
+ active: string;
+ items: string[];
+}) {
+ const { containerRef, enableIndicatorAnimation, indicator, setItemRef } =
+ useActiveIndicator(items, active);
+
+ return (
+
+
+ {items.map((item, index) => (
+
+ {item}
+
+ ))}
+
+
+ {indicator ? `${indicator.width}:${indicator.shouldAnimate}` : "none"}
+
+
+ );
+}
+
+describe("useBackground", () => {
+ it("updates the global background image and broadcasts changes", async () => {
+ const user = userEvent.setup();
+ render();
+
+ expect(screen.getByText("empty")).toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "set" }));
+ expect(window.CustomBackgroundImage).toBe("/bg.png");
+ expect(screen.getByText("/bg.png")).toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "clear" }));
+ expect(screen.getByText("empty")).toBeInTheDocument();
+ });
+
+ it("restores a saved background image during polling", async () => {
+ vi.useFakeTimers();
+ sessionStorage.setItem("savedBackgroundImage", "/saved.png");
+ render();
+
+ act(() => {
+ vi.advanceTimersByTime(100);
+ });
+
+ expect(screen.getByText("/saved.png")).toBeInTheDocument();
+ });
+});
+
+describe("useChartHistory", () => {
+ it("formats websocket message history once and keeps newest data last", () => {
+ const history = [
+ { now: 2, servers: [] },
+ { now: 1, servers: [] },
+ ];
+ const formatFn = vi.fn((wsData: { now: number }, serverId: number) =>
+ serverId === 1 ? wsData.now : null,
+ );
+
+ const { result, rerender } = renderHook(
+ ({ messages }) => useChartHistory(messages, 1, formatFn),
+ { initialProps: { messages: history } },
+ );
+
+ expect(result.current).toEqual([1, 2]);
+ expect(formatFn).toHaveBeenCalledTimes(2);
+
+ rerender({ messages: [...history, { now: 3, servers: [] }] });
+ expect(result.current).toEqual([1, 2]);
+ });
+});
+
+describe("useActiveIndicator", () => {
+ it("tracks the active item and clears when it is not available", async () => {
+ Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
+ configurable: true,
+ get() {
+ return 20;
+ },
+ });
+ Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
+ configurable: true,
+ get() {
+ return 10;
+ },
+ });
+
+ const { rerender } = render(
+ ,
+ );
+
+ expect(screen.getByText("20:false")).toBeInTheDocument();
+ rerender();
+ expect(screen.getByText("none")).toBeInTheDocument();
+ });
+});
diff --git a/src/test/lib/format.test.ts b/src/test/lib/format.test.ts
new file mode 100644
index 0000000..3b06c6a
--- /dev/null
+++ b/src/test/lib/format.test.ts
@@ -0,0 +1,21 @@
+import { describe, expect, it } from "vitest";
+
+import { formatBytes } from "@/lib/format";
+
+describe("formatBytes", () => {
+ it("formats empty byte values as zero KiB", () => {
+ expect(formatBytes(0)).toBe("0 KiB");
+ expect(formatBytes(Number.NaN)).toBe("0 KiB");
+ });
+
+ it("keeps byte values in binary units", () => {
+ expect(formatBytes(512)).toBe("0.50 KiB");
+ expect(formatBytes(1024)).toBe("1.00 KiB");
+ expect(formatBytes(1024 ** 2)).toBe("1.00 MiB");
+ expect(formatBytes(1024 ** 3 * 2.5, 1)).toBe("2.5 GiB");
+ });
+
+ it("clamps negative decimal precision to an integer", () => {
+ expect(formatBytes(1536, -1)).toBe("2 KiB");
+ });
+});
diff --git a/src/test/lib/inject.test.ts b/src/test/lib/inject.test.ts
new file mode 100644
index 0000000..f59fec7
--- /dev/null
+++ b/src/test/lib/inject.test.ts
@@ -0,0 +1,64 @@
+import { describe, expect, it, vi } from "vitest";
+
+import { InjectContext } from "@/lib/inject";
+
+describe("InjectContext", () => {
+ it("injects supported resource nodes and marks them for cleanup", async () => {
+ vi.spyOn(console, "log").mockImplementation(() => undefined);
+
+ await InjectContext(`
+
+
+
+ custom content
+ plain text
+ `);
+
+ expect(document.querySelector('meta[name="x-test"]')).toHaveAttribute(
+ "data-injected",
+ "true",
+ );
+ expect(document.querySelector("style[data-injected]")).toHaveTextContent(
+ ".custom",
+ );
+ expect(document.querySelector("script[data-injected]")).toHaveTextContent(
+ "window.__injected = true;",
+ );
+ expect(document.querySelector("#custom-node")).toHaveAttribute(
+ "data-injected",
+ "true",
+ );
+ expect(document.body).toHaveTextContent("plain text");
+ });
+
+ it("cleans previous injected resources before applying new content", async () => {
+ vi.spyOn(console, "log").mockImplementation(() => undefined);
+
+ await InjectContext(`first
`);
+ await InjectContext(`second
`);
+
+ expect(document.querySelector("#first")).not.toBeInTheDocument();
+ expect(document.querySelector("#second")).toBeInTheDocument();
+ });
+
+ it("logs external resource failures without throwing to callers", async () => {
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
+
+ const appendChild = vi
+ .spyOn(document.head, "appendChild")
+ .mockImplementation((node) => {
+ if (node instanceof HTMLScriptElement) {
+ setTimeout(() => node.onerror?.(new Event("error")), 0);
+ }
+ return node;
+ });
+
+ await InjectContext(``);
+
+ expect(appendChild).toHaveBeenCalled();
+ expect(console.error).toHaveBeenCalledWith(
+ "Error during resource injection:",
+ expect.any(Error),
+ );
+ });
+});
diff --git a/src/test/lib/logo-class.test.tsx b/src/test/lib/logo-class.test.tsx
new file mode 100644
index 0000000..38d3129
--- /dev/null
+++ b/src/test/lib/logo-class.test.tsx
@@ -0,0 +1,39 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import {
+ GetFontLogoClass,
+ GetOsName,
+ MageMicrosoftWindows,
+} from "@/lib/logo-class";
+
+describe("logo-class helpers", () => {
+ it("maps known platform aliases to font logo classes", () => {
+ expect(GetFontLogoClass("ubuntu")).toBe("ubuntu");
+ expect(GetFontLogoClass("darwin")).toBe("apple");
+ expect(GetFontLogoClass("linux")).toBe("tux");
+ expect(GetFontLogoClass("amazon")).toBe("redhat");
+ expect(GetFontLogoClass("arch")).toBe("archlinux");
+ expect(GetFontLogoClass("opensuse-tumbleweed")).toBe("opensuse");
+ expect(GetFontLogoClass("unknown")).toBe("tux");
+ });
+
+ it("maps platform aliases to readable operating system names", () => {
+ expect(GetOsName("ubuntu")).toBe("Ubuntu");
+ expect(GetOsName("darwin")).toBe("macOS");
+ expect(GetOsName("linux")).toBe("Linux");
+ expect(GetOsName("amazon")).toBe("Redhat");
+ expect(GetOsName("arch")).toBe("Archlinux");
+ expect(GetOsName("opensuse-tumbleweed")).toBe("Opensuse");
+ expect(GetOsName("unknown")).toBe("Linux");
+ });
+
+ it("renders the Windows SVG icon", () => {
+ render();
+
+ expect(screen.getByTestId("windows-icon")).toHaveAttribute(
+ "viewBox",
+ "0 0 24 24",
+ );
+ });
+});
diff --git a/src/test/lib/nezha-api.test.ts b/src/test/lib/nezha-api.test.ts
new file mode 100644
index 0000000..6e17fe3
--- /dev/null
+++ b/src/test/lib/nezha-api.test.ts
@@ -0,0 +1,116 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import {
+ fetchLoginUser,
+ fetchMonitor,
+ fetchServerMetrics,
+ fetchService,
+ fetchSetting,
+} from "@/lib/nezha-api";
+
+const jsonResponse = (body: unknown, init?: ResponseInit) =>
+ new Response(JSON.stringify(body), {
+ status: init?.status ?? 200,
+ statusText: init?.statusText,
+ headers: {
+ "Content-Type": "application/json",
+ ...init?.headers,
+ },
+ });
+
+describe("nezha api fetchers", () => {
+ beforeEach(() => {
+ vi.stubGlobal("fetch", vi.fn());
+ });
+
+ it("returns setting payloads", async () => {
+ const payload = {
+ success: true,
+ data: {
+ config: {
+ debug: false,
+ language: "en-US",
+ site_name: "Nezha",
+ user_template: "",
+ admin_template: "",
+ custom_code: "",
+ },
+ version: "1.0.0",
+ },
+ };
+ vi.mocked(fetch).mockResolvedValueOnce(jsonResponse(payload));
+
+ await expect(fetchSetting()).resolves.toEqual(payload);
+ expect(fetch).toHaveBeenCalledWith("/api/v1/setting");
+ });
+
+ it("throws API error messages returned by service endpoints", async () => {
+ vi.mocked(fetch).mockResolvedValueOnce(
+ jsonResponse({ error: "service unavailable" }),
+ );
+
+ await expect(fetchService()).rejects.toThrow("service unavailable");
+ });
+
+ it("adds monitor and metrics query parameters when periods are provided", async () => {
+ vi.mocked(fetch)
+ .mockResolvedValueOnce(jsonResponse({ success: true, data: [] }))
+ .mockResolvedValueOnce(
+ jsonResponse({
+ success: true,
+ data: {
+ server_id: 7,
+ server_name: "edge",
+ metric: "cpu",
+ data_points: [],
+ },
+ }),
+ );
+
+ await fetchMonitor(7, "7d");
+ await fetchServerMetrics(7, "cpu", "30d");
+
+ expect(fetch).toHaveBeenNthCalledWith(
+ 1,
+ "/api/v1/server/7/service?period=7d",
+ );
+ expect(fetch).toHaveBeenNthCalledWith(
+ 2,
+ "/api/v1/server/7/metrics?metric=cpu&period=30d",
+ );
+ });
+
+ it("refreshes the token when a logged-in browser session has cookies", async () => {
+ const consoleLog = vi
+ .spyOn(console, "log")
+ .mockImplementation(() => undefined);
+ Object.defineProperty(document, "cookie", {
+ configurable: true,
+ value: "nezha_token=token; nz-csrf=test-csrf-token",
+ });
+ const payload = {
+ success: true,
+ data: {
+ id: 1,
+ username: "admin",
+ password: "",
+ created_at: "2025-01-01T00:00:00Z",
+ updated_at: "2025-01-01T00:00:00Z",
+ },
+ };
+ vi.mocked(fetch)
+ .mockResolvedValueOnce(jsonResponse(payload))
+ .mockResolvedValueOnce(jsonResponse({ success: true }));
+
+ await expect(fetchLoginUser()).resolves.toEqual(payload);
+
+ expect(fetch).toHaveBeenNthCalledWith(1, "/api/v1/profile");
+ expect(fetch).toHaveBeenNthCalledWith(2, "/api/v1/refresh-token", {
+ method: "POST",
+ headers: {
+ "X-CSRF-Token": "test-csrf-token",
+ },
+ });
+ expect(consoleLog).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/test/lib/static-data.test.ts b/src/test/lib/static-data.test.ts
new file mode 100644
index 0000000..5bfd495
--- /dev/null
+++ b/src/test/lib/static-data.test.ts
@@ -0,0 +1,25 @@
+import { describe, expect, it } from "vitest";
+
+import { geoJsonString } from "@/lib/geo-json-string";
+import { countryCoordinates } from "@/lib/geo-limit";
+
+describe("static geographic data", () => {
+ it("provides known country coordinate metadata", () => {
+ expect(countryCoordinates.CN).toMatchObject({
+ lat: 35,
+ lng: 105,
+ name: "China",
+ });
+ expect(countryCoordinates.US.name).toBe("United States");
+ });
+
+ it("ships parseable GeoJSON feature collection data", () => {
+ const geoJson = JSON.parse(geoJsonString) as {
+ type: string;
+ features: unknown[];
+ };
+
+ expect(geoJson.type).toBe("FeatureCollection");
+ expect(geoJson.features.length).toBeGreaterThan(0);
+ });
+});
diff --git a/src/test/lib/utils.test.ts b/src/test/lib/utils.test.ts
new file mode 100644
index 0000000..c31e07c
--- /dev/null
+++ b/src/test/lib/utils.test.ts
@@ -0,0 +1,346 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import {
+ fetcher,
+ formatNezhaInfo,
+ formatRelativeTime,
+ formatTime,
+ getDaysBetweenDates,
+ getDaysBetweenDatesWithAutoRenewal,
+ getNextCycleTime,
+ handlePublicNote,
+ nezhaFetcher,
+ parsePublicNote,
+} from "@/lib/utils";
+import type { NezhaServer } from "@/types/nezha-api";
+
+const serverFixture: NezhaServer = {
+ id: 1,
+ name: "edge",
+ public_note: '{"planDataMod":{"bandwidth":"1Gbps"}}',
+ last_active: "2025-01-01T00:00:00.000Z",
+ country_code: "US",
+ host: {
+ platform: "linux",
+ platform_version: "6.8",
+ cpu: ["AMD EPYC"],
+ gpu: [],
+ mem_total: 200,
+ disk_total: 400,
+ swap_total: 100,
+ arch: "amd64",
+ boot_time: 1_735_603_200,
+ version: "1.0.0",
+ },
+ state: {
+ cpu: 12,
+ mem_used: 50,
+ swap_used: 10,
+ disk_used: 100,
+ net_in_transfer: 1024,
+ net_out_transfer: 2048,
+ net_in_speed: 1024 * 1024,
+ net_out_speed: 2 * 1024 * 1024,
+ uptime: 3600,
+ load_1: 0.123,
+ load_5: 0.456,
+ load_15: 0.789,
+ tcp_conn_count: 8,
+ udp_conn_count: 4,
+ process_count: 88,
+ temperatures: [],
+ gpu: [],
+ },
+};
+
+describe("date and billing helpers", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2025-01-15T00:00:00.000Z"));
+ });
+
+ it("calculates the next renewal cycle after the specified date", () => {
+ const nextCycle = getNextCycleTime(
+ Date.UTC(2025, 0, 1),
+ 1,
+ Date.UTC(2025, 2, 15),
+ );
+
+ expect(new Date(nextCycle).toISOString()).toBe("2025-04-01T00:00:00.000Z");
+ });
+
+ it("normalizes common cycle labels and remaining package days", () => {
+ const result = getDaysBetweenDatesWithAutoRenewal({
+ startDate: "2025-01-01T00:00:00.000Z",
+ endDate: "2025-01-31T00:00:00.000Z",
+ autoRenewal: "0",
+ cycle: "monthly",
+ amount: "10",
+ });
+
+ expect(result.days).toBe(16);
+ expect(result.cycleLabel).toBe("月");
+ expect(result.remainingPercentage).toBeCloseTo(16 / 30);
+ });
+
+ it("handles auto-renewal before and after the current cycle end", () => {
+ expect(
+ getDaysBetweenDatesWithAutoRenewal({
+ startDate: "2025-01-01T00:00:00.000Z",
+ endDate: "2025-02-01T00:00:00.000Z",
+ autoRenewal: "1",
+ cycle: "quarterly",
+ amount: "10",
+ }),
+ ).toMatchObject({
+ days: 17,
+ cycleLabel: "季",
+ });
+
+ const renewed = getDaysBetweenDatesWithAutoRenewal({
+ startDate: "2024-01-01T00:00:00.000Z",
+ endDate: "2024-12-01T00:00:00.000Z",
+ autoRenewal: "1",
+ cycle: "annual",
+ amount: "10",
+ });
+
+ expect(renewed.days).toBeGreaterThan(300);
+ expect(renewed.cycleLabel).toBe("年");
+ expect(renewed.remainingPercentage).toBeLessThanOrEqual(1);
+ });
+
+ it("rejects invalid cycle calculations", () => {
+ expect(() => getNextCycleTime(Date.UTC(2025, 0, 1), 0, Date.now())).toThrow(
+ "参数无效",
+ );
+ });
+
+ it("formats absolute dates and day differences", () => {
+ expect(formatTime(new Date(2025, 0, 2, 3, 4, 5).getTime())).toBe(
+ "2025-1-2 03:04:05",
+ );
+ expect(getDaysBetweenDates("2025-01-20", "2025-01-15")).toBe(5);
+ });
+
+ it("formats relative timestamps using compact units", () => {
+ expect(formatRelativeTime(Date.now() - 45 * 1000)).toBe("45s");
+ expect(formatRelativeTime(Date.now() - 5 * 60 * 1000)).toBe("5m");
+ expect(formatRelativeTime(Date.now() - 2 * 60 * 60 * 1000)).toBe("2h");
+ expect(formatRelativeTime(Date.now() - 3 * 24 * 60 * 60 * 1000)).toBe("3d");
+ });
+});
+
+describe("public note helpers", () => {
+ it("parses supported public note blocks and defaults missing strings", () => {
+ expect(
+ parsePublicNote(
+ JSON.stringify({
+ billingDataMod: {
+ endDate: "2025-12-31",
+ cycle: "year",
+ },
+ planDataMod: {
+ bandwidth: "1Gbps",
+ IPv4: "1",
+ },
+ }),
+ ),
+ ).toEqual({
+ billingDataMod: {
+ startDate: "",
+ endDate: "2025-12-31",
+ autoRenewal: "",
+ cycle: "year",
+ amount: "",
+ },
+ planDataMod: {
+ bandwidth: "1Gbps",
+ trafficVol: "",
+ trafficType: "",
+ IPv4: "1",
+ IPv6: "",
+ networkRoute: "",
+ extra: "",
+ },
+ });
+ });
+
+ it("returns null for invalid public note JSON", () => {
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
+
+ expect(parsePublicNote("{bad json")).toBeNull();
+ });
+
+ it("returns null or partial blocks for empty and single-section public notes", () => {
+ expect(parsePublicNote("")).toBeNull();
+ expect(parsePublicNote(JSON.stringify({ note: "plain" }))).toBeNull();
+ expect(
+ parsePublicNote(
+ JSON.stringify({
+ billingDataMod: {
+ endDate: "2025-12-31",
+ amount: "20",
+ },
+ }),
+ ),
+ ).toEqual({
+ billingDataMod: {
+ startDate: "",
+ endDate: "2025-12-31",
+ autoRenewal: "",
+ cycle: "",
+ amount: "20",
+ },
+ });
+ expect(
+ parsePublicNote(
+ JSON.stringify({
+ planDataMod: {
+ trafficVol: "1TB",
+ extra: "Backup",
+ },
+ }),
+ ),
+ ).toEqual({
+ planDataMod: {
+ bandwidth: "",
+ trafficVol: "1TB",
+ trafficType: "",
+ IPv4: "",
+ IPv6: "",
+ networkRoute: "",
+ extra: "Backup",
+ },
+ });
+ });
+
+ it("falls back to cached notes when websocket payloads are empty", () => {
+ expect(handlePublicNote(1, "live note")).toBe("live note");
+ expect(handlePublicNote(1, "")).toBe("live note");
+ expect(sessionStorage.getItem("server_1_public_note")).toBe("live note");
+ });
+
+ it("returns an empty public note when no live or cached note exists", () => {
+ expect(handlePublicNote(404, "")).toBe("");
+ });
+});
+
+describe("nezha data formatting", () => {
+ it("maps websocket server state into view-friendly metrics", () => {
+ const now = new Date("2025-01-01T00:00:20.000Z").getTime();
+
+ const result = formatNezhaInfo(now, serverFixture);
+
+ expect(result.online).toBe(true);
+ expect(result.up).toBe(2);
+ expect(result.down).toBe(1);
+ expect(result.mem).toBe(25);
+ expect(result.swap).toBe(10);
+ expect(result.disk).toBe(25);
+ expect(result.load_1).toBe("0.12");
+ expect(result.public_note).toBe(serverFixture.public_note);
+ });
+
+ it("falls back safely for offline timestamps and empty host totals", () => {
+ const result = formatNezhaInfo(Date.now(), {
+ ...serverFixture,
+ public_note: "",
+ last_active: "0001-01-01T00:00:00.000Z",
+ host: {
+ ...serverFixture.host,
+ boot_time: 0,
+ mem_total: 0,
+ swap_total: 0,
+ disk_total: 0,
+ version: "",
+ cpu: [],
+ },
+ state: {
+ ...serverFixture.state,
+ cpu: 0,
+ mem_used: 0,
+ swap_used: 0,
+ disk_used: 0,
+ net_in_speed: 0,
+ net_out_speed: 0,
+ process_count: 0,
+ },
+ });
+
+ expect(result.online).toBe(false);
+ expect(result.last_active_time_string).toBe("");
+ expect(result.boot_time_string).toBe("");
+ expect(result.version).toBeNull();
+ expect(result.mem).toBe(0);
+ expect(result.swap).toBe(0);
+ expect(result.disk).toBe(0);
+ expect(result.public_note).toBe("");
+ });
+});
+
+describe("fetcher", () => {
+ it("returns nested data for successful responses", async () => {
+ vi.stubGlobal(
+ "fetch",
+ vi.fn().mockResolvedValue(
+ new Response(JSON.stringify({ data: { ok: true } }), {
+ headers: { "Content-Type": "application/json" },
+ }),
+ ),
+ );
+
+ await expect(fetcher("/api/v1/demo")).resolves.toEqual({ ok: true });
+ });
+
+ it("logs and rethrows failed responses", async () => {
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
+ vi.stubGlobal(
+ "fetch",
+ vi.fn().mockResolvedValue(
+ new Response(JSON.stringify({ error: "bad" }), {
+ status: 500,
+ statusText: "Server Error",
+ headers: { "Content-Type": "application/json" },
+ }),
+ ),
+ );
+
+ await expect(fetcher("/api/v1/demo")).rejects.toThrow("Server Error");
+ expect(console.error).toHaveBeenCalled();
+ });
+});
+
+describe("nezhaFetcher", () => {
+ it("returns JSON for successful responses", async () => {
+ vi.stubGlobal(
+ "fetch",
+ vi.fn().mockResolvedValue(
+ new Response(JSON.stringify({ success: true }), {
+ headers: { "Content-Type": "application/json" },
+ }),
+ ),
+ );
+
+ await expect(nezhaFetcher("/api/v1/setting")).resolves.toEqual({
+ success: true,
+ });
+ });
+
+ it("throws status and response info for failed responses", async () => {
+ vi.stubGlobal(
+ "fetch",
+ vi.fn().mockResolvedValue(
+ new Response(JSON.stringify({ error: "forbidden" }), {
+ status: 403,
+ statusText: "Forbidden",
+ headers: { "Content-Type": "application/json" },
+ }),
+ ),
+ );
+
+ await expect(nezhaFetcher("/api/v1/setting")).rejects.toMatchObject({
+ status: 403,
+ info: { error: "forbidden" },
+ });
+ });
+});
diff --git a/src/test/pages/Server.test.tsx b/src/test/pages/Server.test.tsx
new file mode 100644
index 0000000..1410718
--- /dev/null
+++ b/src/test/pages/Server.test.tsx
@@ -0,0 +1,545 @@
+import { screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { SortProvider } from "@/context/sort-provider";
+import { StatusProvider } from "@/context/status-provider";
+import type { WebSocketContextType } from "@/context/websocket-context";
+import { WebSocketContext } from "@/context/websocket-context";
+import { useStatus } from "@/hooks/use-status";
+import Servers from "@/pages/Server";
+import { createServer } from "@/test/fixtures";
+import { renderWithProviders } from "@/test/utils";
+import type { NezhaServer } from "@/types/nezha-api";
+
+const apiMocks = vi.hoisted(() => ({
+ fetchServerGroup: vi.fn(),
+ fetchService: vi.fn(),
+}));
+
+vi.mock("@/lib/nezha-api", () => apiMocks);
+
+vi.mock("@/components/GlobalMap", () => ({
+ default: ({ serverList }: { serverList: NezhaServer[] }) => (
+ {serverList.length}
+ ),
+}));
+
+vi.mock("@/components/GroupSwitch", () => ({
+ default: ({
+ tabs,
+ setCurrentTab,
+ }: {
+ tabs: string[];
+ setCurrentTab: (tab: string) => void;
+ }) => (
+
+ {tabs.map((tab) => (
+
+ ))}
+
+ ),
+}));
+
+vi.mock("@/components/ServerOverview", () => ({
+ default: ({
+ offline,
+ online,
+ total,
+ }: {
+ offline: number;
+ online: number;
+ total: number;
+ }) => (
+ {`${total}:${online}:${offline}`}
+ ),
+}));
+
+vi.mock("@/components/ServerCard", () => ({
+ default: ({ serverInfo }: { serverInfo: NezhaServer }) => (
+ {serverInfo.name}
+ ),
+}));
+
+vi.mock("@/components/ServerCardInline", () => ({
+ default: ({ serverInfo }: { serverInfo: NezhaServer }) => (
+ {serverInfo.name}
+ ),
+}));
+
+vi.mock("@/components/ServiceTracker", () => ({
+ ServiceTracker: ({ serverList }: { serverList: NezhaServer[] }) => (
+ {serverList.length}
+ ),
+}));
+
+function StatusControl() {
+ const { setStatus } = useStatus();
+
+ return (
+
+ );
+}
+
+function renderServerPage(
+ websocketValue: Partial,
+ {
+ backendError = null,
+ withStatusControl = false,
+ }: { backendError?: Error | null; withStatusControl?: boolean } = {},
+) {
+ const defaultWebsocketValue: WebSocketContextType = {
+ lastData: null,
+ connected: false,
+ messageHistory: [],
+ reconnect: vi.fn(),
+ needReconnect: false,
+ setNeedReconnect: vi.fn(),
+ };
+
+ return renderWithProviders(
+
+
+
+ {withStatusControl && }
+
+
+
+ ,
+ );
+}
+
+function websocketPayload(servers: NezhaServer[]) {
+ return {
+ now: Date.parse("2025-01-01T00:00:20.000Z"),
+ servers,
+ };
+}
+
+describe("Servers page", () => {
+ beforeEach(() => {
+ apiMocks.fetchServerGroup.mockResolvedValue({
+ success: true,
+ data: [
+ {
+ group: {
+ id: 1,
+ created_at: "",
+ updated_at: "",
+ name: "Edge",
+ },
+ servers: [2],
+ },
+ ],
+ });
+ apiMocks.fetchService.mockResolvedValue({
+ success: true,
+ data: {
+ services: {},
+ cycle_transfer_stats: {},
+ },
+ });
+ });
+
+ it("renders websocket loading and processing states", () => {
+ const { rerender } = renderServerPage({
+ connected: false,
+ lastData: null,
+ });
+
+ expect(screen.getByText("info.websocketConnecting")).toBeInTheDocument();
+
+ rerender(
+
+
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByText("info.processing")).toBeInTheDocument();
+ });
+
+ it("renders a centered backend error instead of a 500 page", async () => {
+ renderServerPage(
+ {
+ connected: false,
+ lastData: null,
+ },
+ { backendError: new Error("settings failed") },
+ );
+
+ expect(
+ screen.getByText("error.backendUnavailableTitle"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("error.backendUnavailableDescription"),
+ ).toBeInTheDocument();
+ expect(screen.getByText("settings failed")).toBeInTheDocument();
+ expect(
+ screen.queryByText("info.websocketConnecting"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("shows backend query errors while waiting for websocket data", async () => {
+ apiMocks.fetchServerGroup.mockRejectedValue(new Error("group failed"));
+ apiMocks.fetchService.mockRejectedValue(new Error("service failed"));
+
+ renderServerPage({
+ connected: false,
+ lastData: null,
+ });
+
+ expect(
+ await screen.findByText("error.backendUnavailableTitle"),
+ ).toBeInTheDocument();
+ expect(screen.getByText("group failed")).toBeInTheDocument();
+ });
+
+ it("summarizes online and offline servers from websocket data", async () => {
+ const online = createServer({ id: 1, name: "alpha" });
+ const offline = createServer({
+ id: 2,
+ name: "beta",
+ last_active: "2024-12-31T23:00:00.000Z",
+ });
+
+ renderServerPage({
+ connected: true,
+ lastData: websocketPayload([online, offline]),
+ });
+
+ expect(screen.getByTestId("server-overview")).toHaveTextContent("2:1:1");
+ expect(screen.getAllByTestId("server-card")).toHaveLength(2);
+ expect(screen.getByText("alpha")).toBeInTheDocument();
+ expect(screen.getByText("beta")).toBeInTheDocument();
+
+ await waitFor(() => {
+ expect(apiMocks.fetchServerGroup).toHaveBeenCalled();
+ expect(apiMocks.fetchService).toHaveBeenCalled();
+ });
+ });
+
+ it("shows an empty state and hides controls when there are no servers", () => {
+ const { container } = renderServerPage({
+ connected: true,
+ lastData: websocketPayload([]),
+ });
+
+ expect(screen.getByTestId("server-overview")).toHaveTextContent("0:0:0");
+ expect(screen.getByText("info.noServers")).toBeInTheDocument();
+ expect(
+ container.querySelector(".server-overview-controls"),
+ ).not.toBeVisible();
+ expect(screen.queryByTestId("server-card")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("global-map")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("service-tracker")).not.toBeInTheDocument();
+ });
+
+ it("shows a filtered empty state while keeping controls available", async () => {
+ const offline = createServer({
+ id: 1,
+ name: "offline",
+ last_active: "2024-12-31T23:00:00.000Z",
+ });
+ const user = userEvent.setup();
+
+ const { container } = renderServerPage(
+ {
+ connected: true,
+ lastData: websocketPayload([offline]),
+ },
+ { withStatusControl: true },
+ );
+
+ await user.click(screen.getByRole("button", { name: "online-only" }));
+
+ expect(screen.getByText("info.noMatchingServers")).toBeInTheDocument();
+ expect(container.querySelector(".server-overview-controls")).not.toBeNull();
+ expect(screen.queryByTestId("server-card")).not.toBeInTheDocument();
+ });
+
+ it("filters servers by selected group", async () => {
+ const online = createServer({ id: 1, name: "alpha" });
+ const offline = createServer({
+ id: 2,
+ name: "beta",
+ last_active: "2024-12-31T23:00:00.000Z",
+ });
+ const user = userEvent.setup();
+
+ renderServerPage({
+ connected: true,
+ lastData: websocketPayload([online, offline]),
+ });
+
+ await user.click(await screen.findByTestId("group-Edge"));
+
+ expect(screen.queryByText("alpha")).not.toBeInTheDocument();
+ expect(screen.getByText("beta")).toBeInTheDocument();
+ expect(sessionStorage.getItem("selectedGroup")).toBe("Edge");
+ });
+
+ it("sorts server cards by selected metrics and direction", async () => {
+ const lowCpu = createServer({
+ id: 1,
+ name: "alpha",
+ state: { cpu: 10 },
+ });
+ const highCpu = createServer({
+ id: 2,
+ name: "beta",
+ state: { cpu: 90 },
+ });
+ const user = userEvent.setup();
+
+ renderServerPage({
+ connected: true,
+ lastData: websocketPayload([lowCpu, highCpu]),
+ });
+
+ await user.selectOptions(screen.getByLabelText("Sort metric"), "cpu");
+ expect(screen.getAllByTestId("server-card")[0]).toHaveTextContent("beta");
+
+ await user.click(screen.getByLabelText("Toggle sort direction"));
+ expect(screen.getAllByTestId("server-card")[0]).toHaveTextContent("alpha");
+ });
+
+ it("keeps name sorting independent from online status", async () => {
+ const onlineAlpha = createServer({
+ id: 1,
+ name: "alpha",
+ });
+ const offlineZeta = createServer({
+ id: 2,
+ name: "zeta",
+ last_active: "2024-12-31T23:00:00.000Z",
+ });
+ const user = userEvent.setup();
+
+ renderServerPage({
+ connected: true,
+ lastData: websocketPayload([onlineAlpha, offlineZeta]),
+ });
+
+ expect(screen.getByLabelText("Toggle sort direction")).toBeDisabled();
+
+ await user.selectOptions(screen.getByLabelText("Sort metric"), "name");
+
+ expect(screen.getAllByTestId("server-card")[0]).toHaveTextContent("zeta");
+
+ await user.click(screen.getByLabelText("Toggle sort direction"));
+ expect(screen.getAllByTestId("server-card")[0]).toHaveTextContent("alpha");
+ });
+
+ it("sorts by system even when a platform value is missing", async () => {
+ const missingPlatform = createServer({
+ id: 1,
+ name: "alpha",
+ host: { platform: undefined },
+ });
+ const linux = createServer({
+ id: 2,
+ name: "beta",
+ host: { platform: "linux" },
+ });
+ const user = userEvent.setup();
+
+ renderServerPage({
+ connected: true,
+ lastData: websocketPayload([missingPlatform, linux]),
+ });
+
+ await user.selectOptions(screen.getByLabelText("Sort metric"), "system");
+
+ const cards = screen.getAllByTestId("server-card");
+ expect(cards).toHaveLength(2);
+ expect(cards[0]).toHaveTextContent("beta");
+ expect(cards[1]).toHaveTextContent("alpha");
+ });
+
+ it("restores the saved main page scroll position after data is ready", async () => {
+ const scrollTo = vi.fn();
+ vi.stubGlobal("scrollTo", scrollTo);
+ vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
+ callback(0);
+ return 0;
+ });
+ sessionStorage.setItem("fromMainPage", "true");
+ sessionStorage.setItem("scrollPosition", "345");
+
+ renderServerPage({
+ connected: true,
+ lastData: websocketPayload([createServer({ id: 1, name: "alpha" })]),
+ });
+
+ await waitFor(() => {
+ expect(scrollTo).toHaveBeenCalledWith({
+ top: 345,
+ left: 0,
+ behavior: "auto",
+ });
+ });
+ });
+
+ it("does not restore stale scroll positions without a main page origin", () => {
+ const scrollTo = vi.fn();
+ vi.stubGlobal("scrollTo", scrollTo);
+ vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
+ callback(0);
+ return 0;
+ });
+ sessionStorage.setItem("scrollPosition", "345");
+
+ renderServerPage({
+ connected: true,
+ lastData: websocketPayload([createServer({ id: 1, name: "alpha" })]),
+ });
+
+ expect(scrollTo).not.toHaveBeenCalledWith({
+ top: 345,
+ left: 0,
+ behavior: "auto",
+ });
+ });
+
+ it("renders every card in large server lists without virtualization", () => {
+ const servers = Array.from({ length: 2000 }, (_, index) =>
+ createServer({ id: index + 1, name: `server-${index + 1}` }),
+ );
+
+ renderServerPage({
+ connected: true,
+ lastData: websocketPayload(servers),
+ });
+
+ expect(screen.getByTestId("server-overview")).toHaveTextContent(
+ "2000:2000:0",
+ );
+ expect(screen.getAllByTestId("server-card")).toHaveLength(2000);
+ });
+
+ it("toggles map and service tracker controls when service data exists", async () => {
+ apiMocks.fetchService.mockResolvedValue({
+ success: true,
+ data: {
+ services: {
+ http: {
+ service_name: "HTTP",
+ current_up: 1,
+ current_down: 0,
+ total_up: 1,
+ total_down: 0,
+ delay: [10],
+ up: [1],
+ down: [0],
+ },
+ },
+ cycle_transfer_stats: {},
+ },
+ });
+ const user = userEvent.setup();
+ const online = createServer({ id: 1, name: "alpha" });
+ const offline = createServer({
+ id: 2,
+ name: "beta",
+ last_active: "2024-12-31T23:00:00.000Z",
+ });
+
+ const { container } = renderServerPage({
+ connected: true,
+ lastData: websocketPayload([online, offline]),
+ });
+
+ await waitFor(() => {
+ expect(apiMocks.fetchService).toHaveBeenCalled();
+ expect(
+ container.querySelectorAll(
+ ".server-overview-controls section > button",
+ ),
+ ).toHaveLength(3);
+ });
+
+ const controls = container.querySelectorAll(
+ ".server-overview-controls section > button",
+ );
+ await user.click(controls[0]);
+ expect(screen.getByTestId("global-map")).toHaveTextContent("2");
+ expect(localStorage.getItem("showMap")).toBe("1");
+
+ await user.click(controls[1]);
+ expect(screen.getByTestId("service-tracker")).toHaveTextContent("2");
+ expect(localStorage.getItem("showServices")).toBe("1");
+ });
+
+ it("does not enable inline cards from storage on mobile widths", () => {
+ localStorage.setItem("inline", "1");
+ Object.defineProperty(window, "innerWidth", {
+ configurable: true,
+ value: 500,
+ });
+ const online = createServer({ id: 1, name: "alpha" });
+
+ renderServerPage({
+ connected: true,
+ lastData: websocketPayload([online]),
+ });
+
+ expect(screen.getByTestId("server-card")).toHaveTextContent("alpha");
+ expect(screen.queryByTestId("server-card-inline")).not.toBeInTheDocument();
+ });
+
+ it("applies external status filters and inline card preferences", async () => {
+ localStorage.setItem("inline", "1");
+ Object.defineProperty(window, "innerWidth", {
+ configurable: true,
+ value: 1024,
+ });
+ const online = createServer({ id: 1, name: "alpha" });
+ const offline = createServer({
+ id: 2,
+ name: "beta",
+ last_active: "2024-12-31T23:00:00.000Z",
+ });
+ const user = userEvent.setup();
+
+ renderServerPage(
+ {
+ connected: true,
+ lastData: websocketPayload([online, offline]),
+ },
+ { withStatusControl: true },
+ );
+
+ await waitFor(() => {
+ expect(screen.getAllByTestId("server-card-inline")).toHaveLength(2);
+ });
+
+ await user.click(screen.getByRole("button", { name: "online-only" }));
+
+ expect(screen.getAllByTestId("server-card-inline")).toHaveLength(1);
+ expect(screen.getByText("alpha")).toBeInTheDocument();
+ expect(screen.queryByText("beta")).not.toBeInTheDocument();
+ });
+});
diff --git a/src/test/pages/pages.test.tsx b/src/test/pages/pages.test.tsx
new file mode 100644
index 0000000..db095d1
--- /dev/null
+++ b/src/test/pages/pages.test.tsx
@@ -0,0 +1,133 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import ErrorPage from "@/pages/ErrorPage";
+import NotFound from "@/pages/NotFound";
+import ServerDetail from "@/pages/ServerDetail";
+
+vi.mock("@/components/NetworkChart", () => ({
+ NetworkChart: ({ server_id, show }: { server_id: number; show: boolean }) => (
+ {`${server_id}:${show}`}
+ ),
+}));
+
+vi.mock("@/components/ServerDetailChart", () => ({
+ default: ({ server_id }: { server_id: string }) => (
+ {server_id}
+ ),
+}));
+
+vi.mock("@/components/ServerDetailOverview", () => ({
+ default: ({ server_id }: { server_id: string }) => (
+ {server_id}
+ ),
+}));
+
+vi.mock("@/components/TabSwitch", () => ({
+ default: ({
+ tabs,
+ setCurrentTab,
+ }: {
+ tabs: string[];
+ setCurrentTab: (tab: string) => void;
+ }) => (
+
+ {tabs.map((tab) => (
+
+ ))}
+
+ ),
+}));
+
+function LocationProbe() {
+ const location = useLocation();
+ return {location.pathname}
;
+}
+
+describe("simple pages", () => {
+ it("renders explicit and translated error messages", () => {
+ const { rerender } = render();
+
+ expect(screen.getByText("418")).toBeInTheDocument();
+ expect(screen.getByText("short")).toBeInTheDocument();
+
+ rerender();
+ expect(screen.getByText("error.somethingWentWrong")).toBeInTheDocument();
+ });
+
+ it("navigates back home from the not found page", async () => {
+ const user = userEvent.setup();
+ render(
+
+
+
+
+
+ >
+ }
+ />
+
+ home
+
+ >
+ }
+ />
+
+ ,
+ );
+
+ expect(screen.getByText("404")).toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "error.backToHome" }));
+ expect(screen.getByText("home")).toBeInTheDocument();
+ expect(screen.getByText("/")).toBeInTheDocument();
+ });
+});
+
+describe("ServerDetail", () => {
+ beforeEach(() => {
+ vi.stubGlobal("scrollTo", vi.fn());
+ });
+
+ it("renders detail tab by default and can switch to network tab", async () => {
+ const user = userEvent.setup();
+ render(
+
+
+ } />
+
+ ,
+ );
+
+ expect(screen.getByTestId("detail-overview")).toHaveTextContent("7");
+ expect(screen.getByTestId("detail-chart")).toHaveTextContent("7");
+ expect(screen.queryByTestId("network-chart")).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "Network" }));
+ expect(await screen.findByTestId("network-chart")).toHaveTextContent(
+ "7:true",
+ );
+ expect(screen.queryByTestId("detail-chart")).not.toBeInTheDocument();
+ });
+
+ it("redirects when route params are missing", async () => {
+ render(
+
+
+ } />
+ redirected} />
+
+ ,
+ );
+
+ expect(await screen.findByText("redirected")).toBeInTheDocument();
+ });
+});
diff --git a/src/test/setup.ts b/src/test/setup.ts
new file mode 100644
index 0000000..43ddc31
--- /dev/null
+++ b/src/test/setup.ts
@@ -0,0 +1,110 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup } from "@testing-library/react";
+import { afterEach, vi } from "vitest";
+
+vi.mock("react-i18next", () => ({
+ initReactI18next: {
+ type: "3rdParty",
+ init: vi.fn(),
+ },
+ useTranslation: () => ({
+ t: (key: string) => key,
+ i18n: {
+ language: "en-US",
+ languages: ["en-US"],
+ changeLanguage: vi.fn(),
+ },
+ }),
+}));
+
+Object.defineProperty(window, "matchMedia", {
+ writable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+});
+
+class ResizeObserverMock {
+ observe = vi.fn();
+ unobserve = vi.fn();
+ disconnect = vi.fn();
+}
+
+class IntersectionObserverMock {
+ readonly root = null;
+ readonly rootMargin = "";
+ readonly thresholds = [];
+
+ observe = vi.fn();
+ unobserve = vi.fn();
+ disconnect = vi.fn();
+ takeRecords = vi.fn(() => []);
+}
+
+Object.defineProperty(globalThis, "ResizeObserver", {
+ writable: true,
+ value: ResizeObserverMock,
+});
+
+Object.defineProperty(globalThis, "IntersectionObserver", {
+ writable: true,
+ value: IntersectionObserverMock,
+});
+
+Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
+ configurable: true,
+ value: vi.fn(),
+});
+
+Object.defineProperty(HTMLElement.prototype, "hasPointerCapture", {
+ configurable: true,
+ value: vi.fn(() => false),
+});
+
+Object.defineProperty(HTMLElement.prototype, "setPointerCapture", {
+ configurable: true,
+ value: vi.fn(),
+});
+
+Object.defineProperty(HTMLElement.prototype, "releasePointerCapture", {
+ configurable: true,
+ value: vi.fn(),
+});
+
+afterEach(() => {
+ cleanup();
+ localStorage.clear();
+ sessionStorage.clear();
+ document.head
+ .querySelectorAll('meta[name="theme-color"], [data-injected]')
+ .forEach((node) => {
+ node.remove();
+ });
+ document.body.querySelectorAll("[data-injected]").forEach((node) => {
+ node.remove();
+ });
+ document.documentElement.className = "";
+ document.documentElement.removeAttribute("style");
+ Object.defineProperty(document, "cookie", {
+ configurable: true,
+ value: "",
+ });
+ window.CustomBackgroundImage = "";
+ window.CustomMobileBackgroundImage = "";
+ window.ForceShowServices = false;
+ window.ForceCardInline = false;
+ window.ForceShowMap = false;
+ window.ForcePeakCutEnabled = false;
+ window.ForceSortType = undefined;
+ window.ForceSortOrder = undefined;
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+ vi.useRealTimers();
+});
diff --git a/src/test/utils.tsx b/src/test/utils.tsx
new file mode 100644
index 0000000..e2c465b
--- /dev/null
+++ b/src/test/utils.tsx
@@ -0,0 +1,42 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { type RenderOptions, render } from "@testing-library/react";
+import type { ReactElement, ReactNode } from "react";
+import { MemoryRouter } from "react-router-dom";
+
+export function createTestQueryClient() {
+ return new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ gcTime: 0,
+ },
+ },
+ });
+}
+
+type RenderWithProvidersOptions = RenderOptions & {
+ route?: string;
+ queryClient?: QueryClient;
+};
+
+export function renderWithProviders(
+ ui: ReactElement,
+ {
+ route = "/",
+ queryClient = createTestQueryClient(),
+ ...renderOptions
+ }: RenderWithProvidersOptions = {},
+) {
+ function Wrapper({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return {
+ queryClient,
+ ...render(ui, { wrapper: Wrapper, ...renderOptions }),
+ };
+}
diff --git a/src/types/nezha-api.ts b/src/types/nezha-api.ts
index aff831e..23359e7 100644
--- a/src/types/nezha-api.ts
+++ b/src/types/nezha-api.ts
@@ -1,5 +1,6 @@
export interface NezhaWebsocketResponse {
now: number;
+ online?: number;
servers: NezhaServer[];
}
@@ -128,10 +129,10 @@ export interface CycleTransferStats {
export interface CycleTransferData {
name: string;
- from: string;
- to: string;
- max: number;
- min: number;
+ from: string | { [key: string]: string };
+ to: string | { [key: string]: string };
+ max: number | { [key: string]: number };
+ min: number | { [key: string]: number };
server_name: {
[key: string]: string;
};
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 84ae7c0..d6337b6 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -14,8 +14,7 @@
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
-
- "baseUrl": ".",
+ "types": ["node", "vitest/globals", "@testing-library/jest-dom"],
"paths": {
"@/*": ["./src/*"]
},
diff --git a/vite.config.ts b/vite.config.ts
index 261c0f7..23f21e5 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,8 +1,8 @@
import { execSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
-import react from '@vitejs/plugin-react'
-import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import { defineConfig } from "vitest/config";
// Get git commit hash
const getGitHash = () => {
@@ -63,7 +63,46 @@ export default defineConfig({
Pragma: "no-cache",
},
},
+ test: {
+ environment: "jsdom",
+ environmentOptions: {
+ jsdom: {
+ url: "https://localhost/",
+ },
+ },
+ globals: true,
+ setupFiles: ["./src/test/setup.ts"],
+ css: true,
+ include: ["src/test/**/*.{test,spec}.{ts,tsx}"],
+ coverage: {
+ provider: "v8",
+ reporter: ["text", "html", "lcov"],
+ reportsDirectory: "./coverage",
+ thresholds: {
+ statements: 86,
+ branches: 74,
+ functions: 86,
+ lines: 86,
+ },
+ include: ["src/**/*.{ts,tsx}"],
+ exclude: [
+ "src/**/*.test.{ts,tsx}",
+ "src/**/*.spec.{ts,tsx}",
+ "src/test/**",
+ "src/types/**",
+ "src/main.tsx",
+ "src/i18n.js",
+ "src/vite-env.d.ts",
+ ],
+ },
+ },
build: {
+ // Target older Safari versions (iOS 15/16) explicitly, since Vite's
+ // default "widely-available browsers" target only covers the latest
+ // two major Safari releases and would otherwise emit syntax that
+ // crashes on older WebKit engines (white screen on load).
+ target: ["es2020", "safari15"],
+ cssTarget: ["safari15"],
rolldownOptions: {
output: {
entryFileNames: `assets/[name].[hash].js`,