mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-05-06 05:48:41 +00:00
Compare commits
52 Commits
9d1f5a1c50
...
42f62f7d5b
| Author | SHA1 | Date | |
|---|---|---|---|
| 42f62f7d5b | |||
| b668063c52 | |||
| cb436904ea | |||
| 191d44bfd0 | |||
| cc1678a21e | |||
| 6398f73f7d | |||
| 2dcf6fb9d8 | |||
| 9a21a664c9 | |||
| ffb6102211 | |||
| f7662d2751 | |||
| e4ba96ea76 | |||
| d7f4f61001 | |||
| eceadb6bff | |||
| b030cd45d6 | |||
| 3be914cecc | |||
| d82c4c4aeb | |||
| 7b085915d9 | |||
| e700ef40d5 | |||
| caf2e20c58 | |||
| 816f6e7f9e | |||
| abb39c3dc1 | |||
| 7e4bed4f6c | |||
| 6172c641a0 | |||
| 0c7c6a1378 | |||
| 7d59371ee3 | |||
| a303a5add2 | |||
| 18e3c74178 | |||
| 6dae6cce8f | |||
| 65b32ec81e | |||
| 1aa66f98ed | |||
| 76590a6bd0 | |||
| af9926e774 | |||
| cc0f5c0a2e | |||
| ad4455e61d | |||
| a6e45eed46 | |||
| 92fada4792 | |||
| 29e349505d | |||
| 3bfd4ef4d2 | |||
| 5a0c873dc8 | |||
| c989a67265 | |||
| 42f99a6f84 | |||
| b4f2abb885 | |||
| 746f890d65 | |||
| 4d4c3f1639 | |||
| 4f9db466a3 | |||
| 409ec0b62c | |||
| 15424513b5 | |||
| ae58d24ba8 | |||
| 9aa83c1a9d | |||
| 2cc5926fbe | |||
| 1fda5ada9f | |||
| 48704b1135 |
@@ -34,6 +34,7 @@ jobs:
|
|||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
|
draft: ${{ contains(github.ref_name, '-alpha') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-rc') }}
|
||||||
files: dist.zip
|
files: dist.zip
|
||||||
|
|
||||||
- name: Changelog
|
- name: Changelog
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run build
|
||||||
|
run: pnpm run build
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": false,
|
|
||||||
"printWidth": 150,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
|
|
||||||
"importOrderSeparation": true,
|
|
||||||
"importOrderSortSpecifiers": true,
|
|
||||||
"endOfLine": "auto",
|
|
||||||
"plugins": ["prettier-plugin-tailwindcss", "@trivago/prettier-plugin-sort-imports"]
|
|
||||||
}
|
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
> 交流
|
> 交流
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> 此项目为 nezha-dash 的官方实现,作为哪吒监控 V1 版本的默认前端,功能上可能与 nezha-dash 有所不同。
|
> 此项目为 nezha-dash 的官方实现,作为哪吒监控的默认前端,功能上可能与 nezha-dash 有所不同。
|
||||||
> https://github.com/hamster1963/nezha-dash
|
> https://github.com/hamster1963/nezha-dash
|
||||||
|
|||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
|
||||||
|
"vcs": {
|
||||||
|
"enabled": true,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": true
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"includes": ["**", "!!**/dist"]
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "tab"
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true,
|
||||||
|
"suspicious": {
|
||||||
|
"noArrayIndexKey": "off",
|
||||||
|
"noExplicitAny": "off"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"noDangerouslySetInnerHtml": "off"
|
||||||
|
},
|
||||||
|
"a11y": {
|
||||||
|
"useButtonType": "off",
|
||||||
|
"noSvgWithoutTitle": "off",
|
||||||
|
"useKeyWithClickEvents": "off",
|
||||||
|
"noStaticElementInteractions": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"css": {
|
||||||
|
"parser": {
|
||||||
|
"tailwindDirectives": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "double"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"assist": {
|
||||||
|
"enabled": true,
|
||||||
|
"actions": {
|
||||||
|
"source": {
|
||||||
|
"organizeImports": "on"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,709 @@
|
|||||||
|
{
|
||||||
|
"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=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-13
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"types": {
|
"types": {
|
||||||
"feat": { "title": "🚀 Features" },
|
"feat": { "title": "🚀 Features" },
|
||||||
"fix": { "title": "🔧 Bug Fixes" },
|
"fix": { "title": "🔧 Bug Fixes" },
|
||||||
"docs": { "title": "📚 Documentation" },
|
"docs": { "title": "📚 Documentation" },
|
||||||
"style": { "title": "💄 Styles" },
|
"style": { "title": "💄 Styles" },
|
||||||
"refactor": { "title": "🔨 Refactor" },
|
"refactor": { "title": "🔨 Refactor" },
|
||||||
"perf": { "title": "🏎 Performance" },
|
"perf": { "title": "🏎 Performance" },
|
||||||
"test": { "title": "🚨 Tests" },
|
"test": { "title": "🚨 Tests" },
|
||||||
"build": { "title": "🛠 Build" },
|
"build": { "title": "🛠 Build" },
|
||||||
"ci": { "title": "👷 CI" },
|
"ci": { "title": "👷 CI" },
|
||||||
"chore": { "title": "🛗 Chore" },
|
"chore": { "title": "🛗 Chore" },
|
||||||
"revert": { "title": "⏪ Revert" }
|
"revert": { "title": "⏪ Revert" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-18
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "default",
|
"style": "default",
|
||||||
"rsc": false,
|
"rsc": false,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.js",
|
"config": "tailwind.config.js",
|
||||||
"css": "src/index.css",
|
"css": "src/index.css",
|
||||||
"baseColor": "stone",
|
"baseColor": "stone",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "@/components/ui",
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import js from "@eslint/js"
|
|
||||||
import reactHooks from "eslint-plugin-react-hooks"
|
|
||||||
import reactRefresh from "eslint-plugin-react-refresh"
|
|
||||||
import globals from "globals"
|
|
||||||
import tseslint from "typescript-eslint"
|
|
||||||
|
|
||||||
export default tseslint.config(
|
|
||||||
{ ignores: ["dist"] },
|
|
||||||
{
|
|
||||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
|
||||||
files: ["**/*.{ts,tsx}"],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
"react-hooks": reactHooks,
|
|
||||||
"react-refresh": reactRefresh,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...reactHooks.configs.recommended.rules,
|
|
||||||
"react-refresh/only-export-components": "off",
|
|
||||||
"react-hooks/exhaustive-deps": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-empty-object-type": "off",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
+5
-5
@@ -10,7 +10,7 @@
|
|||||||
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
||||||
}
|
}
|
||||||
document.documentElement.classList.add(theme)
|
document.documentElement.classList.add(theme)
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
document.documentElement.classList.add("light")
|
document.documentElement.classList.add("light")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -36,15 +36,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
background-color: var(--bg) !important;
|
background-color: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--bg) !important;
|
background-color: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
background-color: var(--bg) !important;
|
background-color: var(--bg);
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
;(function () {
|
;(() => {
|
||||||
const storageKey = "vite-ui-theme"
|
const storageKey = "vite-ui-theme"
|
||||||
const theme = localStorage.getItem(storageKey) || "system"
|
const theme = localStorage.getItem(storageKey) || "system"
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
|
|||||||
+68
-72
@@ -1,74 +1,70 @@
|
|||||||
{
|
{
|
||||||
"name": "nazha-dash-v1",
|
"name": "nazha-dash-v2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "2.0.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "biome lint",
|
||||||
"lint:fix": "eslint --fix .",
|
"lint:fix": "biome lint --fix",
|
||||||
"format": "prettier --write .",
|
"format": "biome format --write .",
|
||||||
"preview": "vite preview"
|
"check": "biome check",
|
||||||
},
|
"check:fix": "biome check --fix --unsafe",
|
||||||
"dependencies": {
|
"preview": "vite preview"
|
||||||
"@fontsource/inter": "5.1.1",
|
},
|
||||||
"@heroicons/react": "2.2.0",
|
"dependencies": {
|
||||||
"@number-flow/react": "0.5.5",
|
"@fontsource/inter": "5.1.1",
|
||||||
"@radix-ui/react-accordion": "1.2.3",
|
"@heroicons/react": "2.2.0",
|
||||||
"@radix-ui/react-checkbox": "1.1.4",
|
"@number-flow/react": "0.5.5",
|
||||||
"@radix-ui/react-dialog": "1.1.6",
|
"@radix-ui/react-accordion": "1.2.3",
|
||||||
"@radix-ui/react-dropdown-menu": "2.1.6",
|
"@radix-ui/react-checkbox": "1.1.4",
|
||||||
"@radix-ui/react-label": "2.1.2",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-popover": "1.1.6",
|
"@radix-ui/react-dropdown-menu": "2.1.6",
|
||||||
"@radix-ui/react-progress": "1.1.2",
|
"@radix-ui/react-label": "2.1.2",
|
||||||
"@radix-ui/react-select": "2.1.6",
|
"@radix-ui/react-popover": "1.1.6",
|
||||||
"@radix-ui/react-separator": "1.1.2",
|
"@radix-ui/react-progress": "1.1.2",
|
||||||
"@radix-ui/react-slot": "1.1.2",
|
"@radix-ui/react-select": "2.1.6",
|
||||||
"@radix-ui/react-switch": "1.1.3",
|
"@radix-ui/react-separator": "1.1.2",
|
||||||
"@radix-ui/react-tooltip": "1.1.8",
|
"@radix-ui/react-slot": "1.1.2",
|
||||||
"@tanstack/react-query": "5.66.7",
|
"@radix-ui/react-switch": "1.1.3",
|
||||||
"@tanstack/react-query-devtools": "5.66.7",
|
"@radix-ui/react-tooltip": "1.1.8",
|
||||||
"@tanstack/react-table": "8.21.2",
|
"@tanstack/react-query": "5.66.7",
|
||||||
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
"@tanstack/react-query-devtools": "5.66.7",
|
||||||
"@types/d3-geo": "3.1.0",
|
"@tanstack/react-table": "8.21.2",
|
||||||
"@types/luxon": "3.4.2",
|
"@types/d3-geo": "3.1.0",
|
||||||
"class-variance-authority": "0.7.1",
|
"@types/luxon": "3.4.2",
|
||||||
"clsx": "2.1.1",
|
"class-variance-authority": "0.7.1",
|
||||||
"cmdk": "1.1.1",
|
"clsx": "2.1.1",
|
||||||
"country-flag-icons": "1.5.18",
|
"cmdk": "1.1.1",
|
||||||
"d3-geo": "3.1.1",
|
"country-flag-icons": "1.5.18",
|
||||||
"dayjs": "1.11.13",
|
"d3-geo": "3.1.1",
|
||||||
"framer-motion": "11.18.2",
|
"dayjs": "1.11.13",
|
||||||
"i18n-iso-countries": "7.14.0",
|
"framer-motion": "12.23.26",
|
||||||
"i18next": "24.2.2",
|
"i18n-iso-countries": "7.14.0",
|
||||||
"lucide-react": "0.460.0",
|
"i18next": "24.2.2",
|
||||||
"luxon": "3.5.0",
|
"lucide-react": "0.460.0",
|
||||||
"prettier-plugin-tailwindcss": "0.6.11",
|
"luxon": "3.5.0",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-i18next": "15.4.1",
|
"react-i18next": "15.4.1",
|
||||||
"react-router-dom": "7.2.0",
|
"react-router-dom": "^7.13.2",
|
||||||
"recharts": "2.15.1",
|
"recharts": "2.15.1",
|
||||||
"sonner": "1.7.4",
|
"sonner": "1.7.4",
|
||||||
"tailwind-merge": "2.6.0",
|
"tailwind-merge": "2.6.0",
|
||||||
"tailwindcss-animate": "1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.20.0",
|
"@biomejs/biome": "2.3.10",
|
||||||
"@types/node": "22.13.4",
|
"@tailwindcss/postcss": "^4.2.2",
|
||||||
"@types/react": "19.0.10",
|
"@types/node": "22.13.4",
|
||||||
"@types/react-dom": "19.0.4",
|
"@types/react": "19.0.10",
|
||||||
"@vitejs/plugin-react-swc": "3.8.0",
|
"@types/react-dom": "19.0.4",
|
||||||
"autoprefixer": "10.4.20",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"eslint": "9.20.1",
|
"globals": "15.15.0",
|
||||||
"eslint-plugin-react-hooks": "5.1.0",
|
"postcss": "8.5.3",
|
||||||
"eslint-plugin-react-refresh": "0.4.19",
|
"tailwindcss": "^4.2.2",
|
||||||
"globals": "15.15.0",
|
"typescript": "~5.6.3",
|
||||||
"postcss": "8.5.3",
|
"vite": "8.0.3"
|
||||||
"tailwindcss": "3.4.17",
|
}
|
||||||
"typescript": "~5.6.3",
|
|
||||||
"typescript-eslint": "8.24.1",
|
|
||||||
"vite": "6.1.1"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+911
-2355
File diff suppressed because it is too large
Load Diff
+4
-5
@@ -1,6 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
"@tailwindcss/postcss": {},
|
||||||
autoprefixer: {},
|
},
|
||||||
},
|
};
|
||||||
}
|
|
||||||
|
|||||||
+15
-15
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "Nezha Monitoring",
|
"name": "Nezha Monitoring",
|
||||||
"short_name": "Nezha Monitoring",
|
"short_name": "Nezha Monitoring",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/android-chrome-192x192.png",
|
"src": "/android-chrome-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"theme_color": "hsl(0 0% 98%)",
|
"theme_color": "hsl(0 0% 98%)",
|
||||||
"background_color": "hsl(0 0% 98%)",
|
"background_color": "hsl(0 0% 98%)",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"orientation": "portrait"
|
"orientation": "portrait"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const { execSync } = require("child_process")
|
const { execSync } = require("node:child_process");
|
||||||
|
|
||||||
// Get the short version of the git hash
|
// Get the short version of the git hash
|
||||||
const gitHash = execSync("git rev-parse --short HEAD").toString().trim()
|
const gitHash = execSync("git rev-parse --short HEAD").toString().trim();
|
||||||
|
|
||||||
// Write it to stdout
|
// Write it to stdout
|
||||||
console.log(gitHash)
|
console.log(gitHash);
|
||||||
|
|||||||
+153
-105
@@ -1,109 +1,157 @@
|
|||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import React, { useEffect, useState } from "react"
|
import { DateTime } from "luxon";
|
||||||
import { useTranslation } from "react-i18next"
|
import type React from "react";
|
||||||
import { Route, BrowserRouter as Router, Routes } from "react-router-dom"
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
|
||||||
|
|
||||||
import { DashCommand } from "./components/DashCommand"
|
import { DashCommand } from "./components/DashCommand";
|
||||||
import ErrorBoundary from "./components/ErrorBoundary"
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import Footer from "./components/Footer"
|
import Footer from "./components/Footer";
|
||||||
import Header, { RefreshToast } from "./components/Header"
|
import Header, { RefreshToast } from "./components/Header";
|
||||||
import { useBackground } from "./hooks/use-background"
|
import { useBackground } from "./hooks/use-background";
|
||||||
import { useTheme } from "./hooks/use-theme"
|
import { useTheme } from "./hooks/use-theme";
|
||||||
import { InjectContext } from "./lib/inject"
|
import { InjectContext } from "./lib/inject";
|
||||||
import { fetchSetting } from "./lib/nezha-api"
|
import { fetchSetting } from "./lib/nezha-api";
|
||||||
import { cn } from "./lib/utils"
|
import { cn } from "./lib/utils";
|
||||||
import ErrorPage from "./pages/ErrorPage"
|
import ErrorPage from "./pages/ErrorPage";
|
||||||
import NotFound from "./pages/NotFound"
|
import NotFound from "./pages/NotFound";
|
||||||
import Server from "./pages/Server"
|
import Server from "./pages/Server";
|
||||||
import ServerDetail from "./pages/ServerDetail"
|
import ServerDetail from "./pages/ServerDetail";
|
||||||
|
|
||||||
|
// Route checker component
|
||||||
|
const RouteChecker: React.FC = () => {
|
||||||
|
return <MainApp />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MainApp: React.FC = () => {
|
||||||
|
const { data: settingData, error } = useQuery({
|
||||||
|
queryKey: ["setting"],
|
||||||
|
queryFn: () => fetchSetting(),
|
||||||
|
refetchOnMount: true,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
});
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
const { setTheme } = useTheme();
|
||||||
|
const [isCustomCodeInjected, setIsCustomCodeInjected] = useState(false);
|
||||||
|
const { backgroundImage: customBackgroundImage } = useBackground();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (settingData?.data?.config?.custom_code) {
|
||||||
|
InjectContext(settingData?.data?.config?.custom_code);
|
||||||
|
setIsCustomCodeInjected(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步自定义配置到全局变量
|
||||||
|
const config = settingData?.data?.config;
|
||||||
|
if (config) {
|
||||||
|
if (config.custom_logo) window.CustomLogo = config.custom_logo;
|
||||||
|
if (config.custom_description)
|
||||||
|
window.CustomDesc = config.custom_description;
|
||||||
|
if (config.custom_links) window.CustomLinks = config.custom_links;
|
||||||
|
|
||||||
|
const hour = DateTime.now().hour;
|
||||||
|
const isNight = hour >= 18 || hour < 6;
|
||||||
|
|
||||||
|
if (isNight && config.background_image_night) {
|
||||||
|
window.CustomBackgroundImage = config.background_image_night;
|
||||||
|
} else if (!isNight && config.background_image_day) {
|
||||||
|
window.CustomBackgroundImage = config.background_image_day;
|
||||||
|
}
|
||||||
|
window.CustomMobileBackgroundImage = window.CustomBackgroundImage;
|
||||||
|
}
|
||||||
|
}, [settingData]);
|
||||||
|
|
||||||
|
// 检测是否强制指定了主题颜色
|
||||||
|
const forceTheme =
|
||||||
|
(window.ForceTheme as string) !== "" ? window.ForceTheme : undefined;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedTheme = localStorage.getItem("vite-ui-theme");
|
||||||
|
// Only auto-apply ForceTheme if the user hasn't manually picked one (or picked 'system')
|
||||||
|
if (
|
||||||
|
(!savedTheme || savedTheme === "system") &&
|
||||||
|
(forceTheme === "dark" || forceTheme === "light")
|
||||||
|
) {
|
||||||
|
setTheme(forceTheme);
|
||||||
|
}
|
||||||
|
}, [forceTheme, setTheme]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorPage code={500} message={error.message} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settingData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingData?.data?.config?.custom_code && !isCustomCodeInjected) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
settingData?.data?.config?.language &&
|
||||||
|
!localStorage.getItem("language")
|
||||||
|
) {
|
||||||
|
i18n.changeLanguage(settingData?.data?.config?.language);
|
||||||
|
}
|
||||||
|
|
||||||
|
const customMobileBackgroundImage =
|
||||||
|
window.CustomMobileBackgroundImage !== ""
|
||||||
|
? window.CustomMobileBackgroundImage
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
{/* 固定定位的背景层 */}
|
||||||
|
{customBackgroundImage && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center dark:brightness-75",
|
||||||
|
{
|
||||||
|
"hidden sm:block": customMobileBackgroundImage,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
style={{ backgroundImage: `url(${customBackgroundImage})` }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{customMobileBackgroundImage && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center sm:hidden dark:brightness-75",
|
||||||
|
)}
|
||||||
|
style={{ backgroundImage: `url(${customMobileBackgroundImage})` }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn("flex min-h-screen w-full flex-col", {
|
||||||
|
"bg-background": !customBackgroundImage,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<main className="flex z-20 min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 p-4 md:p-10 md:pt-8">
|
||||||
|
<RefreshToast />
|
||||||
|
<Header />
|
||||||
|
<DashCommand />
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Server />} />
|
||||||
|
<Route path="/server/:id" element={<ServerDetail />} />
|
||||||
|
<Route path="/error" element={<ErrorPage />} />
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
|
<Footer />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main App wrapper with router
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const { data: settingData, error } = useQuery({
|
return (
|
||||||
queryKey: ["setting"],
|
<Router basename={import.meta.env.BASE_URL}>
|
||||||
queryFn: () => fetchSetting(),
|
<RouteChecker />
|
||||||
refetchOnMount: true,
|
</Router>
|
||||||
refetchOnWindowFocus: true,
|
);
|
||||||
})
|
};
|
||||||
const { i18n } = useTranslation()
|
|
||||||
const { setTheme } = useTheme()
|
|
||||||
const [isCustomCodeInjected, setIsCustomCodeInjected] = useState(false)
|
|
||||||
const { backgroundImage: customBackgroundImage } = useBackground()
|
|
||||||
|
|
||||||
useEffect(() => {
|
export default App;
|
||||||
if (settingData?.data?.config?.custom_code) {
|
|
||||||
InjectContext(settingData?.data?.config?.custom_code)
|
|
||||||
setIsCustomCodeInjected(true)
|
|
||||||
}
|
|
||||||
}, [settingData?.data?.config?.custom_code])
|
|
||||||
|
|
||||||
// 检测是否强制指定了主题颜色
|
|
||||||
const forceTheme =
|
|
||||||
// @ts-expect-error ForceTheme is a global variable
|
|
||||||
(window.ForceTheme as string) !== "" ? window.ForceTheme : undefined
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (forceTheme === "dark" || forceTheme === "light") {
|
|
||||||
setTheme(forceTheme)
|
|
||||||
}
|
|
||||||
}, [forceTheme])
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <ErrorPage code={500} message={error.message} />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!settingData) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settingData?.data?.config?.custom_code && !isCustomCodeInjected) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settingData?.data?.config?.language && !localStorage.getItem("language")) {
|
|
||||||
i18n.changeLanguage(settingData?.data?.config?.language)
|
|
||||||
}
|
|
||||||
|
|
||||||
const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Router basename={import.meta.env.BASE_URL}>
|
|
||||||
<ErrorBoundary>
|
|
||||||
{/* 固定定位的背景层 */}
|
|
||||||
{customBackgroundImage && (
|
|
||||||
<div
|
|
||||||
className={cn("fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center dark:brightness-75", {
|
|
||||||
"hidden sm:block": customMobileBackgroundImage,
|
|
||||||
})}
|
|
||||||
style={{ backgroundImage: `url(${customBackgroundImage})` }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{customMobileBackgroundImage && (
|
|
||||||
<div
|
|
||||||
className={cn("fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center sm:hidden dark:brightness-75")}
|
|
||||||
style={{ backgroundImage: `url(${customMobileBackgroundImage})` }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cn("flex min-h-screen w-full flex-col", {
|
|
||||||
"bg-background": !customBackgroundImage,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<main className="flex z-20 min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 p-4 md:p-10 md:pt-8">
|
|
||||||
<RefreshToast />
|
|
||||||
<Header />
|
|
||||||
<DashCommand />
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<Server />} />
|
|
||||||
<Route path="/server/:id" element={<ServerDetail />} />
|
|
||||||
<Route path="/error" element={<ErrorPage />} />
|
|
||||||
<Route path="*" element={<NotFound />} />
|
|
||||||
</Routes>
|
|
||||||
<Footer />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</Router>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function AnimateCountClient({
|
||||||
|
count,
|
||||||
|
className,
|
||||||
|
minDigits,
|
||||||
|
}: {
|
||||||
|
count: number;
|
||||||
|
className?: string;
|
||||||
|
minDigits?: number;
|
||||||
|
}) {
|
||||||
|
const [previousCount, setPreviousCount] = useState(count);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (count !== previousCount) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setPreviousCount(count);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}, [count, previousCount]);
|
||||||
|
return (
|
||||||
|
<AnimateCount
|
||||||
|
key={count}
|
||||||
|
preCount={previousCount}
|
||||||
|
className={cn("inline-flex items-center leading-none", className)}
|
||||||
|
minDigits={minDigits}
|
||||||
|
data-issues-count-animation
|
||||||
|
>
|
||||||
|
{count}
|
||||||
|
</AnimateCount>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnimateCountClient;
|
||||||
|
|
||||||
|
export function AnimateCount({
|
||||||
|
children: count,
|
||||||
|
className,
|
||||||
|
preCount,
|
||||||
|
minDigits = 1,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
children: number;
|
||||||
|
className?: string;
|
||||||
|
preCount?: number;
|
||||||
|
minDigits?: number;
|
||||||
|
}) {
|
||||||
|
const currentDigits = count.toString().split("");
|
||||||
|
const previousDigits = (
|
||||||
|
preCount !== undefined
|
||||||
|
? preCount.toString()
|
||||||
|
: count - 1 >= 0
|
||||||
|
? (count - 1).toString()
|
||||||
|
: "0"
|
||||||
|
).split("");
|
||||||
|
|
||||||
|
// Ensure both numbers meet the minimum length requirement and maintain the same length for animation
|
||||||
|
const maxLength = Math.max(
|
||||||
|
previousDigits.length,
|
||||||
|
currentDigits.length,
|
||||||
|
minDigits,
|
||||||
|
);
|
||||||
|
while (previousDigits.length < maxLength) {
|
||||||
|
previousDigits.unshift("0");
|
||||||
|
}
|
||||||
|
while (currentDigits.length < maxLength) {
|
||||||
|
currentDigits.unshift("0");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...props} className={cn("flex h-[1em] items-center", className)}>
|
||||||
|
{currentDigits.map((digit, index) => {
|
||||||
|
const hasChanged = digit !== previousDigits[index];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${index}-${digit}`}
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-full min-w-[0.6em] items-center text-center",
|
||||||
|
{
|
||||||
|
"min-w-[0.2em]": digit === ".",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
data-issues-count-exit
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 flex items-center justify-center",
|
||||||
|
hasChanged ? "animate" : "opacity-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{previousDigits[index]}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
data-issues-count-enter
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 flex items-center justify-center",
|
||||||
|
hasChanged && "animate",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{digit}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,62 +1,68 @@
|
|||||||
import { CycleTransferStats, NezhaServer } from "@/types/nezha-api"
|
import type React from "react";
|
||||||
import React from "react"
|
import type { CycleTransferStats, NezhaServer } from "@/types/nezha-api";
|
||||||
|
|
||||||
import { CycleTransferStatsClient } from "./CycleTransferStatsClient"
|
import { CycleTransferStatsClient } from "./CycleTransferStatsClient";
|
||||||
|
|
||||||
interface CycleTransferStatsProps {
|
interface CycleTransferStatsProps {
|
||||||
serverList: NezhaServer[]
|
serverList: NezhaServer[];
|
||||||
cycleStats: CycleTransferStats
|
cycleStats: CycleTransferStats;
|
||||||
className?: string
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({ serverList, cycleStats, className }) => {
|
export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({
|
||||||
if (serverList.length === 0) {
|
serverList,
|
||||||
return null
|
cycleStats,
|
||||||
}
|
className,
|
||||||
|
}) => {
|
||||||
|
if (serverList.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const serverIdList = serverList.map((server) => server.id.toString())
|
const serverIdList = serverList.map((server) => server.id.toString());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
<section className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
{Object.entries(cycleStats).map(([cycleId, cycleData]) => {
|
{Object.entries(cycleStats).map(([cycleId, cycleData]) => {
|
||||||
if (!cycleData.server_name) {
|
if (!cycleData.server_name) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.entries(cycleData.server_name).map(([serverId, serverName]) => {
|
return Object.entries(cycleData.server_name).map(
|
||||||
const transfer = cycleData.transfer?.[serverId] || 0
|
([serverId, serverName]) => {
|
||||||
const nextUpdate = cycleData.next_update?.[serverId]
|
const transfer = cycleData.transfer?.[serverId] || 0;
|
||||||
|
const nextUpdate = cycleData.next_update?.[serverId];
|
||||||
|
|
||||||
if (!serverIdList.includes(serverId)) {
|
if (!serverIdList.includes(serverId)) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!transfer && !nextUpdate) {
|
if (!transfer && !nextUpdate) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CycleTransferStatsClient
|
<CycleTransferStatsClient
|
||||||
key={`${cycleId}-${serverId}`}
|
key={`${cycleId}-${serverId}`}
|
||||||
name={cycleData.name}
|
name={cycleData.name}
|
||||||
from={cycleData.from}
|
from={cycleData.from}
|
||||||
to={cycleData.to}
|
to={cycleData.to}
|
||||||
max={cycleData.max}
|
max={cycleData.max}
|
||||||
serverStats={[
|
serverStats={[
|
||||||
{
|
{
|
||||||
serverId,
|
serverId,
|
||||||
serverName,
|
serverName,
|
||||||
transfer,
|
transfer,
|
||||||
nextUpdate: nextUpdate || "",
|
nextUpdate: nextUpdate || "",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
className={className}
|
className={className}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
},
|
||||||
})}
|
);
|
||||||
</section>
|
})}
|
||||||
)
|
</section>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default CycleTransferStatsCard
|
export default CycleTransferStatsCard;
|
||||||
|
|||||||
@@ -1,79 +1,96 @@
|
|||||||
import { formatBytes } from "@/lib/format"
|
import type React from "react";
|
||||||
import { cn } from "@/lib/utils"
|
import { useTranslation } from "react-i18next";
|
||||||
import React from "react"
|
import { formatBytes } from "@/lib/format";
|
||||||
import { useTranslation } from "react-i18next"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface CycleTransferStatsClientProps {
|
interface CycleTransferStatsClientProps {
|
||||||
name: string
|
name: string;
|
||||||
from: string
|
from: string;
|
||||||
to: string
|
to: string;
|
||||||
max: number
|
max: number;
|
||||||
serverStats: Array<{
|
serverStats: Array<{
|
||||||
serverId: string
|
serverId: string;
|
||||||
serverName: string
|
serverName: string;
|
||||||
transfer: number
|
transfer: number;
|
||||||
nextUpdate: string
|
nextUpdate: string;
|
||||||
}>
|
}>;
|
||||||
className?: string
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> = ({ name, from, to, max, serverStats, className }) => {
|
export const CycleTransferStatsClient: React.FC<
|
||||||
const { t } = useTranslation()
|
CycleTransferStatsClientProps
|
||||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
> = ({ name, from, to, max, serverStats, className }) => {
|
||||||
return (
|
const { t } = useTranslation();
|
||||||
<div
|
const customBackgroundImage =
|
||||||
className={cn(
|
(window.CustomBackgroundImage as string) !== ""
|
||||||
"w-full bg-white px-4 py-3.5 rounded-lg border bg-card text-card-foreground hover:shadow-sm transition-all duration-200 dark:shadow-none",
|
? window.CustomBackgroundImage
|
||||||
className,
|
: undefined;
|
||||||
{
|
return (
|
||||||
"bg-card/70": customBackgroundImage,
|
<div
|
||||||
},
|
className={cn(
|
||||||
)}
|
"w-full bg-white px-4 py-3.5 rounded-lg border bg-card text-card-foreground hover:shadow-xs transition-all duration-200 dark:shadow-none",
|
||||||
>
|
className,
|
||||||
{serverStats.map(({ serverId, serverName, transfer, nextUpdate }) => {
|
{
|
||||||
const progress = (transfer / max) * 100
|
"bg-card/70": customBackgroundImage,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{serverStats.map(({ serverId, serverName, transfer, nextUpdate }) => {
|
||||||
|
const progress = (transfer / max) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={serverId} className="space-y-3">
|
<div key={serverId} className="space-y-3">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{serverName}</span>
|
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||||
<div className="bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-2 py-0.5 rounded text-xs font-medium">{name}</div>
|
{serverName}
|
||||||
</div>
|
</span>
|
||||||
|
<div className="bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-2 py-0.5 rounded text-xs font-medium">
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Progress Section */}
|
{/* Progress Section */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-baseline gap-1">
|
<div className="flex items-baseline gap-1">
|
||||||
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{formatBytes(transfer)}</span>
|
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">/ {formatBytes(max)}</span>
|
{formatBytes(transfer)}
|
||||||
</div>
|
</span>
|
||||||
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">{progress.toFixed(1)}%</span>
|
<span className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
</div>
|
/ {formatBytes(max)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">
|
||||||
|
{progress.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="relative h-1.5">
|
<div className="relative h-1.5">
|
||||||
<div className="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full" />
|
<div className="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full" />
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300"
|
className="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300"
|
||||||
style={{ width: `${Math.min(progress, 100)}%` }}
|
style={{ width: `${Math.min(progress, 100)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400">
|
<div className="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400">
|
||||||
<span>
|
<span>
|
||||||
{new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()}
|
{new Date(from).toLocaleDateString()} -{" "}
|
||||||
</span>
|
{new Date(to).toLocaleDateString()}
|
||||||
<span>
|
</span>
|
||||||
{t("cycleTransfer.nextUpdate")}: {new Date(nextUpdate).toLocaleString()}
|
<span>
|
||||||
</span>
|
{t("cycleTransfer.nextUpdate")}:{" "}
|
||||||
</div>
|
{new Date(nextUpdate).toLocaleString()}
|
||||||
</div>
|
</span>
|
||||||
)
|
</div>
|
||||||
})}
|
</div>
|
||||||
</div>
|
);
|
||||||
)
|
})}
|
||||||
}
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default CycleTransferStatsClient
|
export default CycleTransferStatsClient;
|
||||||
|
|||||||
+117
-106
@@ -1,118 +1,129 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command"
|
import { Home, Moon, Sun, SunMoon } from "lucide-react";
|
||||||
import { useTheme } from "@/hooks/use-theme"
|
import { useEffect, useState } from "react";
|
||||||
import { useWebSocketContext } from "@/hooks/use-websocket-context"
|
import { useTranslation } from "react-i18next";
|
||||||
import { formatNezhaInfo } from "@/lib/utils"
|
import { useNavigate } from "react-router-dom";
|
||||||
import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
import {
|
||||||
import { Home, Moon, Sun, SunMoon } from "lucide-react"
|
CommandDialog,
|
||||||
import { useEffect, useState } from "react"
|
CommandEmpty,
|
||||||
import { useTranslation } from "react-i18next"
|
CommandGroup,
|
||||||
import { useNavigate } from "react-router-dom"
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
CommandSeparator,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import { useCommand } from "@/hooks/use-command";
|
||||||
|
import { useTheme } from "@/hooks/use-theme";
|
||||||
|
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||||
|
import { formatNezhaInfo } from "@/lib/utils";
|
||||||
|
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
|
||||||
|
|
||||||
export function DashCommand() {
|
export function DashCommand() {
|
||||||
const [open, setOpen] = useState(false)
|
const { isOpen, closeCommand, toggleCommand } = useCommand();
|
||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState("");
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const { setTheme } = useTheme()
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
const { lastMessage, connected } = useWebSocketContext()
|
const { lastMessage, connected } = useWebSocketContext();
|
||||||
|
|
||||||
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
|
const nezhaWsData = lastMessage
|
||||||
|
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
|
||||||
|
: null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
setOpen((open) => !open)
|
toggleCommand();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
document.addEventListener("keydown", down)
|
document.addEventListener("keydown", down);
|
||||||
return () => document.removeEventListener("keydown", down)
|
return () => document.removeEventListener("keydown", down);
|
||||||
}, [])
|
}, [toggleCommand]);
|
||||||
|
|
||||||
if (!connected || !nezhaWsData) return null
|
if (!connected || !nezhaWsData) return null;
|
||||||
|
|
||||||
const shortcuts = [
|
const shortcuts = [
|
||||||
{
|
{
|
||||||
keywords: ["home", "homepage"],
|
keywords: ["home", "homepage"],
|
||||||
icon: <Home />,
|
icon: <Home />,
|
||||||
label: t("Home"),
|
label: t("Home"),
|
||||||
action: () => navigate("/"),
|
action: () => navigate("/"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keywords: ["light", "theme", "lightmode"],
|
keywords: ["light", "theme", "lightmode"],
|
||||||
icon: <Sun />,
|
icon: <Sun />,
|
||||||
label: t("ToggleLightMode"),
|
label: t("ToggleLightMode"),
|
||||||
action: () => setTheme("light"),
|
action: () => setTheme("light"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keywords: ["dark", "theme", "darkmode"],
|
keywords: ["dark", "theme", "darkmode"],
|
||||||
icon: <Moon />,
|
icon: <Moon />,
|
||||||
label: t("ToggleDarkMode"),
|
label: t("ToggleDarkMode"),
|
||||||
action: () => setTheme("dark"),
|
action: () => setTheme("dark"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keywords: ["system", "theme", "systemmode"],
|
keywords: ["system", "theme", "systemmode"],
|
||||||
icon: <SunMoon />,
|
icon: <SunMoon />,
|
||||||
label: t("ToggleSystemMode"),
|
label: t("ToggleSystemMode"),
|
||||||
action: () => setTheme("system"),
|
action: () => setTheme("system"),
|
||||||
},
|
},
|
||||||
].map((item) => ({
|
].map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
value: `${item.keywords.join(" ")} ${item.label}`,
|
value: `${item.keywords.join(" ")} ${item.label}`,
|
||||||
}))
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<CommandDialog open={isOpen} onOpenChange={closeCommand}>
|
||||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
<CommandInput
|
||||||
<CommandInput placeholder={t("TypeCommand")} value={search} onValueChange={setSearch} />
|
placeholder={t("TypeCommand")}
|
||||||
<CommandList className="border-t">
|
value={search}
|
||||||
<CommandEmpty>{t("NoResults")}</CommandEmpty>
|
onValueChange={setSearch}
|
||||||
{nezhaWsData.servers && nezhaWsData.servers.length > 0 && (
|
/>
|
||||||
<>
|
<CommandList className="border-t">
|
||||||
<CommandGroup heading={t("Servers")}>
|
<CommandEmpty>{t("NoResults")}</CommandEmpty>
|
||||||
{nezhaWsData.servers.map((server) => (
|
{nezhaWsData.servers && nezhaWsData.servers.length > 0 && (
|
||||||
<CommandItem
|
<CommandGroup heading={t("Servers")}>
|
||||||
key={server.id}
|
{nezhaWsData.servers.map((server) => (
|
||||||
value={server.name}
|
<CommandItem
|
||||||
onSelect={() => {
|
key={server.id}
|
||||||
navigate(`/server/${server.id}`)
|
value={server.name}
|
||||||
setOpen(false)
|
onSelect={() => {
|
||||||
}}
|
navigate(`/server/${server.id}`);
|
||||||
>
|
closeCommand();
|
||||||
{formatNezhaInfo(nezhaWsData.now, server).online ? (
|
}}
|
||||||
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center" />
|
>
|
||||||
) : (
|
{formatNezhaInfo(nezhaWsData.now, server).online ? (
|
||||||
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" />
|
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center" />
|
||||||
)}
|
) : (
|
||||||
<span>{server.name}</span>
|
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" />
|
||||||
</CommandItem>
|
)}
|
||||||
))}
|
<span>{server.name}</span>
|
||||||
</CommandGroup>
|
</CommandItem>
|
||||||
</>
|
))}
|
||||||
)}
|
</CommandGroup>
|
||||||
<CommandSeparator />
|
)}
|
||||||
|
<CommandSeparator />
|
||||||
|
|
||||||
<CommandGroup heading={t("Shortcuts")}>
|
<CommandGroup heading={t("Shortcuts")}>
|
||||||
{shortcuts.map((item) => (
|
{shortcuts.map((item) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={item.label}
|
key={item.label}
|
||||||
value={item.value}
|
value={item.value}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
item.action()
|
item.action();
|
||||||
setOpen(false)
|
closeCommand();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
</>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getDomains, Domain } from '@/api/domain';
|
|||||||
import { CalendarDays, DollarSign } from 'lucide-react';
|
import { CalendarDays, DollarSign } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import RemainPercentBar from "./RemainPercentBar";
|
||||||
|
|
||||||
// =======================================================
|
// =======================================================
|
||||||
// 彩色备注标签组件
|
// 彩色备注标签组件
|
||||||
@@ -98,21 +99,7 @@ const DomainCard = ({ domain }: { domain: Domain }) => {
|
|||||||
const billingData = domain.BillingData || {};
|
const billingData = domain.BillingData || {};
|
||||||
const customBackgroundImage = (window as any).CustomBackgroundImage !== "" ? (window as any).CustomBackgroundImage : undefined;
|
const customBackgroundImage = (window as any).CustomBackgroundImage !== "" ? (window as any).CustomBackgroundImage : undefined;
|
||||||
|
|
||||||
let progressBarColor = 'bg-gray-300';
|
|
||||||
let progressBarWidth = '100%';
|
|
||||||
|
|
||||||
if (expiresIn !== undefined) {
|
|
||||||
if (expiresIn <= 10) {
|
|
||||||
progressBarColor = 'bg-red-500';
|
|
||||||
progressBarWidth = `${Math.max(5, (expiresIn / 10) * 100)}%`;
|
|
||||||
} else if (expiresIn <= 100) {
|
|
||||||
const lightness = 50 + (expiresIn - 10) / 90 * 20;
|
|
||||||
progressBarColor = `bg-[hsl(45,90%,${lightness}%)]`;
|
|
||||||
progressBarWidth = `${Math.max(5, (expiresIn / 100) * 100)}%`;
|
|
||||||
} else {
|
|
||||||
progressBarColor = 'bg-green-500';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={`https://${domain.Domain}`} target="_blank" rel="noopener noreferrer" className="block h-full">
|
<a href={`https://${domain.Domain}`} target="_blank" rel="noopener noreferrer" className="block h-full">
|
||||||
@@ -136,9 +123,7 @@ const DomainCard = ({ domain }: { domain: Domain }) => {
|
|||||||
<span className="truncate">{billingData.renewalPrice || 'N/A'}</span>
|
<span className="truncate">{billingData.renewalPrice || 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="w-full bg-muted rounded-full h-1.5 overflow-hidden">
|
<RemainPercentBar value={expiresIn ? Math.max(0, Math.min(100, (expiresIn / 365) * 100)) : 100} className="w-full h-1.5" />
|
||||||
<div className={cn("h-1.5 rounded-full transition-all duration-500", progressBarColor)} style={{ width: progressBarWidth }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="font-medium text-muted-foreground w-12 text-right">{expiresIn !== undefined ? `${expiresIn}天` : 'N/A'}</span>
|
<span className="font-medium text-muted-foreground w-12 text-right">{expiresIn !== undefined ? `${expiresIn}天` : 'N/A'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,36 +1,41 @@
|
|||||||
import React from "react"
|
import React from "react";
|
||||||
|
|
||||||
import ErrorPage from "../pages/ErrorPage"
|
import ErrorPage from "../pages/ErrorPage";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
hasError: boolean
|
hasError: boolean;
|
||||||
error?: Error
|
error?: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ErrorBoundary extends React.Component<Props, State> {
|
class ErrorBoundary extends React.Component<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props)
|
super(props);
|
||||||
this.state = { hasError: false }
|
this.state = { hasError: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromError(error: Error): State {
|
static getDerivedStateFromError(error: Error): State {
|
||||||
return {
|
return {
|
||||||
hasError: true,
|
hasError: true,
|
||||||
error,
|
error,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
return <ErrorPage code={500} message={this.state.error?.message || "应用程序发生错误"} />
|
return (
|
||||||
}
|
<ErrorPage
|
||||||
|
code={500}
|
||||||
|
message={this.state.error?.message || "应用程序发生错误"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return this.props.children
|
return this.props.children;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ErrorBoundary
|
export default ErrorBoundary;
|
||||||
|
|||||||
+69
-55
@@ -1,60 +1,74 @@
|
|||||||
// src/components/Footer.tsx (已添加您的署名)
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import type React from "react";
|
||||||
import { fetchSetting } from "@/lib/nezha-api"
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { fetchSetting } from "@/lib/nezha-api";
|
||||||
import React from "react"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
|
|
||||||
const Footer: React.FC = () => {
|
const Footer: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const isMac = /macintosh|mac os x/i.test(navigator.userAgent)
|
const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
|
||||||
|
|
||||||
const { data: settingData } = useQuery({
|
const { data: settingData } = useQuery({
|
||||||
queryKey: ["setting"],
|
queryKey: ["setting"],
|
||||||
queryFn: () => fetchSetting(),
|
queryFn: () => fetchSetting(),
|
||||||
refetchOnMount: true,
|
refetchOnMount: true,
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4 server-footer">
|
<footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4 server-footer">
|
||||||
<section className="flex flex-col">
|
<section className="flex flex-col">
|
||||||
<section className="mt-1 flex items-center sm:flex-row flex-col justify-between gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50 server-footer-name">
|
<section className="mt-1 flex items-center sm:flex-row flex-col justify-between gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50 server-footer-name">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
©2020-{new Date().getFullYear()}{" "}
|
©2020-{new Date().getFullYear()}{" "}
|
||||||
<a href={"https://github.com/naiba/nezha"} target="_blank" className="hover:underline">
|
<a
|
||||||
Nezha
|
href={"https://github.com/naiba/nezha"}
|
||||||
</a>
|
target="_blank"
|
||||||
<p>{settingData?.data?.version || ""}</p>
|
rel="noopener"
|
||||||
</div>
|
>
|
||||||
<div className="server-footer-theme flex flex-col items-center sm:items-end">
|
Nezha
|
||||||
<p className="mt-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
|
</a>
|
||||||
<kbd className="pointer-events-none mx-1 inline-flex h-4 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
<p>{settingData?.data?.version || ""}</p>
|
||||||
{isMac ? <span className="text-xs">⌘</span> : "Ctrl "}K
|
</div>
|
||||||
</kbd>
|
<div className="server-footer-theme flex flex-col items-center sm:items-end">
|
||||||
</p>
|
<p className="mt-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
|
||||||
<section>
|
<kbd className="pointer-events-none mx-1 inline-flex h-4 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
||||||
{t("footer.themeBy")}{" "}
|
{isMac ? <span className="text-xs">⌘</span> : "Ctrl "}K
|
||||||
<a href={"https://github.com/hamster1963/nezha-dash"} target="_blank" className="hover:underline">
|
</kbd>
|
||||||
nezha-dash
|
</p>
|
||||||
</a>
|
<section>
|
||||||
{import.meta.env.VITE_GIT_HASH && (
|
{t("footer.themeBy")}
|
||||||
<a href={"https://github.com/hamster1963/nezha-dash-v1/commit/" + import.meta.env.VITE_GIT_HASH} className="ml-1 hover:underline">
|
<a
|
||||||
({import.meta.env.VITE_GIT_HASH})
|
href={"https://github.com/hamster1963/nezha-dash"}
|
||||||
</a>
|
target="_blank"
|
||||||
)}
|
rel="noopener"
|
||||||
</section>
|
>
|
||||||
<section className="mt-1">
|
nezha-dash
|
||||||
{"Modified by "}
|
</a>
|
||||||
<a href={"https://github.com/buriburizaem0n"} target="_blank" className="hover:underline font-medium">
|
{import.meta.env.VITE_GIT_HASH && (
|
||||||
buriburizaem0n
|
<a
|
||||||
</a>
|
href={`https://github.com/hamster1963/nezha-dash-v1/commit/${import.meta.env.VITE_GIT_HASH}`}
|
||||||
</section>
|
className="ml-1"
|
||||||
</div>
|
>
|
||||||
</section>
|
({import.meta.env.VITE_GIT_HASH})
|
||||||
</section>
|
</a>
|
||||||
</footer>
|
)}
|
||||||
)
|
</section>
|
||||||
}
|
<section className="mt-1">
|
||||||
|
{"Modified by "}
|
||||||
|
<a
|
||||||
|
href={"https://github.com/buriburizaem0n"}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
className="hover:underline font-medium"
|
||||||
|
>
|
||||||
|
buriburizaem0n
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Footer
|
export default Footer;
|
||||||
|
|||||||
+210
-161
@@ -1,184 +1,233 @@
|
|||||||
import useTooltip from "@/hooks/use-tooltip"
|
import { geoEquirectangular, geoPath } from "d3-geo";
|
||||||
import { geoJsonString } from "@/lib/geo-json-string"
|
import { useTranslation } from "react-i18next";
|
||||||
import { countryCoordinates } from "@/lib/geo-limit"
|
import useTooltip from "@/hooks/use-tooltip";
|
||||||
import { cn, formatNezhaInfo } from "@/lib/utils"
|
import { geoJsonString } from "@/lib/geo-json-string";
|
||||||
import { NezhaServer } from "@/types/nezha-api"
|
import { countryCoordinates } from "@/lib/geo-limit";
|
||||||
import { geoEquirectangular, geoPath } from "d3-geo"
|
import { cn, formatNezhaInfo } from "@/lib/utils";
|
||||||
import { useTranslation } from "react-i18next"
|
import type { NezhaServer } from "@/types/nezha-api";
|
||||||
|
|
||||||
import MapTooltip from "./MapTooltip"
|
import MapTooltip from "./MapTooltip";
|
||||||
|
|
||||||
export default function GlobalMap({ serverList, now }: { serverList: NezhaServer[]; now: number }) {
|
export default function GlobalMap({
|
||||||
const { t } = useTranslation()
|
serverList,
|
||||||
const countryList: string[] = []
|
now,
|
||||||
const serverCounts: { [key: string]: number } = {}
|
}: {
|
||||||
|
serverList: NezhaServer[];
|
||||||
|
now: number;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const countryList: string[] = [];
|
||||||
|
const serverCounts: { [key: string]: number } = {};
|
||||||
|
|
||||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
const customBackgroundImage =
|
||||||
|
(window.CustomBackgroundImage as string) !== ""
|
||||||
|
? window.CustomBackgroundImage
|
||||||
|
: undefined;
|
||||||
|
|
||||||
serverList.forEach((server) => {
|
serverList.forEach((server) => {
|
||||||
if (server.country_code) {
|
if (server.country_code) {
|
||||||
const countryCode = server.country_code.toUpperCase()
|
const countryCode = server.country_code.toUpperCase();
|
||||||
if (!countryList.includes(countryCode)) {
|
if (!countryList.includes(countryCode)) {
|
||||||
countryList.push(countryCode)
|
countryList.push(countryCode);
|
||||||
}
|
}
|
||||||
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1
|
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
const width = 900
|
const width = 900;
|
||||||
const height = 500
|
const height = 500;
|
||||||
|
|
||||||
const geoJson = JSON.parse(geoJsonString)
|
const geoJson = JSON.parse(geoJsonString);
|
||||||
const filteredFeatures = geoJson.features.filter((feature: { properties: { iso_a3_eh: string } }) => feature.properties.iso_a3_eh !== "")
|
const filteredFeatures = geoJson.features.filter(
|
||||||
|
(feature: { properties: { iso_a3_eh: string } }) =>
|
||||||
|
feature.properties.iso_a3_eh !== "",
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={cn("flex flex-col gap-4 mt-8", {
|
className={cn("flex flex-col gap-4 mt-8", {
|
||||||
"bg-card/70 rounded-lg p-4": customBackgroundImage,
|
"bg-card/70 rounded-lg p-4": customBackgroundImage,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<p className="text-sm font-medium opacity-40">
|
<p className="text-sm font-medium opacity-40">
|
||||||
{t("map.Distributions")} {countryList.length} {t("map.Regions")}
|
{t("map.Distributions")} {countryList.length} {t("map.Regions")}
|
||||||
</p>
|
</p>
|
||||||
<div className="w-full overflow-x-auto">
|
<div className="w-full overflow-x-auto">
|
||||||
<InteractiveMap
|
<InteractiveMap
|
||||||
countries={countryList}
|
countries={countryList}
|
||||||
serverCounts={serverCounts}
|
serverCounts={serverCounts}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
filteredFeatures={filteredFeatures}
|
filteredFeatures={filteredFeatures}
|
||||||
nezhaServerList={serverList}
|
nezhaServerList={serverList}
|
||||||
now={now}
|
now={now}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InteractiveMapProps {
|
interface InteractiveMapProps {
|
||||||
countries: string[]
|
countries: string[];
|
||||||
serverCounts: { [key: string]: number }
|
serverCounts: { [key: string]: number };
|
||||||
width: number
|
width: number;
|
||||||
height: number
|
height: number;
|
||||||
filteredFeatures: {
|
filteredFeatures: {
|
||||||
type: "Feature"
|
type: "Feature";
|
||||||
properties: {
|
properties: {
|
||||||
iso_a2_eh: string
|
iso_a2_eh: string;
|
||||||
[key: string]: string
|
[key: string]: string;
|
||||||
}
|
};
|
||||||
geometry: never
|
geometry: never;
|
||||||
}[]
|
}[];
|
||||||
nezhaServerList: NezhaServer[]
|
nezhaServerList: NezhaServer[];
|
||||||
now: number
|
now: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InteractiveMap({ countries, serverCounts, width, height, filteredFeatures, nezhaServerList, now }: InteractiveMapProps) {
|
export function InteractiveMap({
|
||||||
const { setTooltipData } = useTooltip()
|
countries,
|
||||||
|
serverCounts,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
filteredFeatures,
|
||||||
|
nezhaServerList,
|
||||||
|
now,
|
||||||
|
}: InteractiveMapProps) {
|
||||||
|
const { setTooltipData } = useTooltip();
|
||||||
|
|
||||||
const projection = geoEquirectangular()
|
const projection = geoEquirectangular()
|
||||||
.scale(140)
|
.scale(140)
|
||||||
.translate([width / 2, height / 2])
|
.translate([width / 2, height / 2])
|
||||||
.rotate([-12, 0, 0])
|
.rotate([-12, 0, 0]);
|
||||||
|
|
||||||
const path = geoPath().projection(projection)
|
const path = geoPath().projection(projection);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full aspect-[2/1]" onMouseLeave={() => setTooltipData(null)}>
|
<div
|
||||||
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} xmlns="http://www.w3.org/2000/svg" className="w-full h-auto">
|
className="relative w-full aspect-2/1"
|
||||||
<defs>
|
onMouseLeave={() => setTooltipData(null)}
|
||||||
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse">
|
>
|
||||||
<circle cx="1" cy="1" r="0.5" fill="currentColor" />
|
<svg
|
||||||
</pattern>
|
width={width}
|
||||||
</defs>
|
height={height}
|
||||||
<g>
|
viewBox={`0 0 ${width} ${height}`}
|
||||||
{/* Background rect to handle mouse events in empty areas */}
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<rect x="0" y="0" width={width} height={height} fill="transparent" onMouseEnter={() => setTooltipData(null)} />
|
className="w-full h-auto"
|
||||||
{filteredFeatures.map((feature, index) => {
|
>
|
||||||
const isHighlighted = countries.includes(feature.properties.iso_a2_eh)
|
<defs>
|
||||||
|
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse">
|
||||||
|
<circle cx="1" cy="1" r="0.5" fill="currentColor" />
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
{/* Background rect to handle mouse events in empty areas */}
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill="transparent"
|
||||||
|
onMouseEnter={() => setTooltipData(null)}
|
||||||
|
/>
|
||||||
|
{filteredFeatures.map((feature, index) => {
|
||||||
|
const isHighlighted = countries.includes(
|
||||||
|
feature.properties.iso_a2_eh,
|
||||||
|
);
|
||||||
|
|
||||||
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0
|
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<path
|
<path
|
||||||
key={index}
|
key={index}
|
||||||
d={path(feature) || ""}
|
d={path(feature) || ""}
|
||||||
className={
|
className={
|
||||||
isHighlighted
|
isHighlighted
|
||||||
? "fill-green-700 hover:fill-green-600 dark:fill-green-900 dark:hover:fill-green-700 transition-all cursor-pointer"
|
? "fill-green-700 hover:fill-green-600 dark:fill-green-900 dark:hover:fill-green-700 transition-all cursor-pointer"
|
||||||
: "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]"
|
: "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]"
|
||||||
}
|
}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
if (!isHighlighted) {
|
if (!isHighlighted) {
|
||||||
setTooltipData(null)
|
setTooltipData(null);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (path.centroid(feature)) {
|
if (path.centroid(feature)) {
|
||||||
const countryCode = feature.properties.iso_a2_eh
|
const countryCode = feature.properties.iso_a2_eh;
|
||||||
const countryServers = nezhaServerList
|
const countryServers = nezhaServerList
|
||||||
.filter((server: NezhaServer) => server.country_code?.toUpperCase() === countryCode)
|
.filter(
|
||||||
.map((server: NezhaServer) => ({
|
(server: NezhaServer) =>
|
||||||
name: server.name,
|
server.country_code?.toUpperCase() === countryCode,
|
||||||
status: formatNezhaInfo(now, server).online,
|
)
|
||||||
}))
|
.map((server: NezhaServer) => ({
|
||||||
setTooltipData({
|
id: server.id,
|
||||||
centroid: path.centroid(feature),
|
name: server.name,
|
||||||
country: feature.properties.name,
|
status: formatNezhaInfo(now, server).online,
|
||||||
count: serverCount,
|
}));
|
||||||
servers: countryServers,
|
setTooltipData({
|
||||||
})
|
centroid: path.centroid(feature),
|
||||||
}
|
country: feature.properties.name,
|
||||||
}}
|
count: serverCount,
|
||||||
/>
|
servers: countryServers,
|
||||||
)
|
});
|
||||||
})}
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* 渲染不在 filteredFeatures 中的国家标记点 */}
|
{/* 渲染不在 filteredFeatures 中的国家标记点 */}
|
||||||
{countries.map((countryCode) => {
|
{countries.map((countryCode) => {
|
||||||
// 检查该国家是否已经在 filteredFeatures 中
|
// 检查该国家是否已经在 filteredFeatures 中
|
||||||
const isInFilteredFeatures = filteredFeatures.some((feature) => feature.properties.iso_a2_eh === countryCode)
|
const isInFilteredFeatures = filteredFeatures.some(
|
||||||
|
(feature) => feature.properties.iso_a2_eh === countryCode,
|
||||||
|
);
|
||||||
|
|
||||||
// 如果已经在 filteredFeatures 中,跳过
|
// 如果已经在 filteredFeatures 中,跳过
|
||||||
if (isInFilteredFeatures) return null
|
if (isInFilteredFeatures) return null;
|
||||||
|
|
||||||
// 获取国家的经纬度
|
// 获取国家的经纬度
|
||||||
const coords = countryCoordinates[countryCode]
|
const coords = countryCoordinates[countryCode];
|
||||||
if (!coords) return null
|
if (!coords) return null;
|
||||||
|
|
||||||
// 使用投影函数将经纬度转换为 SVG 坐标
|
// 使用投影函数将经纬度转换为 SVG 坐标
|
||||||
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0]
|
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0];
|
||||||
const serverCount = serverCounts[countryCode] || 0
|
const serverCount = serverCounts[countryCode] || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
key={countryCode}
|
key={countryCode}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
const countryServers = nezhaServerList
|
const countryServers = nezhaServerList
|
||||||
.filter((server: NezhaServer) => server.country_code?.toUpperCase() === countryCode.toUpperCase())
|
.filter(
|
||||||
.map((server: NezhaServer) => ({
|
(server: NezhaServer) =>
|
||||||
name: server.name,
|
server.country_code?.toUpperCase() ===
|
||||||
status: formatNezhaInfo(now, server).online,
|
countryCode.toUpperCase(),
|
||||||
}))
|
)
|
||||||
setTooltipData({
|
.map((server: NezhaServer) => ({
|
||||||
centroid: [x, y],
|
id: server.id,
|
||||||
country: coords.name,
|
name: server.name,
|
||||||
count: serverCount,
|
status: formatNezhaInfo(now, server).online,
|
||||||
servers: countryServers,
|
}));
|
||||||
})
|
setTooltipData({
|
||||||
}}
|
centroid: [x, y],
|
||||||
className="cursor-pointer"
|
country: coords.name,
|
||||||
>
|
count: serverCount,
|
||||||
<circle
|
servers: countryServers,
|
||||||
cx={x}
|
});
|
||||||
cy={y}
|
}}
|
||||||
r={4}
|
className="cursor-pointer"
|
||||||
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
|
>
|
||||||
/>
|
<circle
|
||||||
</g>
|
cx={x}
|
||||||
)
|
cy={y}
|
||||||
})}
|
r={4}
|
||||||
</g>
|
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
|
||||||
</svg>
|
/>
|
||||||
<MapTooltip />
|
</g>
|
||||||
</div>
|
);
|
||||||
)
|
})}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<MapTooltip />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,92 +1,111 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { m } from "framer-motion";
|
||||||
import { m } from "framer-motion"
|
import { createRef, useEffect, useRef } from "react";
|
||||||
import { createRef, useEffect, useRef } from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function GroupSwitch({
|
export default function GroupSwitch({
|
||||||
tabs,
|
tabs,
|
||||||
currentTab,
|
currentTab,
|
||||||
setCurrentTab,
|
setCurrentTab,
|
||||||
}: {
|
}: {
|
||||||
tabs: string[]
|
tabs: string[];
|
||||||
currentTab: string
|
currentTab: string;
|
||||||
setCurrentTab: (tab: string) => void
|
setCurrentTab: (tab: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
const customBackgroundImage =
|
||||||
|
(window.CustomBackgroundImage as string) !== ""
|
||||||
|
? window.CustomBackgroundImage
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const tagRefs = useRef(tabs.map(() => createRef<HTMLDivElement>()))
|
const tagRefs = useRef(tabs.map(() => createRef<HTMLDivElement>()));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = scrollRef.current
|
const container = scrollRef.current;
|
||||||
if (!container) return
|
if (!container) return;
|
||||||
|
|
||||||
const isOverflowing = container.scrollWidth > container.clientWidth
|
const isOverflowing = container.scrollWidth > container.clientWidth;
|
||||||
if (!isOverflowing) return
|
if (!isOverflowing) return;
|
||||||
|
|
||||||
const onWheel = (e: WheelEvent) => {
|
const onWheel = (e: WheelEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
container.scrollLeft += e.deltaY
|
container.scrollLeft += e.deltaY;
|
||||||
}
|
};
|
||||||
|
|
||||||
container.addEventListener("wheel", onWheel, { passive: false })
|
container.addEventListener("wheel", onWheel, { passive: false });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
container.removeEventListener("wheel", onWheel)
|
container.removeEventListener("wheel", onWheel);
|
||||||
}
|
};
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedGroup = sessionStorage.getItem("selectedGroup")
|
if (tabs.length === 1 && tabs[0] === "All") {
|
||||||
if (savedGroup && tabs.includes(savedGroup)) {
|
setCurrentTab("All");
|
||||||
setCurrentTab(savedGroup)
|
return;
|
||||||
}
|
}
|
||||||
}, [tabs, setCurrentTab])
|
const savedGroup = sessionStorage.getItem("selectedGroup");
|
||||||
|
if (savedGroup && tabs.includes(savedGroup)) {
|
||||||
|
setCurrentTab(savedGroup);
|
||||||
|
}
|
||||||
|
}, [tabs, setCurrentTab]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentTagRef = tagRefs.current[tabs.indexOf(currentTab)]
|
const currentTagRef = tagRefs.current[tabs.indexOf(currentTab)];
|
||||||
|
|
||||||
if (currentTagRef && currentTagRef.current) {
|
if (currentTagRef?.current) {
|
||||||
currentTagRef.current.scrollIntoView({
|
currentTagRef.current.scrollIntoView({
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
block: "nearest",
|
block: "nearest",
|
||||||
inline: "center",
|
inline: "center",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}, [currentTab])
|
}, [currentTab, tabs.indexOf]);
|
||||||
|
|
||||||
return (
|
if (tabs.length === 1 && tabs[0] === "All") {
|
||||||
<div ref={scrollRef} className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]">
|
return null;
|
||||||
<div
|
}
|
||||||
className={cn("flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800", {
|
|
||||||
"bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage,
|
return (
|
||||||
})}
|
<div
|
||||||
>
|
ref={scrollRef}
|
||||||
{tabs.map((tab: string, index: number) => (
|
className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]"
|
||||||
<div
|
>
|
||||||
key={tab}
|
<div
|
||||||
ref={tagRefs.current[index]}
|
className={cn(
|
||||||
onClick={() => setCurrentTab(tab)}
|
"flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800",
|
||||||
className={cn(
|
{
|
||||||
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500",
|
"bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage,
|
||||||
currentTab === tab ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500",
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{currentTab === tab && (
|
{tabs.map((tab: string, index: number) => (
|
||||||
<m.div
|
<div
|
||||||
layoutId="tab-switch"
|
key={tab}
|
||||||
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5"
|
ref={tagRefs.current[index]}
|
||||||
style={{
|
onClick={() => setCurrentTab(tab)}
|
||||||
originY: "0px",
|
className={cn(
|
||||||
borderRadius: 46,
|
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-semibold transition-all duration-500",
|
||||||
}}
|
currentTab === tab
|
||||||
/>
|
? "text-black dark:text-white"
|
||||||
)}
|
: "text-stone-400 dark:text-stone-500",
|
||||||
<div className="relative z-20 flex items-center gap-1">
|
)}
|
||||||
<p className="whitespace-nowrap">{tab}</p>
|
>
|
||||||
</div>
|
{currentTab === tab && (
|
||||||
</div>
|
<m.div
|
||||||
))}
|
layoutId="tab-switch"
|
||||||
</div>
|
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5"
|
||||||
</div>
|
style={{
|
||||||
)
|
originY: "0px",
|
||||||
|
borderRadius: 46,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="relative z-20 flex items-center gap-1">
|
||||||
|
<p className="whitespace-nowrap">{tab}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+317
-259
@@ -1,303 +1,361 @@
|
|||||||
import { ModeToggle } from "@/components/ThemeSwitcher"
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { AnimatePresence, m } from "framer-motion";
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { ImageMinus } from "lucide-react";
|
||||||
import { useBackground } from "@/hooks/use-background"
|
import { DateTime } from "luxon";
|
||||||
import { useWebSocketContext } from "@/hooks/use-websocket-context"
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api"
|
import { useTranslation } from "react-i18next";
|
||||||
import { cn } from "@/lib/utils"
|
import { useNavigate } from "react-router-dom";
|
||||||
import NumberFlow, { NumberFlowGroup } from "@number-flow/react"
|
import { ModeToggle } from "@/components/ThemeSwitcher";
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { AnimatePresence, m } from "framer-motion"
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { ImageMinus } from "lucide-react"
|
import { useBackground } from "@/hooks/use-background";
|
||||||
import { DateTime } from "luxon"
|
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api";
|
||||||
import { useTranslation } from "react-i18next"
|
import { cn } from "@/lib/utils";
|
||||||
import { useNavigate } from "react-router-dom"
|
|
||||||
|
|
||||||
import { LanguageSwitcher } from "./LanguageSwitcher"
|
import AnimateCountClient from "./AnimatedCount";
|
||||||
import { Loader, LoadingSpinner } from "./loading/Loader"
|
import { LanguageSwitcher } from "./LanguageSwitcher";
|
||||||
import { Button } from "./ui/button"
|
import { Loader, LoadingSpinner } from "./loading/Loader";
|
||||||
|
import { SearchButton } from "./SearchButton";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
|
interface TimeState {
|
||||||
|
hh: number;
|
||||||
|
mm: number;
|
||||||
|
ss: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCurrentTime = () => {
|
||||||
|
const [time, setTime] = useState<TimeState>({
|
||||||
|
hh: DateTime.now().setLocale("en-US").hour,
|
||||||
|
mm: DateTime.now().setLocale("en-US").minute,
|
||||||
|
ss: DateTime.now().setLocale("en-US").second,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
const now = DateTime.now().setLocale("en-US");
|
||||||
|
setTime({
|
||||||
|
hh: now.hour,
|
||||||
|
mm: now.minute,
|
||||||
|
ss: now.second,
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return time;
|
||||||
|
};
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
const { backgroundImage, updateBackground } = useBackground()
|
const { backgroundImage, updateBackground } = useBackground();
|
||||||
|
|
||||||
const { data: settingData, isLoading } = useQuery({
|
const { data: settingData, isLoading } = useQuery({
|
||||||
queryKey: ["setting"],
|
queryKey: ["setting"],
|
||||||
queryFn: () => fetchSetting(),
|
queryFn: () => fetchSetting(),
|
||||||
refetchOnMount: true,
|
refetchOnMount: true,
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
})
|
});
|
||||||
|
|
||||||
const { lastMessage, connected } = useWebSocketContext()
|
const { lastMessage, connected } = useWebSocketContext();
|
||||||
|
|
||||||
const onlineCount = connected ? (lastMessage ? JSON.parse(lastMessage.data).online || 0 : 0) : "..."
|
const onlineCount = connected
|
||||||
|
? lastMessage
|
||||||
|
? JSON.parse(lastMessage.data).online || 0
|
||||||
|
: 0
|
||||||
|
: "...";
|
||||||
|
|
||||||
const siteName = settingData?.data?.config?.site_name
|
const siteName = settingData?.data?.config?.site_name;
|
||||||
|
|
||||||
// @ts-expect-error CustomLogo is a global variable
|
const customLogo = window.CustomLogo || "/apple-touch-icon.png";
|
||||||
const customLogo = window.CustomLogo || "/apple-touch-icon.png"
|
|
||||||
|
|
||||||
// @ts-expect-error CustomDesc is a global variable
|
const customDesc = window.CustomDesc || t("nezha");
|
||||||
const customDesc = window.CustomDesc || t("nezha")
|
|
||||||
|
|
||||||
const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined
|
const customMobileBackgroundImage =
|
||||||
|
window.CustomMobileBackgroundImage !== ""
|
||||||
|
? window.CustomMobileBackgroundImage
|
||||||
|
: undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const link = document.querySelector("link[rel*='icon']") || document.createElement("link")
|
const link =
|
||||||
// @ts-expect-error set link.type
|
document.querySelector("link[rel*='icon']") ||
|
||||||
link.type = "image/x-icon"
|
document.createElement("link");
|
||||||
// @ts-expect-error set link.rel
|
// @ts-expect-error set link.type
|
||||||
link.rel = "shortcut icon"
|
link.type = "image/x-icon";
|
||||||
// @ts-expect-error set link.href
|
// @ts-expect-error set link.rel
|
||||||
link.href = customLogo
|
link.rel = "shortcut icon";
|
||||||
document.getElementsByTagName("head")[0].appendChild(link)
|
// @ts-expect-error set link.href
|
||||||
}, [customLogo])
|
link.href = customLogo;
|
||||||
|
document.getElementsByTagName("head")[0].appendChild(link);
|
||||||
|
}, [customLogo]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = siteName || "哪吒监控 Nezha Monitoring"
|
document.title = siteName || "哪吒监控 Nezha Monitoring";
|
||||||
}, [siteName])
|
}, [siteName]);
|
||||||
|
|
||||||
const handleBackgroundToggle = () => {
|
const handleBackgroundToggle = () => {
|
||||||
if (window.CustomBackgroundImage) {
|
if (window.CustomBackgroundImage) {
|
||||||
// Store the current background image before removing it
|
// Store the current background image before removing it
|
||||||
sessionStorage.setItem("savedBackgroundImage", window.CustomBackgroundImage)
|
sessionStorage.setItem(
|
||||||
updateBackground(undefined)
|
"savedBackgroundImage",
|
||||||
} else {
|
window.CustomBackgroundImage,
|
||||||
// Restore the saved background image
|
);
|
||||||
const savedImage = sessionStorage.getItem("savedBackgroundImage")
|
updateBackground(undefined);
|
||||||
if (savedImage) {
|
} else {
|
||||||
updateBackground(savedImage)
|
// Restore the saved background image
|
||||||
}
|
const savedImage = sessionStorage.getItem("savedBackgroundImage");
|
||||||
}
|
if (savedImage) {
|
||||||
}
|
updateBackground(savedImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const customBackgroundImage = backgroundImage
|
const customBackgroundImage = backgroundImage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-5xl">
|
<div className="mx-auto w-full max-w-5xl">
|
||||||
<section className="flex items-center justify-between header-top">
|
<section className="flex items-center justify-between header-top">
|
||||||
<section
|
<section
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sessionStorage.removeItem("selectedGroup")
|
sessionStorage.removeItem("selectedGroup");
|
||||||
navigate("/")
|
navigate("/");
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer flex items-center sm:text-base text-sm font-medium"
|
className="cursor-pointer flex items-center sm:text-base text-sm font-medium"
|
||||||
>
|
>
|
||||||
<div className="mr-1 flex flex-row items-center justify-start header-logo">
|
<div className="mr-1 flex flex-row items-center justify-start header-logo">
|
||||||
<img
|
<img
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
alt="apple-touch-icon"
|
alt="apple-touch-icon"
|
||||||
src={customLogo}
|
src={customLogo}
|
||||||
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!"
|
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isLoading ? <Skeleton className="h-6 w-20 rounded-[5px] bg-muted-foreground/10 animate-none" /> : siteName || "NEZHA"}
|
{isLoading ? (
|
||||||
<Separator orientation="vertical" className="mx-2 hidden h-4 w-[1px] md:block" />
|
<Skeleton className="h-6 w-20 rounded-[5px] bg-muted-foreground/10 animate-none" />
|
||||||
<p className="hidden text-sm font-medium opacity-40 md:block">{customDesc}</p>
|
) : (
|
||||||
</section>
|
siteName || "NEZHA"
|
||||||
<section className="flex items-center gap-2 header-handles">
|
)}
|
||||||
<div className="hidden sm:flex items-center gap-2">
|
<Separator
|
||||||
<Links />
|
orientation="vertical"
|
||||||
<DashboardLink />
|
className="mx-2 hidden h-4 w-px md:block"
|
||||||
</div>
|
/>
|
||||||
<LanguageSwitcher />
|
<p className="hidden text-sm font-medium opacity-40 md:block">
|
||||||
<ModeToggle />
|
{customDesc}
|
||||||
{(customBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) && (
|
</p>
|
||||||
<Button
|
</section>
|
||||||
variant="outline"
|
<section className="flex items-center gap-2 header-handles">
|
||||||
size="sm"
|
<div className="hidden sm:flex items-center gap-2">
|
||||||
onClick={handleBackgroundToggle}
|
<Links />
|
||||||
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
<DashboardLink />
|
||||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
</div>
|
||||||
"hidden sm:block": customMobileBackgroundImage,
|
<SearchButton />
|
||||||
})}
|
<LanguageSwitcher />
|
||||||
>
|
<ModeToggle />
|
||||||
<ImageMinus className="w-4 h-4" />
|
{(customBackgroundImage ||
|
||||||
</Button>
|
sessionStorage.getItem("savedBackgroundImage")) && (
|
||||||
)}
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
size="sm"
|
||||||
size="sm"
|
onClick={handleBackgroundToggle}
|
||||||
className={cn("hover:bg-white dark:hover:bg-black cursor-default rounded-full flex items-center px-[9px] bg-white dark:bg-black", {
|
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
||||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||||
})}
|
"hidden sm:block": customMobileBackgroundImage,
|
||||||
>
|
})}
|
||||||
{connected ? onlineCount : <Loader visible={true} />}
|
>
|
||||||
<p className="text-muted-foreground">{connected ? t("online") : t("offline")}</p>
|
<ImageMinus className="w-4 h-4" />
|
||||||
<span
|
</Button>
|
||||||
className={cn("h-2 w-2 rounded-full bg-green-500", {
|
)}
|
||||||
"bg-red-500": !connected,
|
<Button
|
||||||
})}
|
variant="outline"
|
||||||
></span>
|
size="sm"
|
||||||
</Button>
|
className={cn(
|
||||||
</section>
|
"hover:bg-white dark:hover:bg-black cursor-default rounded-full flex items-center px-[9px] bg-white dark:bg-black",
|
||||||
</section>
|
{
|
||||||
<div className="w-full flex justify-between sm:hidden mt-1">
|
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||||
<DashboardLink />
|
},
|
||||||
<Links />
|
)}
|
||||||
</div>
|
>
|
||||||
<Overview />
|
{connected ? onlineCount : <Loader visible={true} />}
|
||||||
</div>
|
<p className="text-muted-foreground">
|
||||||
)
|
{connected ? t("online") : t("offline")}
|
||||||
|
</p>
|
||||||
|
<span
|
||||||
|
className={cn("h-2 w-2 rounded-full bg-green-500", {
|
||||||
|
"bg-red-500": !connected,
|
||||||
|
})}
|
||||||
|
></span>
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<div className="w-full flex justify-between sm:hidden mt-1">
|
||||||
|
<DashboardLink />
|
||||||
|
<Links />
|
||||||
|
</div>
|
||||||
|
<Overview />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type links = {
|
type links = {
|
||||||
link: string
|
link: string;
|
||||||
name: string
|
name: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
function Links() {
|
function Links() {
|
||||||
// @ts-expect-error CustomLinks is a global variable
|
const customLinks = window.CustomLinks as string;
|
||||||
const customLinks = window.CustomLinks as string
|
|
||||||
|
|
||||||
const links: links[] | null = customLinks ? JSON.parse(customLinks) : null
|
const links: links[] | null = customLinks ? JSON.parse(customLinks) : null;
|
||||||
|
|
||||||
if (!links) return null
|
if (!links) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 w-fit">
|
<div className="flex items-center gap-2 w-fit">
|
||||||
{links.map((link, index) => {
|
{links.map((link, index) => {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
key={index}
|
key={index}
|
||||||
href={link.link}
|
href={link.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
|
className="flex items-center gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
|
||||||
>
|
>
|
||||||
{link.name}
|
{link.name}
|
||||||
</a>
|
</a>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RefreshToast() {
|
export function RefreshToast() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { needReconnect } = useWebSocketContext()
|
const { needReconnect } = useWebSocketContext();
|
||||||
|
|
||||||
if (!needReconnect) {
|
if (!needReconnect) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needReconnect) {
|
if (needReconnect) {
|
||||||
sessionStorage.removeItem("needRefresh")
|
sessionStorage.removeItem("needRefresh");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate(0)
|
navigate(0);
|
||||||
}, 1000)
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
<m.div
|
<m.div
|
||||||
initial={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
|
initial={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
|
||||||
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
|
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
|
||||||
exit={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
|
exit={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
|
||||||
transition={{ type: "spring", duration: 0.8 }}
|
transition={{ type: "spring", duration: 0.8 }}
|
||||||
className="fixed left-1/2 -translate-x-1/2 top-8 z-[999] flex items-center justify-between gap-4 rounded-[50px] border-[1px] border-solid bg-white px-2 py-1.5 shadow-xl shadow-black/5 dark:border-stone-700 dark:bg-stone-800 dark:shadow-none"
|
className="fixed left-1/2 -translate-x-1/2 top-8 z-999 flex items-center justify-between gap-4 rounded-[50px] border border-solid bg-white px-2 py-1.5 shadow-xl shadow-black/5 dark:border-stone-700 dark:bg-stone-800 dark:shadow-none"
|
||||||
>
|
>
|
||||||
<section className="flex items-center gap-1.5">
|
<section className="flex items-center gap-1.5">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
<p className="text-[12.5px] font-medium">{t("refreshing")}...</p>
|
<p className="text-[12.5px] font-medium">{t("refreshing")}...</p>
|
||||||
</section>
|
</section>
|
||||||
</m.div>
|
</m.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DashboardLink() {
|
function DashboardLink() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const { setNeedReconnect } = useWebSocketContext()
|
const { setNeedReconnect } = useWebSocketContext();
|
||||||
const previousLoginState = useRef<boolean | null>(null)
|
const previousLoginState = useRef<boolean | null>(null);
|
||||||
const {
|
const {
|
||||||
data: userData,
|
data: userData,
|
||||||
isFetched,
|
isFetched,
|
||||||
isLoadingError,
|
isLoadingError,
|
||||||
isError,
|
isError,
|
||||||
refetch,
|
refetch,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["login-user"],
|
queryKey: ["login-user"],
|
||||||
queryFn: () => fetchLoginUser(),
|
queryFn: () => fetchLoginUser(),
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
refetchIntervalInBackground: true,
|
refetchIntervalInBackground: true,
|
||||||
refetchInterval: 1000 * 30,
|
refetchInterval: 1000 * 30,
|
||||||
retry: 0,
|
retry: 0,
|
||||||
})
|
});
|
||||||
|
|
||||||
const isLogin = isError ? false : userData ? !!userData?.data?.id && !!document.cookie : false
|
const isLogin = isError
|
||||||
|
? false
|
||||||
|
: userData
|
||||||
|
? !!userData?.data?.id && !!document.cookie
|
||||||
|
: false;
|
||||||
|
|
||||||
if (isLoadingError) {
|
if (isLoadingError) {
|
||||||
previousLoginState.current = isLogin
|
previousLoginState.current = isLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refetch()
|
refetch();
|
||||||
}, [document.cookie])
|
}, [refetch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFetched || isError) {
|
if (isFetched || isError) {
|
||||||
// 只有当登录状态发生变化时才设置needReconnect
|
// 只有当登录状态发生变化时才设置needReconnect
|
||||||
if (previousLoginState.current !== null && previousLoginState.current !== isLogin) {
|
if (
|
||||||
setNeedReconnect(true)
|
previousLoginState.current !== null &&
|
||||||
}
|
previousLoginState.current !== isLogin
|
||||||
previousLoginState.current = isLogin
|
) {
|
||||||
}
|
setNeedReconnect(true);
|
||||||
}, [isLogin])
|
}
|
||||||
|
previousLoginState.current = isLogin;
|
||||||
|
}
|
||||||
|
}, [isLogin, isError, isFetched, setNeedReconnect]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<a
|
<a
|
||||||
href={"/dashboard"}
|
href={"/dashboard"}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center text-nowrap gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
|
className="flex items-center text-nowrap gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
|
||||||
>
|
>
|
||||||
{!isLogin && t("login")}
|
{!isLogin && t("login")}
|
||||||
{isLogin && t("dashboard")}
|
{isLogin && t("dashboard")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Overview() {
|
function Overview() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const [time, setTime] = useState({
|
const time = useCurrentTime();
|
||||||
hh: DateTime.now().setLocale("en-US").hour,
|
const [mounted, setMounted] = useState(false);
|
||||||
mm: DateTime.now().setLocale("en-US").minute,
|
|
||||||
ss: DateTime.now().setLocale("en-US").second,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
setMounted(true);
|
||||||
setTime({
|
}, []);
|
||||||
hh: DateTime.now().setLocale("en-US").hour,
|
|
||||||
mm: DateTime.now().setLocale("en-US").minute,
|
|
||||||
ss: DateTime.now().setLocale("en-US").second,
|
|
||||||
})
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
return () => clearInterval(timer)
|
return (
|
||||||
}, [])
|
<section className={"mt-10 flex flex-col md:mt-16 header-timer"}>
|
||||||
return (
|
<p className="text-base font-semibold">👋 {t("overview")}</p>
|
||||||
<section className={"mt-10 flex flex-col md:mt-16 header-timer"}>
|
<div className="flex items-center gap-1">
|
||||||
<p className="text-base font-semibold">👋 {t("overview")}</p>
|
<p className="text-sm font-medium opacity-50">{t("whereTheTimeIs")}</p>
|
||||||
<div className="flex items-center gap-1.5">
|
{mounted ? (
|
||||||
<p className="text-sm font-medium opacity-50">{t("whereTheTimeIs")}</p>
|
<div className="flex items-center font-medium text-sm">
|
||||||
<NumberFlowGroup>
|
<AnimateCountClient count={time.hh} minDigits={2} />
|
||||||
<div style={{ fontVariantNumeric: "tabular-nums" }} className="flex text-sm font-medium mt-0.5">
|
<span className="mb-px font-medium text-sm opacity-50">:</span>
|
||||||
<NumberFlow trend={1} value={time.hh} format={{ minimumIntegerDigits: 2 }} />
|
<AnimateCountClient count={time.mm} minDigits={2} />
|
||||||
<NumberFlow prefix=":" trend={1} value={time.mm} digits={{ 1: { max: 5 } }} format={{ minimumIntegerDigits: 2 }} />
|
<span className="mb-px font-medium text-sm opacity-50">:</span>
|
||||||
<p className="mt-[0.5px]">:{time.ss.toString().padStart(2, "0")}</p>
|
<span className="font-medium text-sm">
|
||||||
</div>
|
<AnimateCountClient count={time.ss} minDigits={2} />
|
||||||
</NumberFlowGroup>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
) : (
|
||||||
)
|
<Skeleton className="h-[21px] w-16 animate-none rounded-[5px] bg-muted-foreground/10" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
export default Header
|
export default Header;
|
||||||
|
|||||||
+27
-27
File diff suppressed because one or more lines are too long
@@ -1,54 +1,78 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { CheckCircleIcon, LanguageIcon } from "@heroicons/react/20/solid";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
import { useTranslation } from "react-i18next";
|
||||||
import { cn } from "@/lib/utils"
|
import { Button } from "@/components/ui/button";
|
||||||
import { CheckCircleIcon, LanguageIcon } from "@heroicons/react/20/solid"
|
import {
|
||||||
import { useTranslation } from "react-i18next"
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function LanguageSwitcher() {
|
export function LanguageSwitcher() {
|
||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
const customBackgroundImage =
|
||||||
|
(window.CustomBackgroundImage as string) !== ""
|
||||||
|
? window.CustomBackgroundImage
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const locale = i18n.languages[0]
|
const locale = i18n.languages[0];
|
||||||
|
|
||||||
const handleSelect = (e: Event, newLocale: string) => {
|
const handleSelect = (e: Event, newLocale: string) => {
|
||||||
e.preventDefault() // 阻止默认的关闭行为
|
e.preventDefault(); // 阻止默认的关闭行为
|
||||||
i18n.changeLanguage(newLocale)
|
i18n.changeLanguage(newLocale);
|
||||||
}
|
};
|
||||||
|
|
||||||
const localeItems = [
|
const localeItems = [
|
||||||
{ name: t("language.zh-CN"), code: "zh-CN" },
|
{ name: t("language.zh-CN"), code: "zh-CN" },
|
||||||
{ name: t("language.zh-TW"), code: "zh-TW" },
|
{ name: t("language.zh-TW"), code: "zh-TW" },
|
||||||
{ name: t("language.en-US"), code: "en-US" },
|
{ name: t("language.en-US"), code: "en-US" },
|
||||||
{ name: t("language.ru-RU"), code: "ru-RU" },
|
{ name: t("language.ru-RU"), code: "ru-RU" },
|
||||||
{ name: t("language.es-ES"), code: "es-ES" },
|
{ name: t("language.es-ES"), code: "es-ES" },
|
||||||
{ name: t("language.de-DE"), code: "de-DE" },
|
{ name: t("language.de-DE"), code: "de-DE" },
|
||||||
{ name: t("language.ta-IN"), code: "ta-IN" },
|
{ name: t("language.ta-IN"), code: "ta-IN" },
|
||||||
]
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
||||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<LanguageIcon className="size-4" />
|
<LanguageIcon className="size-4" />
|
||||||
<span className="sr-only">Change language</span>
|
<span className="sr-only">Change language</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
||||||
{localeItems.map((item) => (
|
{localeItems.map((item, index) => (
|
||||||
<DropdownMenuItem key={item.code} onSelect={(e) => handleSelect(e, item.code)} className={locale === item.code ? "bg-muted gap-3" : ""}>
|
<DropdownMenuItem
|
||||||
{item.name} {locale === item.code && <CheckCircleIcon className="size-4" />}
|
key={item.code}
|
||||||
</DropdownMenuItem>
|
onSelect={(e) => handleSelect(e, item.code)}
|
||||||
))}
|
className={cn(
|
||||||
</DropdownMenuContent>
|
"text-xs",
|
||||||
</DropdownMenu>
|
{
|
||||||
)
|
"gap-3 bg-muted font-semibold": locale === item.code,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rounded-t-[5px]": index === localeItems.length - 1,
|
||||||
|
"rounded-[5px]":
|
||||||
|
index !== 0 && index !== localeItems.length - 1,
|
||||||
|
"rounded-b-[5px]": index === 0,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.name}{" "}
|
||||||
|
{locale === item.code && <CheckCircleIcon className="size-4" />}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,70 @@
|
|||||||
import useTooltip from "@/hooks/use-tooltip"
|
import { AnimatePresence, m } from "framer-motion";
|
||||||
import { AnimatePresence, m } from "framer-motion"
|
import { memo } from "react";
|
||||||
import { memo } from "react"
|
import { useTranslation } from "react-i18next";
|
||||||
import { useTranslation } from "react-i18next"
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import useTooltip from "@/hooks/use-tooltip";
|
||||||
|
|
||||||
const MapTooltip = memo(function MapTooltip() {
|
const MapTooltip = memo(function MapTooltip() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const { tooltipData } = useTooltip()
|
const navigate = useNavigate();
|
||||||
|
const { tooltipData } = useTooltip();
|
||||||
|
|
||||||
if (!tooltipData) return null
|
if (!tooltipData) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<m.div
|
<m.div
|
||||||
initial={{ opacity: 0, filter: "blur(10px)" }}
|
initial={{ opacity: 0, filter: "blur(10px)" }}
|
||||||
animate={{ opacity: 1, filter: "blur(0px)" }}
|
animate={{ opacity: 1, filter: "blur(0px)" }}
|
||||||
exit={{ opacity: 0, filter: "blur(10px)" }}
|
exit={{ opacity: 0, filter: "blur(10px)" }}
|
||||||
className="absolute hidden lg:block bg-white dark:bg-neutral-800 px-2 py-1 rounded shadow-lg text-sm dark:border dark:border-neutral-700 z-50"
|
className="absolute hidden lg:block bg-white dark:bg-neutral-800 px-2 py-1 rounded shadow-lg text-sm dark:border dark:border-neutral-700 z-50"
|
||||||
key={tooltipData.country}
|
key={tooltipData.country}
|
||||||
style={{
|
style={{
|
||||||
left: tooltipData.centroid[0],
|
left: tooltipData.centroid[0],
|
||||||
top: tooltipData.centroid[1],
|
top: tooltipData.centroid[1],
|
||||||
transform: "translate(20%, -50%)",
|
transform: "translate(20%, -50%)",
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{tooltipData.country === "China" ? "Mainland China" : tooltipData.country}</p>
|
<p className="font-medium">
|
||||||
<p className="text-neutral-600 dark:text-neutral-400 mb-1">
|
{tooltipData.country === "China"
|
||||||
{tooltipData.count} {t("map.Servers")}
|
? "Mainland China"
|
||||||
</p>
|
: tooltipData.country}
|
||||||
</div>
|
</p>
|
||||||
<div
|
<p className="text-neutral-600 dark:text-neutral-400 text-xs font-light mb-1">
|
||||||
className="border-t dark:border-neutral-700 pt-1"
|
{tooltipData.count} {t("map.Servers")}
|
||||||
style={{
|
</p>
|
||||||
maxHeight: "200px",
|
</div>
|
||||||
overflowY: "auto",
|
<div
|
||||||
}}
|
className="border-t dark:border-neutral-700 pt-1"
|
||||||
>
|
style={{
|
||||||
{tooltipData.servers.map((server, index: number) => (
|
maxHeight: "200px",
|
||||||
<div key={index} className="flex items-center gap-1.5 py-0.5">
|
overflowY: "auto",
|
||||||
<span className={`w-1.5 h-1.5 shrink-0 rounded-full ${server.status ? "bg-green-500" : "bg-red-500"}`}></span>
|
}}
|
||||||
<span className="text-xs">{server.name}</span>
|
>
|
||||||
</div>
|
{tooltipData.servers.map((server) => (
|
||||||
))}
|
<button
|
||||||
</div>
|
key={server.id}
|
||||||
</m.div>
|
type="button"
|
||||||
</AnimatePresence>
|
className="flex items-center gap-1.5 py-0.5 text-neutral-500 transition-colors hover:text-black dark:text-neutral-400 dark:hover:text-white"
|
||||||
)
|
onClick={() => {
|
||||||
})
|
sessionStorage.setItem("fromMainPage", "true");
|
||||||
|
navigate(`/server/${server.id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`h-1.5 w-1.5 shrink-0 rounded-full ${server.status ? "bg-green-500" : "bg-red-500"}`}
|
||||||
|
/>
|
||||||
|
<span className="text-xs">{server.name}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</m.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default MapTooltip
|
export default MapTooltip;
|
||||||
|
|||||||
+757
-327
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,23 @@
|
|||||||
import { Loader } from "@/components/loading/Loader"
|
import { Loader } from "@/components/loading/Loader";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
export default function NetworkChartLoading() {
|
export default function NetworkChartLoading() {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
||||||
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5">
|
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5">
|
||||||
<CardTitle className="flex items-center gap-0.5 text-xl">
|
<CardTitle className="flex items-center gap-0.5 text-xl">
|
||||||
<div className="aspect-auto h-[20px] w-24 bg-muted"></div>
|
<div className="aspect-auto h-[20px] w-24 bg-muted"></div>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted"></div>
|
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden pr-4 pt-4 sm:block">
|
<div className="hidden pr-4 pt-4 sm:block">
|
||||||
<Loader visible={true} />
|
<Loader visible={true} />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-2 sm:p-6">
|
<CardContent className="px-2 sm:p-6">
|
||||||
<div className="aspect-auto h-[250px] w-full"></div>
|
<div className="aspect-auto h-[250px] w-full"></div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+81
-54
@@ -1,58 +1,85 @@
|
|||||||
import { PublicNoteData, cn } from "@/lib/utils"
|
import { cn, type PublicNoteData } from "@/lib/utils";
|
||||||
|
|
||||||
export default function PlanInfo({ parsedData }: { parsedData: PublicNoteData }) {
|
export default function PlanInfo({
|
||||||
if (!parsedData || !parsedData.planDataMod) {
|
parsedData,
|
||||||
return null
|
}: {
|
||||||
}
|
parsedData: PublicNoteData;
|
||||||
|
}) {
|
||||||
|
if (!parsedData || !parsedData.planDataMod) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const extraList =
|
const extraList =
|
||||||
parsedData.planDataMod.extra.split(",").length > 1
|
parsedData.planDataMod.extra.split(",").length > 1
|
||||||
? parsedData.planDataMod.extra.split(",")
|
? parsedData.planDataMod.extra.split(",")
|
||||||
: parsedData.planDataMod.extra.split(",")[0] === ""
|
: parsedData.planDataMod.extra.split(",")[0] === ""
|
||||||
? []
|
? []
|
||||||
: [parsedData.planDataMod.extra]
|
: [parsedData.planDataMod.extra];
|
||||||
|
const networkRoutes = parsedData.planDataMod.networkRoute
|
||||||
|
? parsedData.planDataMod.networkRoute.split(",")
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex gap-1 items-center flex-wrap mt-0.5">
|
<section className="flex gap-1 items-center flex-wrap mt-0.5">
|
||||||
{parsedData.planDataMod.bandwidth !== "" && (
|
{parsedData.planDataMod.bandwidth !== "" && (
|
||||||
<p className={cn("text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
|
<p
|
||||||
{parsedData.planDataMod.bandwidth}
|
className={cn(
|
||||||
</p>
|
"text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
||||||
)}
|
)}
|
||||||
{parsedData.planDataMod.trafficVol !== "" && (
|
>
|
||||||
<p className={cn("text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
|
{parsedData.planDataMod.bandwidth}
|
||||||
{parsedData.planDataMod.trafficVol}
|
</p>
|
||||||
</p>
|
)}
|
||||||
)}
|
{parsedData.planDataMod.trafficVol !== "" && (
|
||||||
{parsedData.planDataMod.IPv4 === "1" && (
|
<p
|
||||||
<p
|
className={cn(
|
||||||
className={cn("text-[9px] bg-purple-600 text-purple-200 dark:bg-purple-800 dark:text-purple-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}
|
"text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
||||||
>
|
)}
|
||||||
IPv4
|
>
|
||||||
</p>
|
{parsedData.planDataMod.trafficVol}
|
||||||
)}
|
</p>
|
||||||
{parsedData.planDataMod.IPv6 === "1" && (
|
)}
|
||||||
<p className={cn("text-[9px] bg-pink-600 text-pink-200 dark:bg-pink-800 dark:text-pink-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
|
{parsedData.planDataMod.IPv4 === "1" && (
|
||||||
IPv6
|
<p
|
||||||
</p>
|
className={cn(
|
||||||
)}
|
"text-[9px] bg-purple-600 text-purple-200 dark:bg-purple-800 dark:text-purple-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
||||||
{parsedData.planDataMod.networkRoute && (
|
)}
|
||||||
<p className={cn("text-[9px] bg-blue-600 text-blue-200 dark:bg-blue-800 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
|
>
|
||||||
{parsedData.planDataMod.networkRoute.split(",").map((route, index) => {
|
IPv4
|
||||||
return route + (index === parsedData.planDataMod!.networkRoute.split(",").length - 1 ? "" : "|")
|
</p>
|
||||||
})}
|
)}
|
||||||
</p>
|
{parsedData.planDataMod.IPv6 === "1" && (
|
||||||
)}
|
<p
|
||||||
{extraList.map((extra, index) => {
|
className={cn(
|
||||||
return (
|
"text-[9px] bg-pink-600 text-pink-200 dark:bg-pink-800 dark:text-pink-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
||||||
<p
|
)}
|
||||||
key={index}
|
>
|
||||||
className={cn("text-[9px] bg-stone-600 text-stone-200 dark:bg-stone-800 dark:text-stone-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}
|
IPv6
|
||||||
>
|
</p>
|
||||||
{extra}
|
)}
|
||||||
</p>
|
{parsedData.planDataMod.networkRoute && (
|
||||||
)
|
<p
|
||||||
})}
|
className={cn(
|
||||||
</section>
|
"text-[9px] bg-blue-600 text-blue-200 dark:bg-blue-800 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
||||||
)
|
)}
|
||||||
|
>
|
||||||
|
{networkRoutes.map((route, index) => {
|
||||||
|
return route + (index === networkRoutes.length - 1 ? "" : "|");
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{extraList.map((extra, index) => {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
key={index}
|
||||||
|
className={cn(
|
||||||
|
"text-[9px] bg-stone-600 text-stone-200 dark:bg-stone-800 dark:text-stone-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{extra}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { Progress } from "./ui/progress"
|
import { Progress } from "./ui/progress";
|
||||||
|
|
||||||
export default function RemainPercentBar({ value, className }: { value: number; className?: string }) {
|
export default function RemainPercentBar({
|
||||||
return (
|
value,
|
||||||
<Progress
|
className,
|
||||||
aria-label={"Server Usage Bar"}
|
}: {
|
||||||
aria-labelledby={"Server Usage Bar"}
|
value: number;
|
||||||
value={value}
|
className?: string;
|
||||||
indicatorClassName={value < 30 ? "bg-red-500" : value < 70 ? "bg-orange-400" : "bg-green-500"}
|
}) {
|
||||||
className={cn("h-[3px] rounded-sm w-[70px]", className)}
|
return (
|
||||||
/>
|
<Progress
|
||||||
)
|
aria-label={"Server Usage Bar"}
|
||||||
|
aria-labelledby={"Server Usage Bar"}
|
||||||
|
value={value}
|
||||||
|
indicatorClassName={
|
||||||
|
value < 30
|
||||||
|
? "bg-red-500"
|
||||||
|
: value < 70
|
||||||
|
? "bg-orange-400"
|
||||||
|
: "bg-green-500"
|
||||||
|
}
|
||||||
|
className={cn("h-[3px] rounded-sm w-[70px]", className)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
||||||
|
import { useCommand } from "@/hooks/use-command";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
|
export function SearchButton() {
|
||||||
|
const { openCommand } = useCommand();
|
||||||
|
|
||||||
|
const customBackgroundImage =
|
||||||
|
(window.CustomBackgroundImage as string) !== ""
|
||||||
|
? window.CustomBackgroundImage
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
||||||
|
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||||
|
})}
|
||||||
|
onClick={openCommand}
|
||||||
|
title="Search"
|
||||||
|
>
|
||||||
|
<MagnifyingGlassIcon className="size-4" />
|
||||||
|
<span className="sr-only">Search</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
+271
-187
@@ -1,196 +1,280 @@
|
|||||||
import ServerFlag from "@/components/ServerFlag"
|
import { useTranslation } from "react-i18next";
|
||||||
import ServerUsageBar from "@/components/ServerUsageBar"
|
import { useNavigate } from "react-router-dom";
|
||||||
import { formatBytes } from "@/lib/format"
|
import ServerFlag from "@/components/ServerFlag";
|
||||||
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class"
|
import ServerUsageBar from "@/components/ServerUsageBar";
|
||||||
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils"
|
import { formatBytes } from "@/lib/format";
|
||||||
import { NezhaServer } from "@/types/nezha-api"
|
import {
|
||||||
import { useTranslation } from "react-i18next"
|
GetFontLogoClass,
|
||||||
import { useNavigate } from "react-router-dom"
|
GetOsName,
|
||||||
|
MageMicrosoftWindows,
|
||||||
|
} from "@/lib/logo-class";
|
||||||
|
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils";
|
||||||
|
import type { NezhaServer } from "@/types/nezha-api";
|
||||||
|
import BillingInfo from "./billingInfo";
|
||||||
|
import PlanInfo from "./PlanInfo";
|
||||||
|
import { Badge } from "./ui/badge";
|
||||||
|
import { Card } from "./ui/card";
|
||||||
|
|
||||||
import PlanInfo from "./PlanInfo"
|
export default function ServerCard({
|
||||||
import BillingInfo from "./billingInfo"
|
now,
|
||||||
import { Badge } from "./ui/badge"
|
serverInfo,
|
||||||
import { Card } from "./ui/card"
|
}: {
|
||||||
|
now: number;
|
||||||
|
serverInfo: NezhaServer;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
country_code,
|
||||||
|
online,
|
||||||
|
cpu,
|
||||||
|
up,
|
||||||
|
down,
|
||||||
|
mem,
|
||||||
|
stg,
|
||||||
|
net_in_transfer,
|
||||||
|
net_out_transfer,
|
||||||
|
public_note,
|
||||||
|
platform,
|
||||||
|
} = formatNezhaInfo(now, serverInfo);
|
||||||
|
|
||||||
export default function ServerCard({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) {
|
const cardClick = () => {
|
||||||
const { t } = useTranslation()
|
sessionStorage.setItem("fromMainPage", "true");
|
||||||
const navigate = useNavigate()
|
navigate(`/server/${serverInfo.id}`);
|
||||||
const { name, country_code, online, cpu, up, down, mem, stg, net_in_transfer, net_out_transfer, public_note, platform } = formatNezhaInfo(
|
};
|
||||||
now,
|
|
||||||
serverInfo,
|
|
||||||
)
|
|
||||||
|
|
||||||
const cardClick = () => {
|
const showFlag = true;
|
||||||
sessionStorage.setItem("fromMainPage", "true")
|
|
||||||
navigate(`/server/${serverInfo.id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const showFlag = true
|
const customBackgroundImage =
|
||||||
|
(window.CustomBackgroundImage as string) !== ""
|
||||||
|
? window.CustomBackgroundImage
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
// @ts-expect-error ShowNetTransfer is a global variable
|
||||||
|
const showNetTransfer = window.ShowNetTransfer as boolean;
|
||||||
|
|
||||||
// @ts-expect-error ShowNetTransfer is a global variable
|
// @ts-expect-error FixedTopServerName is a global variable
|
||||||
const showNetTransfer = window.ShowNetTransfer as boolean
|
const fixedTopServerName = window.FixedTopServerName as boolean;
|
||||||
|
|
||||||
// @ts-expect-error FixedTopServerName is a global variable
|
const parsedData = parsePublicNote(public_note);
|
||||||
const fixedTopServerName = window.FixedTopServerName as boolean
|
|
||||||
|
|
||||||
const parsedData = parsePublicNote(public_note)
|
return online ? (
|
||||||
|
<Card
|
||||||
return online ? (
|
className={cn(
|
||||||
<Card
|
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
|
||||||
className={cn(
|
{
|
||||||
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
|
"flex-col": fixedTopServerName,
|
||||||
{
|
"lg:flex-row": !fixedTopServerName,
|
||||||
"flex-col": fixedTopServerName,
|
},
|
||||||
"lg:flex-row": !fixedTopServerName,
|
{
|
||||||
},
|
"bg-card/70": customBackgroundImage,
|
||||||
{
|
},
|
||||||
"bg-card/70": customBackgroundImage,
|
)}
|
||||||
},
|
onClick={cardClick}
|
||||||
)}
|
>
|
||||||
onClick={cardClick}
|
<section
|
||||||
>
|
className={cn("grid items-center gap-2", {
|
||||||
<section
|
"lg:w-40": !fixedTopServerName,
|
||||||
className={cn("grid items-center gap-2", {
|
})}
|
||||||
"lg:w-40": !fixedTopServerName,
|
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||||
})}
|
>
|
||||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
||||||
>
|
<div
|
||||||
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
className={cn(
|
||||||
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
|
"flex items-center justify-center",
|
||||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
showFlag ? "min-w-[17px]" : "min-w-0",
|
||||||
</div>
|
)}
|
||||||
<div className="relative flex flex-col">
|
>
|
||||||
<p className={cn("break-normal font-bold tracking-tight", showFlag ? "text-xs " : "text-sm")}>{name}</p>
|
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||||
<div
|
</div>
|
||||||
className={cn("hidden lg:block", {
|
<div className="relative flex flex-col">
|
||||||
"lg:hidden": fixedTopServerName,
|
<p
|
||||||
})}
|
className={cn(
|
||||||
>
|
"break-normal font-bold tracking-tight",
|
||||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
showFlag ? "text-xs " : "text-sm",
|
||||||
</div>
|
)}
|
||||||
</div>
|
>
|
||||||
</section>
|
{name}
|
||||||
<div
|
</p>
|
||||||
className={cn("flex items-center gap-2 -mt-2 lg:hidden", {
|
<div
|
||||||
"lg:flex": fixedTopServerName,
|
className={cn("hidden lg:block", {
|
||||||
})}
|
"lg:hidden": fixedTopServerName,
|
||||||
>
|
})}
|
||||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
>
|
||||||
</div>
|
{parsedData?.billingDataMod && (
|
||||||
<div className="flex flex-col lg:items-start items-center gap-2">
|
<BillingInfo parsedData={parsedData} />
|
||||||
<section
|
)}
|
||||||
className={cn("grid grid-cols-5 items-center gap-3", {
|
</div>
|
||||||
"lg:grid-cols-6 lg:gap-4": fixedTopServerName,
|
</div>
|
||||||
})}
|
</section>
|
||||||
>
|
<div
|
||||||
{fixedTopServerName && (
|
className={cn("flex items-center gap-2 -mt-2 lg:hidden", {
|
||||||
<div className={"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"}>
|
"lg:flex": fixedTopServerName,
|
||||||
<div className="text-xs font-semibold">
|
})}
|
||||||
{platform.includes("Windows") ? (
|
>
|
||||||
<MageMicrosoftWindows className="size-[10px]" />
|
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||||
) : (
|
</div>
|
||||||
<p className={`fl-${GetFontLogoClass(platform)}`} />
|
<div className="flex flex-col lg:items-start items-center gap-2">
|
||||||
)}
|
<section
|
||||||
</div>
|
className={cn("grid grid-cols-5 items-center gap-3", {
|
||||||
<div className={"flex w-14 flex-col"}>
|
"lg:grid-cols-6 lg:gap-4": fixedTopServerName,
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.system")}</p>
|
})}
|
||||||
<div className="flex items-center text-[10.5px] font-semibold">{platform.includes("Windows") ? "Windows" : GetOsName(platform)}</div>
|
>
|
||||||
</div>
|
{fixedTopServerName && (
|
||||||
</div>
|
<div
|
||||||
)}
|
className={
|
||||||
<div className={"flex w-14 flex-col"}>
|
"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"
|
||||||
<p className="text-xs text-muted-foreground">{"CPU"}</p>
|
}
|
||||||
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div>
|
>
|
||||||
<ServerUsageBar value={cpu} />
|
<div className="text-xs font-semibold">
|
||||||
</div>
|
{platform.includes("Windows") ? (
|
||||||
<div className={"flex w-14 flex-col"}>
|
<MageMicrosoftWindows className="size-[10px]" />
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.mem")}</p>
|
) : (
|
||||||
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div>
|
<p className={`fl-${GetFontLogoClass(platform)}`} />
|
||||||
<ServerUsageBar value={mem} />
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex w-14 flex-col"}>
|
<div className={"flex w-14 flex-col"}>
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div>
|
{t("serverCard.system")}
|
||||||
<ServerUsageBar value={stg} />
|
</p>
|
||||||
</div>
|
<div className="flex items-center text-[10.5px] font-semibold">
|
||||||
<div className={"flex w-14 flex-col"}>
|
{platform.includes("Windows")
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p>
|
? "Windows"
|
||||||
<div className="flex items-center text-xs font-semibold">
|
: GetOsName(platform)}
|
||||||
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : up >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex w-14 flex-col"}>
|
)}
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p>
|
<div className={"flex w-14 flex-col"}>
|
||||||
<div className="flex items-center text-xs font-semibold">
|
<p className="text-xs text-muted-foreground">{"CPU"}</p>
|
||||||
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : down >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`}
|
<div className="flex items-center text-xs font-semibold">
|
||||||
</div>
|
{cpu.toFixed(2)}%
|
||||||
</div>
|
</div>
|
||||||
</section>
|
<ServerUsageBar value={cpu} />
|
||||||
{showNetTransfer && (
|
</div>
|
||||||
<section className={"flex items-center w-full justify-between gap-1"}>
|
<div className={"flex w-14 flex-col"}>
|
||||||
<Badge
|
<p className="text-xs text-muted-foreground">
|
||||||
variant="secondary"
|
{t("serverCard.mem")}
|
||||||
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] border-muted-50 shadow-md shadow-neutral-200/30 dark:shadow-none"
|
</p>
|
||||||
>
|
<div className="flex items-center text-xs font-semibold">
|
||||||
{t("serverCard.upload")}:{formatBytes(net_out_transfer)}
|
{mem.toFixed(2)}%
|
||||||
</Badge>
|
</div>
|
||||||
<Badge
|
<ServerUsageBar value={mem} />
|
||||||
variant="outline"
|
</div>
|
||||||
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] shadow-md shadow-neutral-200/30 dark:shadow-none"
|
<div className={"flex w-14 flex-col"}>
|
||||||
>
|
<p className="text-xs text-muted-foreground">
|
||||||
{t("serverCard.download")}:{formatBytes(net_in_transfer)}
|
{t("serverCard.stg")}
|
||||||
</Badge>
|
</p>
|
||||||
</section>
|
<div className="flex items-center text-xs font-semibold">
|
||||||
)}
|
{stg.toFixed(2)}%
|
||||||
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
</div>
|
||||||
</div>
|
<ServerUsageBar value={stg} />
|
||||||
</Card>
|
</div>
|
||||||
) : (
|
<div className={"flex w-14 flex-col"}>
|
||||||
<Card
|
<p className="text-xs text-muted-foreground">
|
||||||
className={cn(
|
{t("serverCard.upload")}
|
||||||
"flex flex-col items-center justify-start gap-3 sm:gap-0 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
|
</p>
|
||||||
showNetTransfer ? "lg:min-h-[91px] min-h-[123px]" : "lg:min-h-[61px] min-h-[93px]",
|
<div className="flex items-center text-xs font-semibold">
|
||||||
{
|
{up >= 1024
|
||||||
"flex-col": fixedTopServerName,
|
? `${(up / 1024).toFixed(2)}G/s`
|
||||||
"lg:flex-row": !fixedTopServerName,
|
: up >= 1
|
||||||
},
|
? `${up.toFixed(2)}M/s`
|
||||||
{
|
: `${(up * 1024).toFixed(2)}K/s`}
|
||||||
"bg-card/70": customBackgroundImage,
|
</div>
|
||||||
},
|
</div>
|
||||||
)}
|
<div className={"flex w-14 flex-col"}>
|
||||||
onClick={cardClick}
|
<p className="text-xs text-muted-foreground">
|
||||||
>
|
{t("serverCard.download")}
|
||||||
<section
|
</p>
|
||||||
className={cn("grid items-center gap-2", {
|
<div className="flex items-center text-xs font-semibold">
|
||||||
"lg:w-40": !fixedTopServerName,
|
{down >= 1024
|
||||||
})}
|
? `${(down / 1024).toFixed(2)}G/s`
|
||||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
: down >= 1
|
||||||
>
|
? `${down.toFixed(2)}M/s`
|
||||||
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
: `${(down * 1024).toFixed(2)}K/s`}
|
||||||
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
|
</div>
|
||||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
</div>
|
||||||
</div>
|
</section>
|
||||||
<div className="relative flex flex-col">
|
{showNetTransfer && (
|
||||||
<p className={cn("break-normal font-bold tracking-tight max-w-[108px]", showFlag ? "text-xs" : "text-sm")}>{name}</p>
|
<section className={"flex items-center w-full justify-between gap-1"}>
|
||||||
<div
|
<Badge
|
||||||
className={cn("hidden lg:block", {
|
variant="secondary"
|
||||||
"lg:hidden": fixedTopServerName,
|
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] border-muted-50 shadow-md shadow-neutral-200/30 dark:shadow-none"
|
||||||
})}
|
>
|
||||||
>
|
{t("serverCard.upload")}:{formatBytes(net_out_transfer)}
|
||||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
</Badge>
|
||||||
</div>
|
<Badge
|
||||||
</div>
|
variant="outline"
|
||||||
</section>
|
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] shadow-md shadow-neutral-200/30 dark:shadow-none"
|
||||||
<div
|
>
|
||||||
className={cn("flex items-center gap-2 lg:hidden", {
|
{t("serverCard.download")}:{formatBytes(net_in_transfer)}
|
||||||
"lg:flex": fixedTopServerName,
|
</Badge>
|
||||||
})}
|
</section>
|
||||||
>
|
)}
|
||||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||||
</div>
|
</div>
|
||||||
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
</Card>
|
||||||
</Card>
|
) : (
|
||||||
)
|
<Card
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center justify-start gap-3 sm:gap-0 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
|
||||||
|
showNetTransfer
|
||||||
|
? "lg:min-h-[91px] min-h-[123px]"
|
||||||
|
: "lg:min-h-[61px] min-h-[93px]",
|
||||||
|
{
|
||||||
|
"flex-col": fixedTopServerName,
|
||||||
|
"lg:flex-row": !fixedTopServerName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg-card/70": customBackgroundImage,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
onClick={cardClick}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
className={cn("grid items-center gap-2", {
|
||||||
|
"lg:w-40": !fixedTopServerName,
|
||||||
|
})}
|
||||||
|
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||||
|
>
|
||||||
|
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center",
|
||||||
|
showFlag ? "min-w-[17px]" : "min-w-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||||
|
</div>
|
||||||
|
<div className="relative flex flex-col">
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"break-normal font-bold tracking-tight max-w-[108px]",
|
||||||
|
showFlag ? "text-xs" : "text-sm",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className={cn("hidden lg:block", {
|
||||||
|
"lg:hidden": fixedTopServerName,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{parsedData?.billingDataMod && (
|
||||||
|
<BillingInfo parsedData={parsedData} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div
|
||||||
|
className={cn("flex items-center gap-2 lg:hidden", {
|
||||||
|
"lg:flex": fixedTopServerName,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||||
|
</div>
|
||||||
|
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+233
-136
@@ -1,143 +1,240 @@
|
|||||||
import ServerFlag from "@/components/ServerFlag"
|
import { useTranslation } from "react-i18next";
|
||||||
import ServerUsageBar from "@/components/ServerUsageBar"
|
import { useNavigate } from "react-router-dom";
|
||||||
import { formatBytes } from "@/lib/format"
|
import ServerFlag from "@/components/ServerFlag";
|
||||||
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class"
|
import ServerUsageBar from "@/components/ServerUsageBar";
|
||||||
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils"
|
import { formatBytes } from "@/lib/format";
|
||||||
import { NezhaServer } from "@/types/nezha-api"
|
import {
|
||||||
import { useTranslation } from "react-i18next"
|
GetFontLogoClass,
|
||||||
import { useNavigate } from "react-router-dom"
|
GetOsName,
|
||||||
|
MageMicrosoftWindows,
|
||||||
|
} from "@/lib/logo-class";
|
||||||
|
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils";
|
||||||
|
import type { NezhaServer } from "@/types/nezha-api";
|
||||||
|
import BillingInfo from "./billingInfo";
|
||||||
|
import PlanInfo from "./PlanInfo";
|
||||||
|
import { Card } from "./ui/card";
|
||||||
|
import { Separator } from "./ui/separator";
|
||||||
|
|
||||||
import PlanInfo from "./PlanInfo"
|
export default function ServerCardInline({
|
||||||
import BillingInfo from "./billingInfo"
|
now,
|
||||||
import { Card } from "./ui/card"
|
serverInfo,
|
||||||
import { Separator } from "./ui/separator"
|
}: {
|
||||||
|
now: number;
|
||||||
|
serverInfo: NezhaServer;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
country_code,
|
||||||
|
online,
|
||||||
|
cpu,
|
||||||
|
up,
|
||||||
|
down,
|
||||||
|
mem,
|
||||||
|
stg,
|
||||||
|
platform,
|
||||||
|
uptime,
|
||||||
|
net_in_transfer,
|
||||||
|
net_out_transfer,
|
||||||
|
public_note,
|
||||||
|
} = formatNezhaInfo(now, serverInfo);
|
||||||
|
|
||||||
export default function ServerCardInline({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) {
|
const cardClick = () => {
|
||||||
const { t } = useTranslation()
|
sessionStorage.setItem("fromMainPage", "true");
|
||||||
const navigate = useNavigate()
|
navigate(`/server/${serverInfo.id}`);
|
||||||
const { name, country_code, online, cpu, up, down, mem, stg, platform, uptime, net_in_transfer, net_out_transfer, public_note } = formatNezhaInfo(
|
};
|
||||||
now,
|
|
||||||
serverInfo,
|
|
||||||
)
|
|
||||||
|
|
||||||
const cardClick = () => {
|
const showFlag = true;
|
||||||
sessionStorage.setItem("fromMainPage", "true")
|
|
||||||
navigate(`/server/${serverInfo.id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const showFlag = true
|
const customBackgroundImage =
|
||||||
|
(window.CustomBackgroundImage as string) !== ""
|
||||||
|
? window.CustomBackgroundImage
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
const parsedData = parsePublicNote(public_note);
|
||||||
|
|
||||||
const parsedData = parsePublicNote(public_note)
|
return online ? (
|
||||||
|
<section>
|
||||||
return online ? (
|
<Card
|
||||||
<section>
|
className={cn(
|
||||||
<Card
|
"flex items-center lg:flex-row justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors min-w-[900px] w-full",
|
||||||
className={cn(
|
{
|
||||||
"flex items-center lg:flex-row justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors min-w-[900px] w-full",
|
"bg-card/70": customBackgroundImage,
|
||||||
{
|
},
|
||||||
"bg-card/70": customBackgroundImage,
|
)}
|
||||||
},
|
onClick={cardClick}
|
||||||
)}
|
>
|
||||||
onClick={cardClick}
|
<section
|
||||||
>
|
className={cn("grid items-center gap-2 lg:w-36")}
|
||||||
<section className={cn("grid items-center gap-2 lg:w-36")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
|
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||||
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
>
|
||||||
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
|
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
||||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
<div
|
||||||
</div>
|
className={cn(
|
||||||
<div className="relative w-28 flex flex-col">
|
"flex items-center justify-center",
|
||||||
<p className={cn("break-normal font-bold tracking-tight", showFlag ? "text-xs " : "text-sm")}>{name}</p>
|
showFlag ? "min-w-[17px]" : "min-w-0",
|
||||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
)}
|
||||||
</div>
|
>
|
||||||
</section>
|
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||||
<Separator orientation="vertical" className="h-8 mx-0 ml-2" />
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="relative w-28 flex flex-col">
|
||||||
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}>
|
<p
|
||||||
<div className={"items-center flex flex-row gap-2 whitespace-nowrap"}>
|
className={cn(
|
||||||
<div className="text-xs font-semibold">
|
"break-normal font-bold tracking-tight",
|
||||||
{platform.includes("Windows") ? (
|
showFlag ? "text-xs " : "text-sm",
|
||||||
<MageMicrosoftWindows className="size-[10px]" />
|
)}
|
||||||
) : (
|
>
|
||||||
<p className={`fl-${GetFontLogoClass(platform)}`} />
|
{name}
|
||||||
)}
|
</p>
|
||||||
</div>
|
{parsedData?.billingDataMod && (
|
||||||
<div className={"flex w-14 flex-col"}>
|
<BillingInfo parsedData={parsedData} />
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.system")}</p>
|
)}
|
||||||
<div className="flex items-center text-[10.5px] font-semibold">{platform.includes("Windows") ? "Windows" : GetOsName(platform)}</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
<Separator orientation="vertical" className="h-8 mx-0 ml-2" />
|
||||||
<div className={"flex w-20 flex-col"}>
|
<div className="flex flex-col gap-1">
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.uptime")}</p>
|
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}>
|
||||||
<div className="flex items-center text-xs font-semibold">
|
<div
|
||||||
{uptime / 86400 >= 1
|
className={"items-center flex flex-row gap-2 whitespace-nowrap"}
|
||||||
? `${(uptime / 86400).toFixed(0)} ${t("serverCard.days")}`
|
>
|
||||||
: `${(uptime / 3600).toFixed(0)} ${t("serverCard.hours")}`}
|
<div className="text-xs font-semibold">
|
||||||
</div>
|
{platform.includes("Windows") ? (
|
||||||
</div>
|
<MageMicrosoftWindows className="size-[10px]" />
|
||||||
<div className={"flex w-14 flex-col"}>
|
) : (
|
||||||
<p className="text-xs text-muted-foreground">{"CPU"}</p>
|
<p className={`fl-${GetFontLogoClass(platform)}`} />
|
||||||
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div>
|
)}
|
||||||
<ServerUsageBar value={cpu} />
|
</div>
|
||||||
</div>
|
<div className={"flex w-14 flex-col"}>
|
||||||
<div className={"flex w-14 flex-col"}>
|
<p className="text-xs text-muted-foreground">
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.mem")}</p>
|
{t("serverCard.system")}
|
||||||
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div>
|
</p>
|
||||||
<ServerUsageBar value={mem} />
|
<div className="flex items-center text-[10.5px] font-semibold">
|
||||||
</div>
|
{platform.includes("Windows")
|
||||||
<div className={"flex w-14 flex-col"}>
|
? "Windows"
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p>
|
: GetOsName(platform)}
|
||||||
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div>
|
</div>
|
||||||
<ServerUsageBar value={stg} />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"flex w-16 flex-col"}>
|
<div className={"flex w-20 flex-col"}>
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
<div className="flex items-center text-xs font-semibold">
|
{t("serverCard.uptime")}
|
||||||
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : up >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`}
|
</p>
|
||||||
</div>
|
<div className="flex items-center text-xs font-semibold">
|
||||||
</div>
|
{uptime / 86400 >= 1
|
||||||
<div className={"flex w-16 flex-col"}>
|
? `${(uptime / 86400).toFixed(0)} ${t("serverCard.days")}`
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p>
|
: `${(uptime / 3600).toFixed(0)} ${t("serverCard.hours")}`}
|
||||||
<div className="flex items-center text-xs font-semibold">
|
</div>
|
||||||
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : down >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`}
|
</div>
|
||||||
</div>
|
<div className={"flex w-14 flex-col"}>
|
||||||
</div>
|
<p className="text-xs text-muted-foreground">{"CPU"}</p>
|
||||||
<div className={"flex w-20 flex-col"}>
|
<div className="flex items-center text-xs font-semibold">
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.totalUpload")}</p>
|
{cpu.toFixed(2)}%
|
||||||
<div className="flex items-center text-xs font-semibold">{formatBytes(net_out_transfer)}</div>
|
</div>
|
||||||
</div>
|
<ServerUsageBar value={cpu} />
|
||||||
<div className={"flex w-20 flex-col"}>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">{t("serverCard.totalDownload")}</p>
|
<div className={"flex w-14 flex-col"}>
|
||||||
<div className="flex items-center text-xs font-semibold">{formatBytes(net_in_transfer)}</div>
|
<p className="text-xs text-muted-foreground">
|
||||||
</div>
|
{t("serverCard.mem")}
|
||||||
</section>
|
</p>
|
||||||
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
<div className="flex items-center text-xs font-semibold">
|
||||||
</div>
|
{mem.toFixed(2)}%
|
||||||
</Card>
|
</div>
|
||||||
</section>
|
<ServerUsageBar value={mem} />
|
||||||
) : (
|
</div>
|
||||||
<Card
|
<div className={"flex w-14 flex-col"}>
|
||||||
className={cn(
|
<p className="text-xs text-muted-foreground">
|
||||||
"flex min-h-[61px] min-w-[900px] items-center justify-start p-3 md:px-5 flex-row cursor-pointer hover:bg-accent/50 transition-colors",
|
{t("serverCard.stg")}
|
||||||
{
|
</p>
|
||||||
"bg-card/70": customBackgroundImage,
|
<div className="flex items-center text-xs font-semibold">
|
||||||
},
|
{stg.toFixed(2)}%
|
||||||
)}
|
</div>
|
||||||
onClick={cardClick}
|
<ServerUsageBar value={stg} />
|
||||||
>
|
</div>
|
||||||
<section className={cn("grid items-center gap-2 w-40")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
|
<div className={"flex w-16 flex-col"}>
|
||||||
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
<p className="text-xs text-muted-foreground">
|
||||||
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
|
{t("serverCard.upload")}
|
||||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
</p>
|
||||||
</div>
|
<div className="flex items-center text-xs font-semibold">
|
||||||
<div className="relative flex flex-col">
|
{up >= 1024
|
||||||
<p className={cn("break-normal font-bold w-28 tracking-tight", showFlag ? "text-xs" : "text-sm")}>{name}</p>
|
? `${(up / 1024).toFixed(2)}G/s`
|
||||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
: up >= 1
|
||||||
</div>
|
? `${up.toFixed(2)}M/s`
|
||||||
</section>
|
: `${(up * 1024).toFixed(2)}K/s`}
|
||||||
<Separator orientation="vertical" className="h-8 ml-3 lg:ml-1 mr-3" />
|
</div>
|
||||||
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
</div>
|
||||||
</Card>
|
<div className={"flex w-16 flex-col"}>
|
||||||
)
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("serverCard.download")}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center text-xs font-semibold">
|
||||||
|
{down >= 1024
|
||||||
|
? `${(down / 1024).toFixed(2)}G/s`
|
||||||
|
: down >= 1
|
||||||
|
? `${down.toFixed(2)}M/s`
|
||||||
|
: `${(down * 1024).toFixed(2)}K/s`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"flex w-20 flex-col"}>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("serverCard.totalUpload")}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center text-xs font-semibold">
|
||||||
|
{formatBytes(net_out_transfer)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={"flex w-20 flex-col"}>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("serverCard.totalDownload")}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center text-xs font-semibold">
|
||||||
|
{formatBytes(net_in_transfer)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[61px] min-w-[900px] items-center justify-start p-3 md:px-5 flex-row cursor-pointer hover:bg-accent/50 transition-colors",
|
||||||
|
{
|
||||||
|
"bg-card/70": customBackgroundImage,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
onClick={cardClick}
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
className={cn("grid items-center gap-2 w-40")}
|
||||||
|
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||||
|
>
|
||||||
|
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center",
|
||||||
|
showFlag ? "min-w-[17px]" : "min-w-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||||
|
</div>
|
||||||
|
<div className="relative flex flex-col">
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"break-normal font-bold w-28 tracking-tight",
|
||||||
|
showFlag ? "text-xs" : "text-sm",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</p>
|
||||||
|
{parsedData?.billingDataMod && (
|
||||||
|
<BillingInfo parsedData={parsedData} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<Separator orientation="vertical" className="h-8 ml-3 lg:ml-1 mr-3" />
|
||||||
|
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+2016
-872
File diff suppressed because it is too large
Load Diff
@@ -1,310 +1,377 @@
|
|||||||
import { BackIcon } from "@/components/Icon"
|
import countries from "i18n-iso-countries";
|
||||||
import ServerFlag from "@/components/ServerFlag"
|
import enLocale from "i18n-iso-countries/langs/en.json";
|
||||||
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading"
|
import { useEffect, useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { useTranslation } from "react-i18next";
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useWebSocketContext } from "@/hooks/use-websocket-context"
|
import { BackIcon } from "@/components/Icon";
|
||||||
import { formatBytes } from "@/lib/format"
|
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading";
|
||||||
import { cn, formatNezhaInfo } from "@/lib/utils"
|
import ServerFlag from "@/components/ServerFlag";
|
||||||
import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
import { Badge } from "@/components/ui/badge";
|
||||||
import countries from "i18n-iso-countries"
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import enLocale from "i18n-iso-countries/langs/en.json"
|
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||||
import { useEffect, useState } from "react"
|
import { formatBytes } from "@/lib/format";
|
||||||
import { useTranslation } from "react-i18next"
|
import { cn, formatNezhaInfo } from "@/lib/utils";
|
||||||
import { useNavigate } from "react-router-dom"
|
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
|
||||||
|
|
||||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion"
|
import {
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "./ui/accordion";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "./ui/tooltip";
|
||||||
|
|
||||||
export default function ServerDetailOverview({ server_id }: { server_id: string }) {
|
export default function ServerDetailOverview({
|
||||||
const { t } = useTranslation()
|
server_id,
|
||||||
const navigate = useNavigate()
|
}: {
|
||||||
|
server_id: string;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [hasHistory, setHasHistory] = useState(false)
|
const [hasHistory, setHasHistory] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const previousPath = sessionStorage.getItem("fromMainPage")
|
const previousPath = sessionStorage.getItem("fromMainPage");
|
||||||
if (previousPath) {
|
if (previousPath) {
|
||||||
setHasHistory(true)
|
setHasHistory(true);
|
||||||
}
|
}
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const { lastMessage, connected } = useWebSocketContext()
|
const { lastMessage, connected } = useWebSocketContext();
|
||||||
|
|
||||||
if (!connected && !lastMessage) {
|
if (!connected && !lastMessage) {
|
||||||
return <ServerDetailLoading />
|
return <ServerDetailLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const linkClick = () => {
|
const linkClick = () => {
|
||||||
if (hasHistory) {
|
if (hasHistory) {
|
||||||
navigate(-1)
|
navigate(-1);
|
||||||
} else {
|
} else {
|
||||||
navigate("/")
|
navigate("/");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
|
const nezhaWsData = lastMessage
|
||||||
|
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
|
||||||
|
: null;
|
||||||
|
|
||||||
if (!nezhaWsData) {
|
if (!nezhaWsData) {
|
||||||
return <ServerDetailLoading />
|
return <ServerDetailLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id))
|
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
|
||||||
|
|
||||||
if (!server) {
|
if (!server) {
|
||||||
return <ServerDetailLoading />
|
return <ServerDetailLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
online,
|
online,
|
||||||
uptime,
|
uptime,
|
||||||
version,
|
version,
|
||||||
arch,
|
arch,
|
||||||
mem_total,
|
mem_total,
|
||||||
disk_total,
|
disk_total,
|
||||||
country_code,
|
country_code,
|
||||||
platform,
|
platform,
|
||||||
platform_version,
|
platform_version,
|
||||||
cpu_info,
|
cpu_info,
|
||||||
gpu_info,
|
gpu_info,
|
||||||
load_1,
|
load_1,
|
||||||
load_5,
|
load_5,
|
||||||
load_15,
|
load_15,
|
||||||
net_out_transfer,
|
net_out_transfer,
|
||||||
net_in_transfer,
|
net_in_transfer,
|
||||||
last_active_time_string,
|
last_active_time_string,
|
||||||
boot_time_string,
|
boot_time_string,
|
||||||
} = formatNezhaInfo(nezhaWsData.now, server)
|
} = formatNezhaInfo(nezhaWsData.now, server);
|
||||||
|
|
||||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
const customBackgroundImage =
|
||||||
|
(window.CustomBackgroundImage as string) !== ""
|
||||||
|
? window.CustomBackgroundImage
|
||||||
|
: undefined;
|
||||||
|
|
||||||
countries.registerLocale(enLocale)
|
countries.registerLocale(enLocale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn({
|
className={cn({
|
||||||
"bg-card/70 p-4 rounded-[10px]": customBackgroundImage,
|
"bg-card/70 p-4 rounded-[10px]": customBackgroundImage,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={linkClick}
|
onClick={linkClick}
|
||||||
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-1 text-xl server-name"
|
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-1 text-xl server-name"
|
||||||
>
|
>
|
||||||
<BackIcon />
|
<BackIcon />
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
<section className="flex flex-wrap gap-2 mt-3">
|
<section className="flex flex-wrap gap-2 mt-3">
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
<CardContent className="px-1.5 py-1">
|
<CardContent className="px-1.5 py-1">
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
<p className="text-xs text-muted-foreground">{t("serverDetail.status")}</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
<Badge
|
{t("serverDetail.status")}
|
||||||
className={cn("text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white", {
|
</p>
|
||||||
" bg-green-800": online,
|
<Badge
|
||||||
" bg-red-600": !online,
|
className={cn(
|
||||||
})}
|
"text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white",
|
||||||
>
|
{
|
||||||
{online ? t("serverDetail.online") : t("serverDetail.offline")}
|
" bg-green-800": online,
|
||||||
</Badge>
|
" bg-red-600": !online,
|
||||||
</section>
|
},
|
||||||
</CardContent>
|
)}
|
||||||
</Card>
|
>
|
||||||
{online && (
|
{online ? t("serverDetail.online") : t("serverDetail.offline")}
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
</Badge>
|
||||||
<CardContent className="px-1.5 py-1">
|
</section>
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
</CardContent>
|
||||||
<p className="text-xs text-muted-foreground">{t("serverDetail.uptime")}</p>
|
</Card>
|
||||||
<div className="text-xs">
|
{online && (
|
||||||
{" "}
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
{uptime / 86400 >= 1
|
<CardContent className="px-1.5 py-1">
|
||||||
? `${Math.floor(uptime / 86400)} ${t("serverDetail.days")} ${Math.floor((uptime % 86400) / 3600)} ${t("serverDetail.hours")}`
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
: `${Math.floor(uptime / 3600)} ${t("serverDetail.hours")}`}
|
<p className="text-xs text-muted-foreground">
|
||||||
</div>
|
{t("serverDetail.uptime")}
|
||||||
</section>
|
</p>
|
||||||
</CardContent>
|
<div className="text-xs">
|
||||||
</Card>
|
{" "}
|
||||||
)}
|
{uptime / 86400 >= 1
|
||||||
{version && (
|
? `${Math.floor(uptime / 86400)} ${t("serverDetail.days")} ${Math.floor((uptime % 86400) / 3600)} ${t("serverDetail.hours")}`
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
: `${Math.floor(uptime / 3600)} ${t("serverDetail.hours")}`}
|
||||||
<CardContent className="px-1.5 py-1">
|
</div>
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
</section>
|
||||||
<p className="text-xs text-muted-foreground">{t("serverDetail.version")}</p>
|
</CardContent>
|
||||||
<div className="text-xs">{version} </div>
|
</Card>
|
||||||
</section>
|
)}
|
||||||
</CardContent>
|
{version && (
|
||||||
</Card>
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
)}
|
<CardContent className="px-1.5 py-1">
|
||||||
{arch && (
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
<p className="text-xs text-muted-foreground">
|
||||||
<CardContent className="px-1.5 py-1">
|
{t("serverDetail.version")}
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">{t("serverDetail.arch")}</p>
|
<div className="text-xs">{version} </div>
|
||||||
<div className="text-xs">{arch} </div>
|
</section>
|
||||||
</section>
|
</CardContent>
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
)}
|
||||||
)}
|
{arch && (
|
||||||
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
|
<CardContent className="px-1.5 py-1">
|
||||||
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("serverDetail.arch")}
|
||||||
|
</p>
|
||||||
|
<div className="text-xs">{arch} </div>
|
||||||
|
</section>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{mem_total ? (
|
{mem_total ? (
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
<CardContent className="px-1.5 py-1">
|
<CardContent className="px-1.5 py-1">
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
<p className="text-xs text-muted-foreground">{t("serverDetail.mem")}</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
<div className="text-xs">{formatBytes(mem_total)}</div>
|
{t("serverDetail.mem")}
|
||||||
</section>
|
</p>
|
||||||
</CardContent>
|
<div className="text-xs">{formatBytes(mem_total)}</div>
|
||||||
</Card>
|
</section>
|
||||||
) : null}
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{disk_total ? (
|
{disk_total ? (
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
<CardContent className="px-1.5 py-1">
|
<CardContent className="px-1.5 py-1">
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
<p className="text-xs text-muted-foreground">{t("serverDetail.disk")}</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
<div className="text-xs">{formatBytes(disk_total)}</div>
|
{t("serverDetail.disk")}
|
||||||
</section>
|
</p>
|
||||||
</CardContent>
|
<div className="text-xs">{formatBytes(disk_total)}</div>
|
||||||
</Card>
|
</section>
|
||||||
) : null}
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{country_code && (
|
{country_code && (
|
||||||
<TooltipProvider delayDuration={100}>
|
<TooltipProvider delayDuration={100}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
<CardContent className="px-1.5 py-1">
|
<CardContent className="px-1.5 py-1">
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
<p className="text-xs text-muted-foreground">{t("serverDetail.region")}</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
<section className="flex items-start gap-1">
|
{t("serverDetail.region")}
|
||||||
<div className="text-xs text-start">{country_code?.toUpperCase()}</div>
|
</p>
|
||||||
{country_code && <ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} />}
|
<section className="flex items-start gap-1">
|
||||||
</section>
|
<div className="text-xs text-start">
|
||||||
</section>
|
{country_code?.toUpperCase()}
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
{country_code && (
|
||||||
</TooltipTrigger>
|
<ServerFlag
|
||||||
<TooltipContent>
|
className="text-[11px] -mt-px"
|
||||||
<p>{countries.getName(country_code?.toUpperCase(), "en")}</p>
|
country_code={country_code}
|
||||||
</TooltipContent>
|
/>
|
||||||
</Tooltip>
|
)}
|
||||||
</TooltipProvider>
|
</section>
|
||||||
)}
|
</section>
|
||||||
</section>
|
</CardContent>
|
||||||
<section className="flex flex-wrap gap-2 mt-1">
|
</Card>
|
||||||
{platform && (
|
</TooltipTrigger>
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
<TooltipContent>
|
||||||
<CardContent className="px-1.5 py-1">
|
<p>{countries.getName(country_code?.toUpperCase(), "en")}</p>
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
</TooltipContent>
|
||||||
<p className="text-xs text-muted-foreground">{t("serverDetail.system")}</p>
|
</Tooltip>
|
||||||
<div className="text-xs">
|
</TooltipProvider>
|
||||||
{" "}
|
)}
|
||||||
{platform} {platform_version ? " - " + platform_version : ""}
|
</section>
|
||||||
</div>
|
<section className="flex flex-wrap gap-2 mt-1">
|
||||||
</section>
|
{platform && (
|
||||||
</CardContent>
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
</Card>
|
<CardContent className="px-1.5 py-1">
|
||||||
)}
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
{cpu_info.length > 0 && (
|
<p className="text-xs text-muted-foreground">
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
{t("serverDetail.system")}
|
||||||
<CardContent className="px-1.5 py-1">
|
</p>
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
<div className="text-xs">
|
||||||
<p className="text-xs text-muted-foreground">{"CPU"}</p>
|
{" "}
|
||||||
<div className="text-xs"> {cpu_info.join(", ")}</div>
|
{platform} {platform_version ? ` - ${platform_version}` : ""}
|
||||||
</section>
|
</div>
|
||||||
</CardContent>
|
</section>
|
||||||
</Card>
|
</CardContent>
|
||||||
)}
|
</Card>
|
||||||
{gpu_info.length > 0 && (
|
)}
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
{cpu_info.length > 0 && (
|
||||||
<CardContent className="px-1.5 py-1">
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
<CardContent className="px-1.5 py-1">
|
||||||
<p className="text-xs text-muted-foreground">{"GPU"}</p>
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
<div className="text-xs">{gpu_info.join(", ")}</div>
|
<p className="text-xs text-muted-foreground">{"CPU"}</p>
|
||||||
</section>
|
<div className="text-xs"> {cpu_info.join(", ")}</div>
|
||||||
</CardContent>
|
</section>
|
||||||
</Card>
|
</CardContent>
|
||||||
)}
|
</Card>
|
||||||
</section>
|
)}
|
||||||
<section className="flex flex-wrap gap-2 mt-1">
|
{gpu_info.length > 0 && (
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
<CardContent className="px-1.5 py-1">
|
<CardContent className="px-1.5 py-1">
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
<p className="text-xs text-muted-foreground">{"Load"}</p>
|
<p className="text-xs text-muted-foreground">{"GPU"}</p>
|
||||||
<div className="text-xs">
|
<div className="text-xs">{gpu_info.join(", ")}</div>
|
||||||
{load_1} / {load_5} / {load_15}
|
</section>
|
||||||
</div>
|
</CardContent>
|
||||||
</section>
|
</Card>
|
||||||
</CardContent>
|
)}
|
||||||
</Card>
|
</section>
|
||||||
{net_out_transfer ? (
|
<section className="flex flex-wrap gap-2 mt-1">
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
<CardContent className="px-1.5 py-1">
|
<CardContent className="px-1.5 py-1">
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
<p className="text-xs text-muted-foreground">{t("serverDetail.upload")}</p>
|
<p className="text-xs text-muted-foreground">{"Load"}</p>
|
||||||
{net_out_transfer ? (
|
<div className="text-xs">
|
||||||
<div className="text-xs"> {formatBytes(net_out_transfer)} </div>
|
{load_1} / {load_5} / {load_15}
|
||||||
) : (
|
</div>
|
||||||
<div className="text-xs"> {t("serverDetail.unknown")}</div>
|
</section>
|
||||||
)}
|
</CardContent>
|
||||||
</section>
|
</Card>
|
||||||
</CardContent>
|
{net_out_transfer ? (
|
||||||
</Card>
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
) : null}
|
<CardContent className="px-1.5 py-1">
|
||||||
{net_in_transfer ? (
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
<p className="text-xs text-muted-foreground">
|
||||||
<CardContent className="px-1.5 py-1">
|
{t("serverDetail.upload")}
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">{t("serverDetail.download")}</p>
|
{net_out_transfer ? (
|
||||||
{net_in_transfer ? (
|
<div className="text-xs">
|
||||||
<div className="text-xs"> {formatBytes(net_in_transfer)} </div>
|
{" "}
|
||||||
) : (
|
{formatBytes(net_out_transfer)}{" "}
|
||||||
<div className="text-xs"> {t("serverDetail.unknown")}</div>
|
</div>
|
||||||
)}
|
) : (
|
||||||
</section>
|
<div className="text-xs"> {t("serverDetail.unknown")}</div>
|
||||||
</CardContent>
|
)}
|
||||||
</Card>
|
</section>
|
||||||
) : null}
|
</CardContent>
|
||||||
</section>
|
</Card>
|
||||||
<section className="flex flex-wrap gap-2 mt-1">
|
) : null}
|
||||||
{server?.state.temperatures && server?.state.temperatures.length > 0 && (
|
{net_in_transfer ? (
|
||||||
<section className="flex flex-wrap gap-2 ml-1.5">
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
<Accordion type="single" collapsible className="w-fit">
|
<CardContent className="px-1.5 py-1">
|
||||||
<AccordionItem value="item-1" className="border-none">
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
<AccordionTrigger className="text-xs py-0 text-muted-foreground font-normal">{t("serverDetail.temperature")}</AccordionTrigger>
|
<p className="text-xs text-muted-foreground">
|
||||||
<AccordionContent className="pb-0">
|
{t("serverDetail.download")}
|
||||||
<section className="flex items-start flex-wrap gap-2">
|
</p>
|
||||||
{server?.state.temperatures.map((item, index) => (
|
{net_in_transfer ? (
|
||||||
<div className="text-xs flex items-center" key={index}>
|
<div className="text-xs">
|
||||||
<p className="font-semibold">{item.Name}</p>: {item.Temperature.toFixed(2)} °C
|
{" "}
|
||||||
</div>
|
{formatBytes(net_in_transfer)}{" "}
|
||||||
))}
|
</div>
|
||||||
</section>
|
) : (
|
||||||
</AccordionContent>
|
<div className="text-xs"> {t("serverDetail.unknown")}</div>
|
||||||
</AccordionItem>
|
)}
|
||||||
</Accordion>
|
</section>
|
||||||
</section>
|
</CardContent>
|
||||||
)}
|
</Card>
|
||||||
</section>
|
) : null}
|
||||||
|
</section>
|
||||||
|
<section className="flex flex-wrap gap-2 mt-1">
|
||||||
|
{server?.state.temperatures &&
|
||||||
|
server?.state.temperatures.length > 0 && (
|
||||||
|
<section className="flex flex-wrap gap-2 ml-1.5">
|
||||||
|
<Accordion type="single" collapsible className="w-fit">
|
||||||
|
<AccordionItem value="item-1" className="border-none">
|
||||||
|
<AccordionTrigger className="text-xs py-0 text-muted-foreground font-normal">
|
||||||
|
{t("serverDetail.temperature")}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="pb-0">
|
||||||
|
<section className="flex items-start flex-wrap gap-2">
|
||||||
|
{server?.state.temperatures.map((item, index) => (
|
||||||
|
<div className="text-xs flex items-center" key={index}>
|
||||||
|
<p className="font-semibold">{item.Name}</p>:{" "}
|
||||||
|
{item.Temperature.toFixed(2)} °C
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="flex flex-wrap gap-2 mt-1">
|
<section className="flex flex-wrap gap-2 mt-1">
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
<CardContent className="px-1.5 py-1">
|
<CardContent className="px-1.5 py-1">
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
<p className="text-xs text-muted-foreground">{t("serverDetail.bootTime")}</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
<div className="text-xs">{boot_time_string ? boot_time_string : "N/A"}</div>
|
{t("serverDetail.bootTime")}
|
||||||
</section>
|
</p>
|
||||||
</CardContent>
|
<div className="text-xs">
|
||||||
</Card>
|
{boot_time_string ? boot_time_string : "N/A"}
|
||||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
</div>
|
||||||
<CardContent className="px-1.5 py-1">
|
</section>
|
||||||
<section className="flex flex-col items-start gap-0.5">
|
</CardContent>
|
||||||
<p className="text-xs text-muted-foreground">{t("serverDetail.lastActive")}</p>
|
</Card>
|
||||||
<div className="text-xs">{last_active_time_string ? last_active_time_string : "N/A"}</div>
|
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||||
</section>
|
<CardContent className="px-1.5 py-1">
|
||||||
</CardContent>
|
<section className="flex flex-col items-start gap-0.5">
|
||||||
</Card>
|
<p className="text-xs text-muted-foreground">
|
||||||
</section>
|
{t("serverDetail.lastActive")}
|
||||||
</div>
|
</p>
|
||||||
)
|
<div className="text-xs">
|
||||||
|
{last_active_time_string ? last_active_time_string : "N/A"}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||||
|
import { formatNezhaInfo } from "@/lib/utils";
|
||||||
|
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
|
||||||
|
|
||||||
|
export default function ServerDetailSummary({
|
||||||
|
server_id,
|
||||||
|
}: {
|
||||||
|
server_id: number;
|
||||||
|
}) {
|
||||||
|
const { lastMessage, connected } = useWebSocketContext();
|
||||||
|
|
||||||
|
if (!connected && !lastMessage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nezhaWsData = lastMessage
|
||||||
|
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!nezhaWsData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cpu, mem, disk, up, down, tcp, udp, process } = formatNezhaInfo(
|
||||||
|
nezhaWsData.now,
|
||||||
|
server,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-2 flex flex-wrap items-center gap-4 server-detail-summary">
|
||||||
|
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
|
||||||
|
<section className="flex items-center justify-between">
|
||||||
|
<span className="text-[10px] text-muted-foreground">CPU</span>
|
||||||
|
<span className="font-medium text-[10px]">{cpu.toFixed(2)}%</span>
|
||||||
|
</section>
|
||||||
|
<UsageBar value={cpu} />
|
||||||
|
</section>
|
||||||
|
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
|
||||||
|
<section className="flex items-center justify-between">
|
||||||
|
<span className="text-[10px] text-muted-foreground">Mem</span>
|
||||||
|
<span className="font-medium text-[10px]">{mem.toFixed(2)}%</span>
|
||||||
|
</section>
|
||||||
|
<UsageBar value={mem} />
|
||||||
|
</section>
|
||||||
|
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
|
||||||
|
<section className="flex items-center justify-between">
|
||||||
|
<span className="text-[10px] text-muted-foreground">Disk</span>
|
||||||
|
<span className="font-medium text-[10px]">{disk.toFixed(2)}%</span>
|
||||||
|
</section>
|
||||||
|
<UsageBar value={disk} />
|
||||||
|
</section>
|
||||||
|
<section className="flex min-w-[85px] flex-col justify-center px-1.5 py-1">
|
||||||
|
<section className="flex items-center justify-between gap-4">
|
||||||
|
<span className="text-[10px] text-muted-foreground">Process</span>
|
||||||
|
<span className="font-medium text-[10px]">{process}</span>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section className="flex min-w-[70px] flex-col justify-center gap-0.5 px-1.5 py-1">
|
||||||
|
<section className="flex items-center justify-between gap-4">
|
||||||
|
<span className="text-[10px] text-muted-foreground">TCP</span>
|
||||||
|
<span className="font-medium text-[10px]">{tcp}</span>
|
||||||
|
</section>
|
||||||
|
<section className="flex items-center justify-between gap-4">
|
||||||
|
<span className="text-[10px] text-muted-foreground">UDP</span>
|
||||||
|
<span className="font-medium text-[10px]">{udp}</span>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section className="flex min-w-[120px] flex-col justify-center gap-0.5 px-1.5 py-1">
|
||||||
|
<section className="flex items-center justify-between gap-4">
|
||||||
|
<span className="text-[10px] text-muted-foreground">Upload</span>
|
||||||
|
<span className="font-medium text-[10px]">{up.toFixed(2)}M/s</span>
|
||||||
|
</section>
|
||||||
|
<section className="flex items-center justify-between gap-4">
|
||||||
|
<span className="text-[10px] text-muted-foreground">Download</span>
|
||||||
|
<span className="font-medium text-[10px]">{down.toFixed(2)}M/s</span>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type UsageBarProps = {
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function UsageBar({ value }: UsageBarProps) {
|
||||||
|
return (
|
||||||
|
<Progress
|
||||||
|
aria-label={"Server Usage Bar"}
|
||||||
|
aria-labelledby={"Server Usage Bar"}
|
||||||
|
value={value}
|
||||||
|
indicatorClassName={
|
||||||
|
value > 90
|
||||||
|
? "bg-red-500"
|
||||||
|
: value > 70
|
||||||
|
? "bg-orange-400"
|
||||||
|
: "bg-green-500"
|
||||||
|
}
|
||||||
|
className={"h-[3px] rounded-sm bg-stone-200 dark:bg-stone-800"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,42 +1,52 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import getUnicodeFlagIcon from "country-flag-icons/unicode";
|
||||||
import getUnicodeFlagIcon from "country-flag-icons/unicode"
|
import { useEffect, useState } from "react";
|
||||||
import { useEffect, useState } from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function ServerFlag({ country_code, className }: { country_code: string; className?: string }) {
|
export default function ServerFlag({
|
||||||
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false)
|
country_code,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
country_code: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false);
|
||||||
|
|
||||||
// @ts-expect-error ForceUseSvgFlag is a global variable
|
// @ts-expect-error ForceUseSvgFlag is a global variable
|
||||||
const forceUseSvgFlag = window.ForceUseSvgFlag as boolean
|
const forceUseSvgFlag = window.ForceUseSvgFlag as boolean;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (forceUseSvgFlag) {
|
if (forceUseSvgFlag) {
|
||||||
// 如果环境变量要求直接使用 SVG,则无需检查 Emoji 支持
|
// 如果环境变量要求直接使用 SVG,则无需检查 Emoji 支持
|
||||||
setSupportsEmojiFlags(false)
|
setSupportsEmojiFlags(false);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkEmojiSupport = () => {
|
const checkEmojiSupport = () => {
|
||||||
const canvas = document.createElement("canvas")
|
const canvas = document.createElement("canvas");
|
||||||
const ctx = canvas.getContext("2d")
|
const ctx = canvas.getContext("2d");
|
||||||
const emojiFlag = "🇺🇸" // 使用美国国旗作为测试
|
const emojiFlag = "🇺🇸"; // 使用美国国旗作为测试
|
||||||
if (!ctx) return
|
if (!ctx) return;
|
||||||
ctx.fillStyle = "#000"
|
ctx.fillStyle = "#000";
|
||||||
ctx.textBaseline = "top"
|
ctx.textBaseline = "top";
|
||||||
ctx.font = "32px Arial"
|
ctx.font = "32px Arial";
|
||||||
ctx.fillText(emojiFlag, 0, 0)
|
ctx.fillText(emojiFlag, 0, 0);
|
||||||
|
|
||||||
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0
|
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0;
|
||||||
setSupportsEmojiFlags(support)
|
setSupportsEmojiFlags(support);
|
||||||
}
|
};
|
||||||
|
|
||||||
checkEmojiSupport()
|
checkEmojiSupport();
|
||||||
}, [])
|
}, [forceUseSvgFlag]);
|
||||||
|
|
||||||
if (!country_code) return null
|
if (!country_code) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={cn("text-[12px] text-muted-foreground", className)}>
|
<span className={cn("text-[12px] text-muted-foreground", className)}>
|
||||||
{forceUseSvgFlag || !supportsEmojiFlags ? <span className={`fi fi-${country_code}`} /> : getUnicodeFlagIcon(country_code)}
|
{forceUseSvgFlag || !supportsEmojiFlags ? (
|
||||||
</span>
|
<span className={`fi fi-${country_code}`} />
|
||||||
)
|
) : (
|
||||||
|
getUnicodeFlagIcon(country_code)
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+199
-149
@@ -1,158 +1,208 @@
|
|||||||
// src/components/ServerOverview.tsx (最终完整版)
|
import {
|
||||||
|
ArrowDownCircleIcon,
|
||||||
|
ArrowUpCircleIcon,
|
||||||
|
} from "@heroicons/react/20/solid";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { useStatus } from "@/hooks/use-status";
|
||||||
|
import { formatBytes } from "@/lib/format";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Globe } from "lucide-react";
|
||||||
|
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
|
||||||
import { useStatus } from "@/hooks/use-status"
|
|
||||||
import { formatBytes} from "@/lib/format"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { Globe } from "lucide-react"
|
|
||||||
|
|
||||||
// 扩展 props 类型,以接收域名总数和新的交互逻辑
|
|
||||||
type ServerOverviewProps = {
|
type ServerOverviewProps = {
|
||||||
online: number
|
online: number;
|
||||||
offline: number
|
offline: number;
|
||||||
total: number
|
total: number;
|
||||||
up: number
|
up: number;
|
||||||
down: number
|
down: number;
|
||||||
upSpeed: number
|
upSpeed: number;
|
||||||
downSpeed: number
|
downSpeed: number;
|
||||||
totalDomains: number // 新增:接收域名总数
|
totalDomains: number; // 新增:接收域名总数
|
||||||
onViewChange: (view: 'servers' | 'domains') => void // 新增:点击事件回调
|
onViewChange: (view: 'servers' | 'domains') => void; // 新增:点击事件回调
|
||||||
activeView: 'servers' | 'domains' // 新增:当前激活的视图
|
activeView: 'servers' | 'domains'; // 新增:当前激活的视图
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function ServerOverview({
|
export default function ServerOverview({
|
||||||
online,
|
online,
|
||||||
offline,
|
offline,
|
||||||
total,
|
total,
|
||||||
up,
|
up,
|
||||||
down,
|
down,
|
||||||
upSpeed,
|
upSpeed,
|
||||||
downSpeed,
|
downSpeed,
|
||||||
totalDomains,
|
totalDomains,
|
||||||
onViewChange,
|
onViewChange,
|
||||||
activeView,
|
activeView,
|
||||||
}: ServerOverviewProps) {
|
}: ServerOverviewProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const { status, setStatus } = useStatus()
|
const { status, setStatus } = useStatus();
|
||||||
|
|
||||||
// --- 所有原始变量和逻辑保持不变 ---
|
// @ts-expect-error DisableAnimatedMan is a global variable
|
||||||
const disableAnimatedMan = (window as any).DisableAnimatedMan as boolean
|
const disableAnimatedMan = window.DisableAnimatedMan as boolean;
|
||||||
const customIllustration = (window as any).CustomIllustration || "/animated-man.webp"
|
|
||||||
const customBackgroundImage = (window as any).CustomBackgroundImage !== "" ? (window as any).CustomBackgroundImage : undefined
|
|
||||||
|
|
||||||
// 新增:一个组合了两个动作的点击处理函数
|
// @ts-expect-error CustomIllustration is a global variable
|
||||||
const handleServerCardClick = (serverStatus: 'all' | 'online' | 'offline') => {
|
const customIllustration = window.CustomIllustration || "/animated-man.webp";
|
||||||
onViewChange('servers'); // 动作1: 确保视图切换回服务器
|
|
||||||
setStatus(serverStatus); // 动作2: 执行原有的状态筛选
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
const customBackgroundImage =
|
||||||
<>
|
(window.CustomBackgroundImage as string) !== ""
|
||||||
<section className="grid grid-cols-2 gap-4 lg:grid-cols-5 server-overview">
|
? window.CustomBackgroundImage
|
||||||
<Card
|
: undefined;
|
||||||
onClick={() => handleServerCardClick("all")}
|
|
||||||
className={cn(
|
|
||||||
"hover:border-blue-500 cursor-pointer transition-all",
|
|
||||||
{ "bg-card/70": customBackgroundImage },
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CardContent className="flex h-full items-center px-6 py-3">
|
|
||||||
<section className="flex flex-col gap-1">
|
|
||||||
<p className="text-sm font-medium md:text-base">{t("serverOverview.totalServers")}</p>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="relative flex h-2 w-2">
|
|
||||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span>
|
|
||||||
</span>
|
|
||||||
<div className="text-lg font-semibold">{total}</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card
|
|
||||||
onClick={() => handleServerCardClick("online")}
|
|
||||||
className={cn(
|
|
||||||
"cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all",
|
|
||||||
{ "bg-card/70": customBackgroundImage },
|
|
||||||
{ "ring-green-500 ring-2 border-transparent": activeView === 'servers' && status === "online" }
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CardContent className="flex h-full items-center px-6 py-3">
|
|
||||||
<section className="flex flex-col gap-1">
|
|
||||||
<p className="text-sm font-medium md:text-base">{t("serverOverview.onlineServers")}</p>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="relative flex h-2 w-2">
|
|
||||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
|
|
||||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
|
|
||||||
</span>
|
|
||||||
<div className="text-lg font-semibold">{online}</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card
|
|
||||||
onClick={() => handleServerCardClick("offline")}
|
|
||||||
className={cn(
|
|
||||||
"cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all",
|
|
||||||
{ "bg-card/70": customBackgroundImage },
|
|
||||||
{ "ring-red-500 ring-2 border-transparent": activeView === 'servers' && status === "offline" }
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CardContent className="flex h-full items-center px-6 py-3">
|
|
||||||
<section className="flex flex-col gap-1">
|
|
||||||
<p className="text-sm font-medium md:text-base">{t("serverOverview.offlineServers")}</p>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="relative flex h-2 w-2">
|
|
||||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"></span>
|
|
||||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
|
|
||||||
</span>
|
|
||||||
<div className="text-lg font-semibold">{offline}</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
onClick={() => onViewChange('domains')}
|
|
||||||
className={cn(
|
|
||||||
"cursor-pointer hover:ring-indigo-500 ring-1 ring-transparent transition-all",
|
|
||||||
{ "bg-card/70": customBackgroundImage },
|
|
||||||
{ "ring-indigo-500 ring-2 border-transparent": activeView === 'domains' }
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CardContent className="flex h-full items-center px-6 py-3">
|
|
||||||
<section className="flex flex-col gap-1">
|
|
||||||
<p className="text-sm font-medium md:text-base">总域名数</p>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Globe className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<div className="text-lg font-semibold">{totalDomains}</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card
|
// 新增:一个组合了两个动作的点击处理函数
|
||||||
className={cn("hover:ring-purple-500 ring-1 ring-transparent transition-all", { "bg-card/70": customBackgroundImage })}
|
const handleServerCardClick = (serverStatus: 'all' | 'online' | 'offline') => {
|
||||||
>
|
onViewChange('servers'); // 动作1: 确保视图切换回服务器
|
||||||
<CardContent className="flex h-full items-center relative px-6 py-3">
|
setStatus(serverStatus); // 动作2: 执行原有的状态筛选
|
||||||
<section className="flex flex-col gap-1 w-full">
|
};
|
||||||
<div className="flex items-center w-full justify-between"><p className="text-sm font-medium md:text-base">{t("serverOverview.network")}</p></div>
|
|
||||||
<section className="flex items-start flex-row z-10 pr-0 gap-1">
|
return (
|
||||||
<p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">↑{formatBytes(up)}</p>
|
<section className="grid grid-cols-2 gap-4 lg:grid-cols-5 server-overview">
|
||||||
<p className="sm:text-[12px] text-[10px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">↓{formatBytes(down)}</p>
|
<Card
|
||||||
</section>
|
onClick={() => {
|
||||||
<section className="flex flex-col sm:flex-row -mr-1 sm:items-center items-start gap-1">
|
handleServerCardClick("all");
|
||||||
<p className="text-[11px] flex items-center text-nowrap font-semibold"><ArrowUpCircleIcon className="size-3 mr-0.5 sm:mb-[1px]" />{formatBytes(upSpeed)}/s</p>
|
}}
|
||||||
<p className="text-[11px] flex items-center text-nowrap font-semibold"><ArrowDownCircleIcon className="size-3 mr-0.5" />{formatBytes(downSpeed)}/s</p>
|
className={cn("hover:border-blue-500 cursor-pointer transition-all", {
|
||||||
</section>
|
"bg-card/70": customBackgroundImage,
|
||||||
</section>
|
})}
|
||||||
{!disableAnimatedMan && (
|
>
|
||||||
<img className="absolute right-[-30px] top-[-120px] z-10 w-40 scale-100 group-hover:opacity-50 md:scale-100 transition-all" alt={"animated-man"} src={customIllustration} loading="eager" />
|
<CardContent className="flex h-full items-center px-6 py-3">
|
||||||
)}
|
<section className="flex flex-col gap-1">
|
||||||
</CardContent>
|
<p className="text-sm font-medium md:text-base">
|
||||||
</Card>
|
{t("serverOverview.totalServers")}
|
||||||
</section>
|
</p>
|
||||||
</>
|
<div className="flex items-center gap-2">
|
||||||
)
|
<span className="relative flex h-2 w-2">
|
||||||
}
|
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span>
|
||||||
|
</span>
|
||||||
|
<div className="text-lg font-semibold">{total}</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
onClick={() => {
|
||||||
|
handleServerCardClick("online");
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all",
|
||||||
|
{
|
||||||
|
"bg-card/70": customBackgroundImage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ring-green-500 ring-2 border-transparent": activeView === "servers" && status === "online",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CardContent className="flex h-full items-center px-6 py-3">
|
||||||
|
<section className="flex flex-col gap-1">
|
||||||
|
<p className="text-sm font-medium md:text-base">
|
||||||
|
{t("serverOverview.onlineServers")}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="relative flex h-2 w-2">
|
||||||
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="text-lg font-semibold">{online}</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
onClick={() => {
|
||||||
|
handleServerCardClick("offline");
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all",
|
||||||
|
{
|
||||||
|
"bg-card/70": customBackgroundImage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ring-red-500 ring-2 border-transparent": activeView === "servers" && status === "offline",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CardContent className="flex h-full items-center px-6 py-3">
|
||||||
|
<section className="flex flex-col gap-1">
|
||||||
|
<p className="text-sm font-medium md:text-base">
|
||||||
|
{t("serverOverview.offlineServers")}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="relative flex h-2 w-2">
|
||||||
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
|
||||||
|
</span>
|
||||||
|
<div className="text-lg font-semibold">{offline}</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
onClick={() => onViewChange("domains")}
|
||||||
|
className={cn(
|
||||||
|
"cursor-pointer hover:ring-indigo-500 ring-1 ring-transparent transition-all",
|
||||||
|
{
|
||||||
|
"bg-card/70": customBackgroundImage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ring-indigo-500 ring-2 border-transparent": activeView === "domains",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CardContent className="flex h-full items-center px-6 py-3">
|
||||||
|
<section className="flex flex-col gap-1">
|
||||||
|
<p className="text-sm font-medium md:text-base">总域名数</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Globe className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="text-lg font-semibold">{totalDomains}</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
"hover:ring-purple-500 ring-1 ring-transparent transition-all",
|
||||||
|
{
|
||||||
|
"bg-card/70": customBackgroundImage,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CardContent className="flex h-full items-center relative px-6 py-3">
|
||||||
|
<section className="flex flex-col gap-1 w-full">
|
||||||
|
<div className="flex items-center w-full justify-between">
|
||||||
|
<p className="text-sm font-medium md:text-base">
|
||||||
|
{t("serverOverview.network")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<section className="flex items-start flex-row z-10 pr-0 gap-1">
|
||||||
|
<p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">
|
||||||
|
↑{formatBytes(up)}
|
||||||
|
</p>
|
||||||
|
<p className="sm:text-[12px] text-[10px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">
|
||||||
|
↓{formatBytes(down)}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section className="flex flex-col sm:flex-row -mr-1 sm:items-center items-start gap-1">
|
||||||
|
<p className="text-[11px] flex items-center text-nowrap font-semibold">
|
||||||
|
<ArrowUpCircleIcon className="size-3 mr-0.5 sm:mb-px" />
|
||||||
|
{formatBytes(upSpeed)}/s
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] flex items-center text-nowrap font-semibold">
|
||||||
|
<ArrowDownCircleIcon className="size-3 mr-0.5" />
|
||||||
|
{formatBytes(downSpeed)}/s
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
{!disableAnimatedMan && (
|
||||||
|
<img
|
||||||
|
className="absolute right-3 top-[-85px] z-50 w-20 scale-90 group-hover:opacity-50 md:scale-100 transition-all"
|
||||||
|
alt={"animated-man"}
|
||||||
|
src={customIllustration}
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
import { Progress } from "@/components/ui/progress"
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
|
||||||
type ServerUsageBarProps = {
|
type ServerUsageBarProps = {
|
||||||
value: number
|
value: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function ServerUsageBar({ value }: ServerUsageBarProps) {
|
export default function ServerUsageBar({ value }: ServerUsageBarProps) {
|
||||||
return (
|
return (
|
||||||
<Progress
|
<Progress
|
||||||
aria-label={"Server Usage Bar"}
|
aria-label={"Server Usage Bar"}
|
||||||
aria-labelledby={"Server Usage Bar"}
|
aria-labelledby={"Server Usage Bar"}
|
||||||
value={value}
|
value={value}
|
||||||
indicatorClassName={value > 90 ? "bg-red-500" : value > 70 ? "bg-orange-400" : "bg-green-500"}
|
indicatorClassName={
|
||||||
className={"h-[3px] rounded-sm"}
|
value > 90
|
||||||
/>
|
? "bg-red-500"
|
||||||
)
|
: value > 70
|
||||||
|
? "bg-orange-400"
|
||||||
|
: "bg-green-500"
|
||||||
|
}
|
||||||
|
className={"h-[3px] rounded-sm"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +1,100 @@
|
|||||||
import { fetchService } from "@/lib/nezha-api"
|
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
||||||
import { NezhaServer, ServiceData } from "@/types/nezha-api"
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid"
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { fetchService } from "@/lib/nezha-api";
|
||||||
import { useTranslation } from "react-i18next"
|
import type { NezhaServer, ServiceData } from "@/types/nezha-api";
|
||||||
|
|
||||||
import { CycleTransferStatsCard } from "./CycleTransferStats"
|
import { CycleTransferStatsCard } from "./CycleTransferStats";
|
||||||
import ServiceTrackerClient from "./ServiceTrackerClient"
|
import { Loader } from "./loading/Loader";
|
||||||
import { Loader } from "./loading/Loader"
|
import ServiceTrackerClient from "./ServiceTrackerClient";
|
||||||
|
|
||||||
export function ServiceTracker({ serverList }: { serverList: NezhaServer[] }) {
|
export function ServiceTracker({ serverList }: { serverList: NezhaServer[] }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const { data: serviceData, isLoading } = useQuery({
|
const { data: serviceData, isLoading } = useQuery({
|
||||||
queryKey: ["service"],
|
queryKey: ["service"],
|
||||||
queryFn: () => fetchService(),
|
queryFn: () => fetchService(),
|
||||||
refetchOnMount: true,
|
refetchOnMount: true,
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
refetchInterval: 10000,
|
refetchInterval: 10000,
|
||||||
})
|
});
|
||||||
|
|
||||||
const processServiceData = (serviceData: ServiceData) => {
|
const processServiceData = (serviceData: ServiceData) => {
|
||||||
const days = serviceData.up.map((up, index) => {
|
const days = serviceData.up.map((up, index) => {
|
||||||
const totalChecks = up + serviceData.down[index]
|
const totalChecks = up + serviceData.down[index];
|
||||||
const dailyUptime = totalChecks > 0 ? (up / totalChecks) * 100 : 0
|
const dailyUptime = totalChecks > 0 ? (up / totalChecks) * 100 : 0;
|
||||||
return {
|
return {
|
||||||
completed: up > serviceData.down[index],
|
completed: up > serviceData.down[index],
|
||||||
date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000),
|
date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000),
|
||||||
uptime: dailyUptime,
|
uptime: dailyUptime,
|
||||||
delay: serviceData.delay[index] || 0,
|
delay: serviceData.delay[index] || 0,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|
||||||
const totalUp = serviceData.up.reduce((a, b) => a + b, 0)
|
const totalUp = serviceData.up.reduce((a, b) => a + b, 0);
|
||||||
const totalChecks = serviceData.up.reduce((a, b) => a + b, 0) + serviceData.down.reduce((a, b) => a + b, 0)
|
const totalChecks =
|
||||||
const uptime = (totalUp / totalChecks) * 100
|
serviceData.up.reduce((a, b) => a + b, 0) +
|
||||||
|
serviceData.down.reduce((a, b) => a + b, 0);
|
||||||
|
const uptime = (totalUp / totalChecks) * 100;
|
||||||
|
|
||||||
const avgDelay = serviceData.delay.length > 0 ? serviceData.delay.reduce((a, b) => a + b, 0) / serviceData.delay.length : 0
|
const avgDelay =
|
||||||
|
serviceData.delay.length > 0
|
||||||
|
? serviceData.delay.reduce((a, b) => a + b, 0) /
|
||||||
|
serviceData.delay.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
return { days, uptime, avgDelay }
|
return { days, uptime, avgDelay };
|
||||||
}
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 text-sm font-medium flex items-center gap-1">
|
<div className="mt-4 text-sm font-medium flex items-center gap-1">
|
||||||
<Loader visible={true} />
|
<Loader visible={true} />
|
||||||
{t("serviceTracker.loading")}
|
{t("serviceTracker.loading")}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!serviceData?.data?.services && !serviceData?.data?.cycle_transfer_stats) {
|
if (
|
||||||
return (
|
!serviceData?.data?.services &&
|
||||||
<div className="mt-4 text-sm font-medium flex items-center gap-1">
|
!serviceData?.data?.cycle_transfer_stats
|
||||||
<ExclamationTriangleIcon className="w-4 h-4" />
|
) {
|
||||||
{t("serviceTracker.noService")}
|
return (
|
||||||
</div>
|
<div className="mt-4 text-sm font-medium flex items-center gap-1">
|
||||||
)
|
<ExclamationTriangleIcon className="w-4 h-4" />
|
||||||
}
|
{t("serviceTracker.noService")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 w-full mx-auto ">
|
<div className="mt-4 w-full mx-auto ">
|
||||||
{serviceData.data.cycle_transfer_stats && (
|
{serviceData.data.cycle_transfer_stats && (
|
||||||
<div>
|
<div>
|
||||||
<CycleTransferStatsCard serverList={serverList} cycleStats={serviceData.data.cycle_transfer_stats} />
|
<CycleTransferStatsCard
|
||||||
</div>
|
serverList={serverList}
|
||||||
)}
|
cycleStats={serviceData.data.cycle_transfer_stats}
|
||||||
{serviceData.data.services && Object.keys(serviceData.data.services).length > 0 && (
|
/>
|
||||||
<section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4">
|
</div>
|
||||||
{Object.entries(serviceData.data.services).map(([name, data]) => {
|
)}
|
||||||
const { days, uptime, avgDelay } = processServiceData(data)
|
{serviceData.data.services &&
|
||||||
return <ServiceTrackerClient key={name} days={days} title={data.service_name} uptime={uptime} avgDelay={avgDelay} />
|
Object.keys(serviceData.data.services).length > 0 && (
|
||||||
})}
|
<section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4">
|
||||||
</section>
|
{Object.entries(serviceData.data.services).map(([name, data]) => {
|
||||||
)}
|
const { days, uptime, avgDelay } = processServiceData(data);
|
||||||
</div>
|
return (
|
||||||
)
|
<ServiceTrackerClient
|
||||||
|
key={name}
|
||||||
|
days={days}
|
||||||
|
title={data.service_name}
|
||||||
|
uptime={uptime}
|
||||||
|
avgDelay={avgDelay}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ServiceTracker
|
export default ServiceTracker;
|
||||||
|
|||||||
@@ -1,118 +1,166 @@
|
|||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
import type React from "react";
|
||||||
import { cn } from "@/lib/utils"
|
import { useTranslation } from "react-i18next";
|
||||||
import React from "react"
|
import {
|
||||||
import { useTranslation } from "react-i18next"
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { Separator } from "./ui/separator"
|
import { Separator } from "./ui/separator";
|
||||||
|
|
||||||
interface ServiceTrackerProps {
|
interface ServiceTrackerProps {
|
||||||
days: Array<{
|
days: Array<{
|
||||||
completed: boolean
|
completed: boolean;
|
||||||
date?: Date
|
date?: Date;
|
||||||
uptime: number
|
uptime: number;
|
||||||
delay: number
|
delay: number;
|
||||||
}>
|
}>;
|
||||||
className?: string
|
className?: string;
|
||||||
title?: string
|
title?: string;
|
||||||
uptime?: number
|
uptime?: number;
|
||||||
avgDelay?: number
|
avgDelay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, className, title, uptime = 100, avgDelay = 0 }) => {
|
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
|
||||||
const { t } = useTranslation()
|
days,
|
||||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
className,
|
||||||
|
title,
|
||||||
|
uptime = 100,
|
||||||
|
avgDelay = 0,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const customBackgroundImage =
|
||||||
|
(window.CustomBackgroundImage as string) !== ""
|
||||||
|
? window.CustomBackgroundImage
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const getUptimeColor = (uptime: number) => {
|
const getUptimeColor = (uptime: number) => {
|
||||||
if (uptime >= 99) return "text-emerald-500"
|
if (uptime >= 99) return "text-emerald-500";
|
||||||
if (uptime >= 95) return "text-amber-500"
|
if (uptime >= 95) return "text-amber-500";
|
||||||
return "text-rose-500"
|
return "text-rose-500";
|
||||||
}
|
};
|
||||||
|
|
||||||
const getDelayColor = (delay: number) => {
|
const getDelayColor = (delay: number) => {
|
||||||
if (delay < 100) return "text-emerald-500"
|
if (delay < 100) return "text-emerald-500";
|
||||||
if (delay < 300) return "text-amber-500"
|
if (delay < 300) return "text-amber-500";
|
||||||
return "text-rose-500"
|
return "text-rose-500";
|
||||||
}
|
};
|
||||||
|
|
||||||
const getStatusColor = (uptime: number) => {
|
const getStatusColor = (uptime: number) => {
|
||||||
if (uptime >= 99) return "bg-emerald-500"
|
if (uptime >= 99) return "bg-emerald-500";
|
||||||
if (uptime >= 95) return "bg-amber-500"
|
if (uptime >= 95) return "bg-amber-500";
|
||||||
return "bg-rose-500"
|
return "bg-rose-500";
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full space-y-3 bg-white px-4 py-4 rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
|
"w-full space-y-3 bg-white px-4 py-4 rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
|
||||||
className,
|
className,
|
||||||
{
|
{
|
||||||
"bg-card/70": customBackgroundImage,
|
"bg-card/70": customBackgroundImage,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className={cn("w-2.5 h-2.5 rounded-full transition-colors", getStatusColor(uptime))} />
|
<div
|
||||||
<span className="font-medium text-sm">{title}</span>
|
className={cn(
|
||||||
</div>
|
"w-2.5 h-2.5 rounded-full transition-colors",
|
||||||
<div className="flex items-center gap-3">
|
getStatusColor(uptime),
|
||||||
<span className={cn("font-medium text-sm transition-colors", getDelayColor(avgDelay))}>{avgDelay.toFixed(0)}ms</span>
|
)}
|
||||||
<Separator className="h-4" orientation="vertical" />
|
/>
|
||||||
<span className={cn("font-medium text-sm transition-colors", getUptimeColor(uptime))}>
|
<span className="font-medium text-sm">{title}</span>
|
||||||
{uptime.toFixed(1)}% {t("serviceTracker.uptime")}
|
</div>
|
||||||
</span>
|
<div className="flex items-center gap-3">
|
||||||
</div>
|
<span
|
||||||
</div>
|
className={cn(
|
||||||
|
"font-medium text-sm transition-colors",
|
||||||
|
getDelayColor(avgDelay),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{avgDelay.toFixed(0)}ms
|
||||||
|
</span>
|
||||||
|
<Separator className="h-4" orientation="vertical" />
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"font-medium text-sm transition-colors",
|
||||||
|
getUptimeColor(uptime),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{uptime.toFixed(1)}% {t("serviceTracker.uptime")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-[3px] bg-muted/30 p-1 rounded-lg">
|
<div className="flex gap-[3px] bg-muted/30 p-1 rounded-lg">
|
||||||
{days.map((day, index) => (
|
{days.map((day, index) => (
|
||||||
<TooltipProvider delayDuration={50} key={index}>
|
<TooltipProvider delayDuration={50} key={index}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex-1 h-7 rounded-[4px] transition-all duration-200 cursor-help",
|
"relative flex-1 h-7 rounded-[4px] transition-all duration-200 cursor-help",
|
||||||
"before:absolute before:inset-0 before:rounded-[4px] before:opacity-0 hover:before:opacity-100 before:bg-white/10 before:transition-opacity",
|
"before:absolute before:inset-0 before:rounded-[4px] before:opacity-0 hover:before:opacity-100 before:bg-white/10 before:transition-opacity",
|
||||||
"after:absolute after:inset-0 after:rounded-[4px] after:shadow-[inset_0_1px_theme(colors.white/10%)]",
|
"after:absolute after:inset-0 after:rounded-[4px] after:shadow-[inset_0_1px_--theme(--color-white/10%)]",
|
||||||
day.completed
|
day.completed
|
||||||
? "bg-gradient-to-b from-green-500/90 to-green-600 shadow-[0_1px_2px_theme(colors.green.600/30%)]"
|
? "bg-linear-to-b from-green-500/90 to-green-600 shadow-[0_1px_2px_--theme(--color-green-600/30%)]"
|
||||||
: "bg-gradient-to-b from-red-500/80 to-red-600/90 shadow-[0_1px_2px_theme(colors.red.600/30%)]",
|
: "bg-linear-to-b from-red-500/80 to-red-600/90 shadow-[0_1px_2px_--theme(--color-red-600/30%)]",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="p-0 overflow-hidden">
|
<TooltipContent className="p-0 overflow-hidden">
|
||||||
<div className="px-3 py-2 bg-popover">
|
<div className="px-3 py-2 bg-popover">
|
||||||
<p className="font-medium text-sm mb-2">{day.date?.toLocaleDateString()}</p>
|
<p className="font-medium text-sm mb-2">
|
||||||
<div className="space-y-1.5">
|
{day.date?.toLocaleDateString()}
|
||||||
<div className="flex items-center justify-between gap-3">
|
</p>
|
||||||
<span className="text-xs text-muted-foreground">{t("serviceTracker.uptime")}:</span>
|
<div className="space-y-1.5">
|
||||||
<span className={cn("text-xs font-medium", day.uptime > 95 ? "text-green-500" : "text-red-500")}>{day.uptime.toFixed(1)}%</span>
|
<div className="flex items-center justify-between gap-3">
|
||||||
</div>
|
<span className="text-xs text-muted-foreground">
|
||||||
<div className="flex items-center justify-between gap-3">
|
{t("serviceTracker.uptime")}:
|
||||||
<span className="text-xs text-muted-foreground">{t("serviceTracker.delay")}:</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-xs font-medium",
|
"text-xs font-medium",
|
||||||
day.delay < 100 ? "text-green-500" : day.delay < 300 ? "text-yellow-500" : "text-red-500",
|
day.uptime > 95 ? "text-green-500" : "text-red-500",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{day.delay.toFixed(0)}ms
|
{day.uptime.toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-center justify-between gap-3">
|
||||||
</div>
|
<span className="text-xs text-muted-foreground">
|
||||||
</TooltipContent>
|
{t("serviceTracker.delay")}:
|
||||||
</Tooltip>
|
</span>
|
||||||
</TooltipProvider>
|
<span
|
||||||
))}
|
className={cn(
|
||||||
</div>
|
"text-xs font-medium",
|
||||||
|
day.delay < 100
|
||||||
|
? "text-green-500"
|
||||||
|
: day.delay < 300
|
||||||
|
? "text-yellow-500"
|
||||||
|
: "text-red-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{day.delay.toFixed(0)}ms
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between text-xs text-stone-500 dark:text-stone-400">
|
<div className="flex justify-between text-xs text-stone-500 dark:text-stone-400">
|
||||||
<span>30 {t("serviceTracker.daysAgo")}</span>
|
<span>30 {t("serviceTracker.daysAgo")}</span>
|
||||||
<span>{t("serviceTracker.today")}</span>
|
<span>{t("serviceTracker.today")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ServiceTrackerClient
|
export default ServiceTrackerClient;
|
||||||
|
|||||||
@@ -1,42 +1,58 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { m } from "framer-motion";
|
||||||
import { m } from "framer-motion"
|
import { useTranslation } from "react-i18next";
|
||||||
import { useTranslation } from "react-i18next"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function TabSwitch({ tabs, currentTab, setCurrentTab }: { tabs: string[]; currentTab: string; setCurrentTab: (tab: string) => void }) {
|
export default function TabSwitch({
|
||||||
const { t } = useTranslation()
|
tabs,
|
||||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
currentTab,
|
||||||
return (
|
setCurrentTab,
|
||||||
<div className="z-50 flex flex-col items-start rounded-[50px] server-info-tab">
|
}: {
|
||||||
<div
|
tabs: string[];
|
||||||
className={cn("flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800", {
|
currentTab: string;
|
||||||
"bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage,
|
setCurrentTab: (tab: string) => void;
|
||||||
})}
|
}) {
|
||||||
>
|
const { t } = useTranslation();
|
||||||
{tabs.map((tab: string) => (
|
const customBackgroundImage =
|
||||||
<div
|
(window.CustomBackgroundImage as string) !== ""
|
||||||
key={tab}
|
? window.CustomBackgroundImage
|
||||||
onClick={() => setCurrentTab(tab)}
|
: undefined;
|
||||||
className={cn(
|
return (
|
||||||
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500",
|
<div className="z-50 flex flex-col items-start rounded-[50px] server-info-tab">
|
||||||
currentTab === tab ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500",
|
<div
|
||||||
)}
|
className={cn(
|
||||||
>
|
"flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800",
|
||||||
{currentTab === tab && (
|
{
|
||||||
<m.div
|
"bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage,
|
||||||
layoutId="tab-switch-active"
|
},
|
||||||
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5"
|
)}
|
||||||
style={{
|
>
|
||||||
originY: "0px",
|
{tabs.map((tab: string) => (
|
||||||
borderRadius: 46,
|
<div
|
||||||
}}
|
key={tab}
|
||||||
/>
|
onClick={() => setCurrentTab(tab)}
|
||||||
)}
|
className={cn(
|
||||||
<div className="relative z-20 flex items-center gap-1">
|
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-semibold transition-all duration-500",
|
||||||
<p className="whitespace-nowrap">{t("tabSwitch." + tab)}</p>
|
currentTab === tab
|
||||||
</div>
|
? "text-black dark:text-white"
|
||||||
</div>
|
: "text-stone-400 dark:text-stone-500",
|
||||||
))}
|
)}
|
||||||
</div>
|
>
|
||||||
</div>
|
{currentTab === tab && (
|
||||||
)
|
<m.div
|
||||||
|
layoutId="tab-switch-active"
|
||||||
|
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5"
|
||||||
|
style={{
|
||||||
|
originY: "0px",
|
||||||
|
borderRadius: 46,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="relative z-20 flex items-center gap-1">
|
||||||
|
<p className="whitespace-nowrap">{t(`tabSwitch.${tab}`)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,41 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useTheme } from "@/hooks/use-theme"
|
import { useEffect } from "react";
|
||||||
import { useEffect } from "react"
|
import { useTheme } from "@/hooks/use-theme";
|
||||||
|
|
||||||
export function ThemeColorManager() {
|
export function ThemeColorManager() {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateThemeColor = () => {
|
const updateThemeColor = () => {
|
||||||
const currentTheme = theme
|
const currentTheme = theme;
|
||||||
const meta = document.querySelector('meta[name="theme-color"]')
|
const meta = document.querySelector('meta[name="theme-color"]');
|
||||||
|
|
||||||
if (!meta) {
|
if (!meta) {
|
||||||
const newMeta = document.createElement("meta")
|
const newMeta = document.createElement("meta");
|
||||||
newMeta.name = "theme-color"
|
newMeta.name = "theme-color";
|
||||||
document.head.appendChild(newMeta)
|
document.head.appendChild(newMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeColor =
|
const themeColor =
|
||||||
currentTheme === "dark"
|
currentTheme === "dark"
|
||||||
? "hsl(30 15% 8%)" // 深色模式背景色
|
? "hsl(30 15% 8%)" // 深色模式背景色
|
||||||
: "hsl(0 0% 98%)" // 浅色模式背景色
|
: "hsl(0 0% 98%)"; // 浅色模式背景色
|
||||||
|
|
||||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
document
|
||||||
}
|
.querySelector('meta[name="theme-color"]')
|
||||||
|
?.setAttribute("content", themeColor);
|
||||||
|
};
|
||||||
|
|
||||||
// Update on mount and theme change
|
// Update on mount and theme change
|
||||||
updateThemeColor()
|
updateThemeColor();
|
||||||
|
|
||||||
// Listen for system theme changes
|
// Listen for system theme changes
|
||||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
mediaQuery.addEventListener("change", updateThemeColor)
|
mediaQuery.addEventListener("change", updateThemeColor);
|
||||||
|
|
||||||
return () => mediaQuery.removeEventListener("change", updateThemeColor)
|
return () => mediaQuery.removeEventListener("change", updateThemeColor);
|
||||||
}, [theme])
|
}, [theme]);
|
||||||
|
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,81 @@
|
|||||||
import { ReactNode, createContext, useEffect, useState } from "react"
|
import { createContext, type ReactNode, useEffect, useState } from "react";
|
||||||
|
|
||||||
export type Theme = "dark" | "light" | "system"
|
export type Theme = "dark" | "light" | "system";
|
||||||
|
|
||||||
type ThemeProviderProps = {
|
type ThemeProviderProps = {
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
defaultTheme?: Theme
|
defaultTheme?: Theme;
|
||||||
storageKey?: string
|
storageKey?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
type ThemeProviderState = {
|
type ThemeProviderState = {
|
||||||
theme: Theme
|
theme: Theme;
|
||||||
setTheme: (theme: Theme) => void
|
setTheme: (theme: Theme) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
const initialState: ThemeProviderState = {
|
const initialState: ThemeProviderState = {
|
||||||
theme: "system",
|
theme: "system",
|
||||||
setTheme: () => null,
|
setTheme: () => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
storageKey = "vite-ui-theme",
|
||||||
|
}: ThemeProviderProps) {
|
||||||
|
const [theme, setTheme] = useState<Theme>(
|
||||||
|
() => (localStorage.getItem(storageKey) as Theme) || "system",
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement;
|
||||||
|
|
||||||
|
root.classList.add("disable-transitions");
|
||||||
|
root.classList.remove("light", "dark");
|
||||||
|
|
||||||
|
if (theme === "system") {
|
||||||
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
.matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
|
||||||
|
root.classList.add(systemTheme);
|
||||||
|
const themeColor =
|
||||||
|
systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
|
||||||
|
document
|
||||||
|
.querySelector('meta[name="theme-color"]')
|
||||||
|
?.setAttribute("content", themeColor);
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
root.classList.remove("disable-transitions");
|
||||||
|
}, 0);
|
||||||
|
return () => window.clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
root.classList.add(theme);
|
||||||
|
const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
|
||||||
|
document
|
||||||
|
.querySelector('meta[name="theme-color"]')
|
||||||
|
?.setAttribute("content", themeColor);
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
root.classList.remove("disable-transitions");
|
||||||
|
}, 0);
|
||||||
|
return () => window.clearTimeout(timeoutId);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
theme,
|
||||||
|
setTheme: (theme: Theme) => {
|
||||||
|
localStorage.setItem(storageKey, theme);
|
||||||
|
setTheme(theme);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProviderContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</ThemeProviderContext.Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
export { ThemeProviderContext };
|
||||||
|
|
||||||
export function ThemeProvider({ children, storageKey = "vite-ui-theme" }: ThemeProviderProps) {
|
|
||||||
const [theme, setTheme] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || "system")
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const root = window.document.documentElement
|
|
||||||
|
|
||||||
root.classList.remove("light", "dark")
|
|
||||||
|
|
||||||
if (theme === "system") {
|
|
||||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
||||||
|
|
||||||
root.classList.add(systemTheme)
|
|
||||||
const themeColor = systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
|
|
||||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
root.classList.add(theme)
|
|
||||||
const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
|
|
||||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
|
||||||
}, [theme])
|
|
||||||
|
|
||||||
const value = {
|
|
||||||
theme,
|
|
||||||
setTheme: (theme: Theme) => {
|
|
||||||
localStorage.setItem(storageKey, theme)
|
|
||||||
setTheme(theme)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ThemeProviderContext.Provider value={value}>{children}</ThemeProviderContext.Provider>
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ThemeProviderContext }
|
|
||||||
|
|||||||
@@ -1,53 +1,76 @@
|
|||||||
import { Theme } from "@/components/ThemeProvider"
|
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||||
import { Button } from "@/components/ui/button"
|
import { Moon, Sun } from "lucide-react";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
import { useTranslation } from "react-i18next";
|
||||||
import { cn } from "@/lib/utils"
|
import type { Theme } from "@/components/ThemeProvider";
|
||||||
import { CheckCircleIcon } from "@heroicons/react/20/solid"
|
import { Button } from "@/components/ui/button";
|
||||||
import { Moon, Sun } from "lucide-react"
|
import {
|
||||||
import { useTranslation } from "react-i18next"
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { useTheme } from "../hooks/use-theme"
|
import { useTheme } from "../hooks/use-theme";
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation();
|
||||||
const { setTheme, theme } = useTheme()
|
const { setTheme, theme } = useTheme();
|
||||||
|
|
||||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
const customBackgroundImage =
|
||||||
|
(window.CustomBackgroundImage as string) !== ""
|
||||||
|
? window.CustomBackgroundImage
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const handleSelect = (e: Event, newTheme: Theme) => {
|
const handleSelect = (e: Event, newTheme: Theme) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
setTheme(newTheme)
|
setTheme(newTheme);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
||||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
||||||
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "light" })} onSelect={(e) => handleSelect(e, "light")}>
|
<DropdownMenuItem
|
||||||
{t("theme.light")}
|
className={cn("rounded-b-[5px] text-xs", {
|
||||||
{theme === "light" && <CheckCircleIcon className="size-4" />}
|
"gap-3 bg-muted font-semibold": theme === "light",
|
||||||
</DropdownMenuItem>
|
})}
|
||||||
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "dark" })} onSelect={(e) => handleSelect(e, "dark")}>
|
onSelect={(e) => handleSelect(e, "light")}
|
||||||
{t("theme.dark")}
|
>
|
||||||
{theme === "dark" && <CheckCircleIcon className="size-4" />}
|
{t("theme.light")}
|
||||||
</DropdownMenuItem>
|
{theme === "light" && <CheckCircleIcon className="size-4" />}
|
||||||
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "system" })} onSelect={(e) => handleSelect(e, "system")}>
|
</DropdownMenuItem>
|
||||||
{t("theme.system")}
|
<DropdownMenuItem
|
||||||
{theme === "system" && <CheckCircleIcon className="size-4" />}
|
className={cn("rounded-[5px] text-xs", {
|
||||||
</DropdownMenuItem>
|
"gap-3 bg-muted font-semibold": theme === "dark",
|
||||||
</DropdownMenuContent>
|
})}
|
||||||
</DropdownMenu>
|
onSelect={(e) => handleSelect(e, "dark")}
|
||||||
)
|
>
|
||||||
|
{t("theme.dark")}
|
||||||
|
{theme === "dark" && <CheckCircleIcon className="size-4" />}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={cn("rounded-t-[5px] text-xs", {
|
||||||
|
"gap-3 bg-muted font-semibold": theme === "system",
|
||||||
|
})}
|
||||||
|
onSelect={(e) => handleSelect(e, "system")}
|
||||||
|
>
|
||||||
|
{t("theme.system")}
|
||||||
|
{theme === "system" && <CheckCircleIcon className="size-4" />}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-62
@@ -1,68 +1,106 @@
|
|||||||
import { PublicNoteData, cn, getDaysBetweenDatesWithAutoRenewal } from "@/lib/utils"
|
import { useTranslation } from "react-i18next";
|
||||||
import { useTranslation } from "react-i18next"
|
import {
|
||||||
|
cn,
|
||||||
|
getDaysBetweenDatesWithAutoRenewal,
|
||||||
|
type PublicNoteData,
|
||||||
|
} from "@/lib/utils";
|
||||||
|
|
||||||
import RemainPercentBar from "./RemainPercentBar"
|
import RemainPercentBar from "./RemainPercentBar";
|
||||||
|
|
||||||
export default function BillingInfo({ parsedData }: { parsedData: PublicNoteData }) {
|
export default function BillingInfo({
|
||||||
const { t } = useTranslation()
|
parsedData,
|
||||||
if (!parsedData || !parsedData.billingDataMod) {
|
}: {
|
||||||
return null
|
parsedData: PublicNoteData;
|
||||||
}
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
if (!parsedData || !parsedData.billingDataMod) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let isNeverExpire = false
|
let isNeverExpire = false;
|
||||||
let daysLeftObject = {
|
let daysLeftObject = {
|
||||||
days: 0,
|
days: 0,
|
||||||
cycleLabel: "",
|
cycleLabel: "",
|
||||||
remainingPercentage: 0,
|
remainingPercentage: 0,
|
||||||
}
|
};
|
||||||
|
const hasBillingDates =
|
||||||
|
Boolean(parsedData.billingDataMod.startDate) ||
|
||||||
|
Boolean(parsedData.billingDataMod.endDate);
|
||||||
|
|
||||||
if (parsedData?.billingDataMod?.endDate) {
|
if (parsedData?.billingDataMod?.endDate) {
|
||||||
if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) {
|
if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) {
|
||||||
isNeverExpire = true
|
isNeverExpire = true;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
daysLeftObject = getDaysBetweenDatesWithAutoRenewal(parsedData.billingDataMod)
|
daysLeftObject = getDaysBetweenDatesWithAutoRenewal(
|
||||||
} catch (error) {
|
parsedData.billingDataMod,
|
||||||
console.error(error)
|
);
|
||||||
return (
|
} catch (error) {
|
||||||
<div className={cn("text-[10px] text-muted-foreground text-red-600")}>
|
console.error(error);
|
||||||
{t("billingInfo.remaining")}: {t("billingInfo.error")}
|
return (
|
||||||
</div>
|
<div className={cn("text-[10px] text-muted-foreground text-red-600")}>
|
||||||
)
|
{t("billingInfo.remaining")}: {t("billingInfo.error")}
|
||||||
}
|
</div>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return daysLeftObject.days >= 0 ? (
|
return daysLeftObject.days >= 0 ? (
|
||||||
<>
|
<>
|
||||||
{parsedData.billingDataMod.amount && parsedData.billingDataMod.amount !== "0" && parsedData.billingDataMod.amount !== "-1" ? (
|
{parsedData.billingDataMod.amount &&
|
||||||
<p className={cn("text-[10px] text-muted-foreground ")}>
|
parsedData.billingDataMod.amount !== "0" &&
|
||||||
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
|
parsedData.billingDataMod.amount !== "-1" ? (
|
||||||
</p>
|
<p className={cn("text-[10px] text-muted-foreground ")}>
|
||||||
) : parsedData.billingDataMod.amount === "0" ? (
|
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/
|
||||||
<p className={cn("text-[10px] text-green-600 ")}>{t("billingInfo.free")}</p>
|
{parsedData.billingDataMod.cycle}
|
||||||
) : parsedData.billingDataMod.amount === "-1" ? (
|
</p>
|
||||||
<p className={cn("text-[10px] text-pink-600 ")}>{t("billingInfo.usage-baseed")}</p>
|
) : parsedData.billingDataMod.amount === "0" ? (
|
||||||
) : null}
|
<p className={cn("text-[10px] text-green-600 ")}>
|
||||||
<div className={cn("text-[10px] text-muted-foreground")}>
|
{t("billingInfo.free")}
|
||||||
{t("billingInfo.remaining")}: {isNeverExpire ? t("billingInfo.indefinite") : daysLeftObject.days + " " + t("billingInfo.days")}
|
</p>
|
||||||
</div>
|
) : parsedData.billingDataMod.amount === "-1" ? (
|
||||||
{!isNeverExpire && <RemainPercentBar className="mt-0.5" value={daysLeftObject.remainingPercentage * 100} />}
|
<p className={cn("text-[10px] text-pink-600 ")}>
|
||||||
</>
|
{t("billingInfo.usage-baseed")}
|
||||||
) : (
|
</p>
|
||||||
<>
|
) : null}
|
||||||
{parsedData.billingDataMod.amount && parsedData.billingDataMod.amount !== "0" && parsedData.billingDataMod.amount !== "-1" ? (
|
{hasBillingDates && (
|
||||||
<p className={cn("text-[10px] text-muted-foreground ")}>
|
<div className={cn("text-[10px] text-muted-foreground")}>
|
||||||
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
|
{t("billingInfo.remaining")}:{" "}
|
||||||
</p>
|
{isNeverExpire
|
||||||
) : parsedData.billingDataMod.amount === "0" ? (
|
? t("billingInfo.indefinite")
|
||||||
<p className={cn("text-[10px] text-green-600 ")}>{t("billingInfo.free")}</p>
|
: `${daysLeftObject.days} ${t("billingInfo.days")}`}
|
||||||
) : parsedData.billingDataMod.amount === "-1" ? (
|
</div>
|
||||||
<p className={cn("text-[10px] text-pink-600 ")}>{t("billingInfo.usage-baseed")}</p>
|
)}
|
||||||
) : null}
|
{hasBillingDates && !isNeverExpire && (
|
||||||
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
|
<RemainPercentBar
|
||||||
{t("billingInfo.expired")}: {daysLeftObject.days * -1} {t("billingInfo.days")}
|
className="mt-0.5"
|
||||||
</p>
|
value={daysLeftObject.remainingPercentage * 100}
|
||||||
</>
|
/>
|
||||||
)
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{parsedData.billingDataMod.amount &&
|
||||||
|
parsedData.billingDataMod.amount !== "0" &&
|
||||||
|
parsedData.billingDataMod.amount !== "-1" ? (
|
||||||
|
<p className={cn("text-[10px] text-muted-foreground ")}>
|
||||||
|
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/
|
||||||
|
{parsedData.billingDataMod.cycle}
|
||||||
|
</p>
|
||||||
|
) : parsedData.billingDataMod.amount === "0" ? (
|
||||||
|
<p className={cn("text-[10px] text-green-600 ")}>
|
||||||
|
{t("billingInfo.free")}
|
||||||
|
</p>
|
||||||
|
) : parsedData.billingDataMod.amount === "-1" ? (
|
||||||
|
<p className={cn("text-[10px] text-pink-600 ")}>
|
||||||
|
{t("billingInfo.usage-baseed")}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
|
||||||
|
{t("billingInfo.expired")}: {daysLeftObject.days * -1}{" "}
|
||||||
|
{t("billingInfo.days")}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export default function ChartSkeleton({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}: {
|
||||||
|
width?: number | string;
|
||||||
|
height?: number | string;
|
||||||
|
}) {
|
||||||
|
const resolvedWidth = typeof width === "number" ? `${width}px` : width;
|
||||||
|
const resolvedHeight = typeof height === "number" ? `${height}px` : height;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative h-full w-full overflow-hidden"
|
||||||
|
style={{
|
||||||
|
width: resolvedWidth || "100%",
|
||||||
|
height: resolvedHeight || "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="size-4 rounded-full border-2 border-muted-foreground/20 border-t-muted-foreground/70 animate-spin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,32 +1,32 @@
|
|||||||
const bars = Array(8).fill(0)
|
const bars = Array(8).fill(0);
|
||||||
|
|
||||||
export const Loader = ({ visible }: { visible: boolean }) => {
|
export const Loader = ({ visible }: { visible: boolean }) => {
|
||||||
return (
|
return (
|
||||||
<div className="hamster-loading-wrapper" data-visible={visible}>
|
<div className="hamster-loading-wrapper" data-visible={visible}>
|
||||||
<div className="hamster-spinner">
|
<div className="hamster-spinner">
|
||||||
{bars.map((_, i) => (
|
{bars.map((_, i) => (
|
||||||
<div className="hamster-loading-bar" key={`hamster-bar-${i}`} />
|
<div className="hamster-loading-bar" key={`hamster-bar-${i}`} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const LoadingSpinner = () => {
|
export const LoadingSpinner = () => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
className={"size-4 animate-spin"}
|
className={"size-4 animate-spin"}
|
||||||
>
|
>
|
||||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useNavigate } from "react-router-dom"
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
import { BackIcon } from "../Icon"
|
import { BackIcon } from "../Icon";
|
||||||
|
|
||||||
export function ServerDetailChartLoading() {
|
export function ServerDetailChartLoading() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
|
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
|
||||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServerDetailLoading() {
|
export function ServerDetailLoading() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-5xl px-0">
|
<div className="mx-auto w-full max-w-5xl px-0">
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("/")
|
navigate("/");
|
||||||
}}
|
}}
|
||||||
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
|
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
|
||||||
>
|
>
|
||||||
<BackIcon />
|
<BackIcon />
|
||||||
<Skeleton className="h-[20px] w-24 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
<Skeleton className="h-[20px] w-24 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
</div>
|
</div>
|
||||||
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { domMax as default } from "framer-motion"
|
export { domMax as default } from "framer-motion";
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { LazyMotion } from "framer-motion"
|
import { LazyMotion } from "framer-motion";
|
||||||
|
|
||||||
const loadFeatures = () => import("./framer-lazy-feature").then((res) => res.default)
|
const loadFeatures = () =>
|
||||||
|
import("./framer-lazy-feature").then((res) => res.default);
|
||||||
|
|
||||||
export const MotionProvider = ({ children }: { children: React.ReactNode }) => {
|
export const MotionProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<LazyMotion features={loadFeatures} strict key="framer">
|
<LazyMotion features={loadFeatures} strict key="framer">
|
||||||
{children}
|
{children}
|
||||||
</LazyMotion>
|
</LazyMotion>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,49 +1,55 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
import { ChevronDown } from "lucide-react";
|
||||||
import { ChevronDown } from "lucide-react"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Accordion = AccordionPrimitive.Root
|
const Accordion = AccordionPrimitive.Root;
|
||||||
|
|
||||||
const AccordionItem = React.forwardRef<
|
const AccordionItem = React.forwardRef<
|
||||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
>(({ className, ...props }, ref) => <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />)
|
>(({ className, ...props }, ref) => (
|
||||||
AccordionItem.displayName = "AccordionItem"
|
<AccordionPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn("border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AccordionItem.displayName = "AccordionItem";
|
||||||
|
|
||||||
const AccordionTrigger = React.forwardRef<
|
const AccordionTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<AccordionPrimitive.Header className="flex">
|
<AccordionPrimitive.Header className="flex">
|
||||||
<AccordionPrimitive.Trigger
|
<AccordionPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-1 items-center justify-start py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
"flex flex-1 items-center justify-start py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||||
</AccordionPrimitive.Trigger>
|
</AccordionPrimitive.Trigger>
|
||||||
</AccordionPrimitive.Header>
|
</AccordionPrimitive.Header>
|
||||||
))
|
));
|
||||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
const AccordionContent = React.forwardRef<
|
const AccordionContent = React.forwardRef<
|
||||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<AccordionPrimitive.Content
|
<AccordionPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||||
</AccordionPrimitive.Content>
|
</AccordionPrimitive.Content>
|
||||||
))
|
));
|
||||||
|
|
||||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||||
|
|||||||
@@ -1,89 +1,107 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
max: number
|
max: number;
|
||||||
value: number
|
value: number;
|
||||||
min: number
|
min: number;
|
||||||
className?: string
|
className?: string;
|
||||||
primaryColor?: string
|
primaryColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AnimatedCircularProgressBar({ max = 100, min = 0, value = 0, primaryColor, className }: Props) {
|
export default function AnimatedCircularProgressBar({
|
||||||
const circumference = 2 * Math.PI * 45
|
max = 100,
|
||||||
const percentPx = circumference / 100
|
min = 0,
|
||||||
const currentPercent = ((value - min) / (max - min)) * 100
|
value = 0,
|
||||||
|
primaryColor,
|
||||||
|
className,
|
||||||
|
}: Props) {
|
||||||
|
const circumference = 2 * Math.PI * 45;
|
||||||
|
const percentPx = circumference / 100;
|
||||||
|
const currentPercent = ((value - min) / (max - min)) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("relative size-40 text-2xl font-semibold", className)}
|
className={cn("relative size-40 text-2xl font-semibold", className)}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--circle-size": "100px",
|
"--circle-size": "100px",
|
||||||
"--circumference": circumference,
|
"--circumference": circumference,
|
||||||
"--percent-to-px": `${percentPx}px`,
|
"--percent-to-px": `${percentPx}px`,
|
||||||
"--gap-percent": "5",
|
"--gap-percent": "5",
|
||||||
"--offset-factor": "0",
|
"--offset-factor": "0",
|
||||||
"--transition-length": "1s",
|
"--transition-length": "1s",
|
||||||
"--transition-step": "200ms",
|
"--transition-step": "200ms",
|
||||||
"--delay": "0s",
|
"--delay": "0s",
|
||||||
"--percent-to-deg": "3.6deg",
|
"--percent-to-deg": "3.6deg",
|
||||||
transform: "translateZ(0)",
|
transform: "translateZ(0)",
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg fill="none" className="size-full" strokeWidth="2" viewBox="0 0 100 100">
|
<svg
|
||||||
{currentPercent <= 90 && currentPercent >= 0 && (
|
fill="none"
|
||||||
<circle
|
className="size-full"
|
||||||
cx="50"
|
strokeWidth="2"
|
||||||
cy="50"
|
viewBox="0 0 100 100"
|
||||||
r="45"
|
>
|
||||||
strokeWidth="10"
|
{currentPercent <= 90 && currentPercent >= 0 && (
|
||||||
strokeDashoffset="0"
|
<circle
|
||||||
strokeLinecap="round"
|
cx="50"
|
||||||
strokeLinejoin="round"
|
cy="50"
|
||||||
className="opacity-100 stroke-muted"
|
r="45"
|
||||||
style={
|
strokeWidth="10"
|
||||||
{
|
strokeDashoffset="0"
|
||||||
"--stroke-percent": 90 - currentPercent,
|
strokeLinecap="round"
|
||||||
"--offset-factor-secondary": "calc(1 - var(--offset-factor))",
|
strokeLinejoin="round"
|
||||||
strokeDasharray: "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
|
className="opacity-100 stroke-muted"
|
||||||
transform: "rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
|
style={
|
||||||
transition: "all var(--transition-length) ease var(--delay)",
|
{
|
||||||
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
"--stroke-percent": 90 - currentPercent,
|
||||||
} as React.CSSProperties
|
"--offset-factor-secondary": "calc(1 - var(--offset-factor))",
|
||||||
}
|
strokeDasharray:
|
||||||
/>
|
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
|
||||||
)}
|
transform:
|
||||||
<circle
|
"rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
|
||||||
cx="50"
|
transition: "all var(--transition-length) ease var(--delay)",
|
||||||
cy="50"
|
transformOrigin:
|
||||||
r="45"
|
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
||||||
strokeWidth="10"
|
} as React.CSSProperties
|
||||||
strokeDashoffset="0"
|
}
|
||||||
strokeLinecap="round"
|
/>
|
||||||
strokeLinejoin="round"
|
)}
|
||||||
className={cn("opacity-100 stroke-current", {
|
<circle
|
||||||
"stroke-[var(--stroke-primary-color)]": primaryColor,
|
cx="50"
|
||||||
})}
|
cy="50"
|
||||||
style={
|
r="45"
|
||||||
{
|
strokeWidth="10"
|
||||||
"--stroke-primary-color": primaryColor,
|
strokeDashoffset="0"
|
||||||
"--stroke-percent": currentPercent,
|
strokeLinecap="round"
|
||||||
strokeDasharray: "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
|
strokeLinejoin="round"
|
||||||
transition: "var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)",
|
className={cn("opacity-100 stroke-current", {
|
||||||
transitionProperty: "stroke-dasharray,transform",
|
"stroke-(--stroke-primary-color)": primaryColor,
|
||||||
transform: "rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))",
|
})}
|
||||||
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
style={
|
||||||
} as React.CSSProperties
|
{
|
||||||
}
|
"--stroke-primary-color": primaryColor,
|
||||||
/>
|
"--stroke-percent": currentPercent,
|
||||||
</svg>
|
strokeDasharray:
|
||||||
<span
|
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
|
||||||
data-current-value={currentPercent}
|
transition:
|
||||||
className="duration-[var(--transition-length)] delay-[var(--delay)] absolute inset-0 m-auto size-fit ease-linear animate-in fade-in"
|
"var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)",
|
||||||
>
|
transitionProperty: "stroke-dasharray,transform",
|
||||||
{currentPercent}
|
transform:
|
||||||
</span>
|
"rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))",
|
||||||
</div>
|
transformOrigin:
|
||||||
)
|
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span
|
||||||
|
data-current-value={currentPercent}
|
||||||
|
className="duration-[var(--transition-length)] delay-[var(--delay)] absolute inset-0 m-auto size-fit ease-linear animate-in fade-in"
|
||||||
|
>
|
||||||
|
{currentPercent}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-21
@@ -1,28 +1,35 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { type VariantProps, cva } from "class-variance-authority"
|
import type * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
default:
|
||||||
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
secondary:
|
||||||
outline: "text-foreground",
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
},
|
destructive:
|
||||||
},
|
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
defaultVariants: {
|
outline: "text-foreground",
|
||||||
variant: "default",
|
},
|
||||||
},
|
},
|
||||||
},
|
defaultVariants: {
|
||||||
)
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
export { Badge, badgeVariants };
|
||||||
|
|||||||
@@ -1,42 +1,55 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { type VariantProps, cva } from "class-variance-authority"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
destructive:
|
||||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
outline:
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
secondary:
|
||||||
},
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
size: {
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
default: "h-10 px-4 py-2",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
sm: "h-9 rounded-md px-3",
|
},
|
||||||
lg: "h-11 rounded-md px-8",
|
size: {
|
||||||
icon: "h-10 w-10",
|
default: "h-10 px-4 py-2",
|
||||||
},
|
sm: "h-9 rounded-md px-3",
|
||||||
},
|
lg: "h-11 rounded-md px-8",
|
||||||
defaultVariants: {
|
icon: "h-10 w-10",
|
||||||
variant: "default",
|
},
|
||||||
size: "default",
|
},
|
||||||
},
|
defaultVariants: {
|
||||||
},
|
variant: "default",
|
||||||
)
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
export interface ButtonProps
|
||||||
asChild?: boolean
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => {
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
const Comp = asChild ? Slot : "button"
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
const Comp = asChild ? Slot : "button";
|
||||||
})
|
return (
|
||||||
Button.displayName = "Button"
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Button.displayName = "Button";
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants };
|
||||||
|
|||||||
+78
-31
@@ -1,38 +1,85 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
const Card = React.forwardRef<
|
||||||
<div
|
HTMLDivElement,
|
||||||
ref={ref}
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
className={cn("rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none", className)}
|
>(({ className, ...props }, ref) => (
|
||||||
{...props}
|
<div
|
||||||
/>
|
ref={ref}
|
||||||
))
|
className={cn(
|
||||||
Card.displayName = "Card"
|
"rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Card.displayName = "Card";
|
||||||
|
|
||||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
const CardHeader = React.forwardRef<
|
||||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
HTMLDivElement,
|
||||||
))
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
CardHeader.displayName = "CardHeader"
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardHeader.displayName = "CardHeader";
|
||||||
|
|
||||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(({ className, ...props }, ref) => (
|
const CardTitle = React.forwardRef<
|
||||||
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
|
HTMLParagraphElement,
|
||||||
))
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
CardTitle.displayName = "CardTitle"
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardTitle.displayName = "CardTitle";
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(({ className, ...props }, ref) => (
|
const CardDescription = React.forwardRef<
|
||||||
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
HTMLParagraphElement,
|
||||||
))
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
CardDescription.displayName = "CardDescription"
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardDescription.displayName = "CardDescription";
|
||||||
|
|
||||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
const CardContent = React.forwardRef<
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
HTMLDivElement,
|
||||||
))
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
CardContent.displayName = "CardContent"
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
));
|
||||||
|
CardContent.displayName = "CardContent";
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
const CardFooter = React.forwardRef<
|
||||||
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
HTMLDivElement,
|
||||||
))
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
CardFooter.displayName = "CardFooter"
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CardFooter.displayName = "CardFooter";
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
};
|
||||||
|
|||||||
+346
-226
@@ -1,277 +1,397 @@
|
|||||||
import { cn } from "@/lib/utils"
|
"use client";
|
||||||
import * as React from "react"
|
|
||||||
import * as RechartsPrimitive from "recharts"
|
import * as React from "react";
|
||||||
|
import * as RechartsPrimitive from "recharts";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
const THEMES = { light: "", dark: ".dark" } as const
|
const THEMES = { light: "", dark: ".dark" } as const;
|
||||||
|
|
||||||
export type ChartConfig = {
|
export type ChartConfig = {
|
||||||
[k in string]: {
|
[k: string]: {
|
||||||
label?: React.ReactNode
|
label?: React.ReactNode;
|
||||||
icon?: React.ComponentType
|
icon?: React.ComponentType;
|
||||||
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> })
|
} & (
|
||||||
}
|
| { color?: string; theme?: never }
|
||||||
|
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
type ChartContextProps = {
|
type ChartContextProps = {
|
||||||
config: ChartConfig
|
config: ChartConfig;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||||
|
|
||||||
function useChart() {
|
function useChart() {
|
||||||
const context = React.useContext(ChartContext)
|
const context = React.useContext(ChartContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useChart must be used within a <ChartContainer />")
|
throw new Error("useChart must be used within a <ChartContainer />");
|
||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChartContainer = React.forwardRef<
|
const ChartContainer = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> & {
|
React.ComponentProps<"div"> & {
|
||||||
config: ChartConfig
|
config: ChartConfig;
|
||||||
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"]
|
children: React.ComponentProps<
|
||||||
}
|
typeof RechartsPrimitive.ResponsiveContainer
|
||||||
|
>["children"];
|
||||||
|
}
|
||||||
>(({ id, className, children, config, ...props }, ref) => {
|
>(({ id, className, children, config, ...props }, ref) => {
|
||||||
const uniqueId = React.useId()
|
const uniqueId = React.useId();
|
||||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContext.Provider value={{ config }}>
|
<ChartContext.Provider value={{ config }}>
|
||||||
<div
|
<div
|
||||||
data-chart={chartId}
|
data-chart={chartId}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChartStyle id={chartId} config={config} />
|
<ChartStyle id={chartId} config={config} />
|
||||||
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
|
<RechartsPrimitive.ResponsiveContainer>
|
||||||
</div>
|
{children}
|
||||||
</ChartContext.Provider>
|
</RechartsPrimitive.ResponsiveContainer>
|
||||||
)
|
</div>
|
||||||
})
|
</ChartContext.Provider>
|
||||||
ChartContainer.displayName = "Chart"
|
);
|
||||||
|
});
|
||||||
|
ChartContainer.displayName = "Chart";
|
||||||
|
|
||||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||||
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color)
|
const colorConfig = Object.entries(config).filter(
|
||||||
|
([, config]) => config.theme || config.color,
|
||||||
|
);
|
||||||
|
|
||||||
if (!colorConfig.length) {
|
if (!colorConfig.length) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<style
|
<style
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: Object.entries(THEMES)
|
__html: Object.entries(THEMES)
|
||||||
.map(
|
.map(
|
||||||
([theme, prefix]) => `
|
([theme, prefix]) => `
|
||||||
${prefix} [data-chart=${id}] {
|
${prefix} [data-chart=${id}] {
|
||||||
${colorConfig
|
${colorConfig
|
||||||
.map(([key, itemConfig]) => {
|
.map(([key, itemConfig]) => {
|
||||||
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
|
const color =
|
||||||
return color ? ` --color-${key}: ${color};` : null
|
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||||
})
|
itemConfig.color;
|
||||||
.join("\n")}
|
return color ? ` --color-${key}: ${color};` : null;
|
||||||
|
})
|
||||||
|
.join("\n")}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||||
|
|
||||||
const ChartTooltipContent = React.forwardRef<
|
const ChartTooltipContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
React.ComponentProps<"div"> & {
|
||||||
React.ComponentProps<"div"> & {
|
active?: boolean;
|
||||||
hideLabel?: boolean
|
payload?: any[];
|
||||||
hideIndicator?: boolean
|
label?: any;
|
||||||
indicator?: "line" | "dot" | "dashed"
|
hideLabel?: boolean;
|
||||||
nameKey?: string
|
hideIndicator?: boolean;
|
||||||
labelKey?: string
|
indicator?: "line" | "dot" | "dashed";
|
||||||
}
|
nameKey?: string;
|
||||||
|
labelKey?: string;
|
||||||
|
labelFormatter?: (value: any, payload: any[]) => React.ReactNode;
|
||||||
|
formatter?: (
|
||||||
|
value: any,
|
||||||
|
name: any,
|
||||||
|
item: any,
|
||||||
|
index: number,
|
||||||
|
payload: any,
|
||||||
|
) => React.ReactNode;
|
||||||
|
color?: string;
|
||||||
|
labelClassName?: string;
|
||||||
|
}
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
active,
|
active,
|
||||||
payload,
|
payload,
|
||||||
className,
|
className,
|
||||||
indicator = "dot",
|
indicator = "dot",
|
||||||
hideLabel = false,
|
hideLabel = false,
|
||||||
hideIndicator = false,
|
hideIndicator = false,
|
||||||
label,
|
label,
|
||||||
labelFormatter,
|
labelFormatter,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
formatter,
|
formatter,
|
||||||
color,
|
color,
|
||||||
nameKey,
|
nameKey,
|
||||||
labelKey,
|
labelKey,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const { config } = useChart()
|
const { config } = useChart();
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
const tooltipLabel = React.useMemo(() => {
|
||||||
if (hideLabel || !payload?.length) {
|
if (hideLabel || !payload?.length) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [item] = payload
|
const [item] = payload;
|
||||||
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
const key = `${labelKey || item.dataKey || item.name || "value"}`;
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
const value = !labelKey && typeof label === "string" ? config[label as keyof typeof config]?.label || label : itemConfig?.label
|
const value =
|
||||||
|
!labelKey && typeof label === "string"
|
||||||
|
? config[label as keyof typeof config]?.label || label
|
||||||
|
: itemConfig?.label;
|
||||||
|
|
||||||
if (labelFormatter) {
|
if (labelFormatter) {
|
||||||
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
|
return (
|
||||||
}
|
<div className={cn("font-medium", labelClassName)}>
|
||||||
|
{labelFormatter(value, payload)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||||
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey])
|
}, [
|
||||||
|
label,
|
||||||
|
labelFormatter,
|
||||||
|
payload,
|
||||||
|
hideLabel,
|
||||||
|
labelClassName,
|
||||||
|
config,
|
||||||
|
labelKey,
|
||||||
|
]);
|
||||||
|
|
||||||
if (!active || !payload?.length) {
|
if (!active || !payload?.length) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
payload.sort((a, b) => {
|
||||||
|
return Number(b.value) - Number(a.value);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{!nestLabel ? tooltipLabel : null}
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
{payload.map((item, index) => {
|
|
||||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
const indicatorColor = color || item.payload.fill || item.color
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.dataKey}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
"grid min-w-32 items-start gap-1.5 overflow-hidden rounded-sm border border-border/50 bg-stone-100 text-xs dark:bg-stone-900",
|
||||||
indicator === "dot" && "items-center",
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{formatter && item?.value !== undefined && item.name ? (
|
{!nestLabel && (
|
||||||
formatter(item.value, item.name, item, index, item.payload)
|
<div className="mx-auto -mb-1 px-2.5 pt-1">
|
||||||
) : (
|
{!nestLabel ? tooltipLabel : null}
|
||||||
<>
|
</div>
|
||||||
{itemConfig?.icon ? (
|
)}
|
||||||
<itemConfig.icon />
|
|
||||||
) : (
|
|
||||||
!hideIndicator && (
|
|
||||||
<div
|
|
||||||
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
|
|
||||||
"h-2.5 w-2.5": indicator === "dot",
|
|
||||||
"w-1": indicator === "line",
|
|
||||||
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
|
|
||||||
"my-0.5": nestLabel && indicator === "dashed",
|
|
||||||
})}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--color-bg": indicatorColor,
|
|
||||||
"--color-border": indicatorColor,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<div className={cn("flex flex-1 justify-between leading-none", nestLabel ? "items-end" : "items-center")}>
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
{nestLabel ? tooltipLabel : null}
|
|
||||||
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
|
|
||||||
</div>
|
|
||||||
{item.value && <span className="font-mono font-medium tabular-nums text-foreground">{item.value.toLocaleString()}</span>}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ChartTooltipContent.displayName = "ChartTooltip"
|
|
||||||
|
|
||||||
const ChartLegend = RechartsPrimitive.Legend
|
<div
|
||||||
|
className={cn("grid gap-1.5 bg-white px-2.5 py-1.5 dark:bg-black", {
|
||||||
|
"border-t": !nestLabel,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{payload.map((item, index) => {
|
||||||
|
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
|
const indicatorColor = color || item.payload.fill || item.color;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.dataKey}
|
||||||
|
className={cn(
|
||||||
|
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||||
|
indicator === "dot" && "items-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatter && item?.value !== undefined && item.name ? (
|
||||||
|
formatter(item.value, item.name, item, index, item.payload)
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{itemConfig?.icon ? (
|
||||||
|
<itemConfig.icon />
|
||||||
|
) : (
|
||||||
|
!hideIndicator && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 rounded-[2px] border-border bg-(--color-bg)",
|
||||||
|
{
|
||||||
|
"h-2.5 w-2.5": indicator === "dot",
|
||||||
|
"w-1": indicator === "line",
|
||||||
|
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||||
|
indicator === "dashed",
|
||||||
|
"my-0.5": nestLabel && indicator === "dashed",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--color-bg": indicatorColor,
|
||||||
|
"--color-border": indicatorColor,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 justify-between leading-none",
|
||||||
|
nestLabel ? "items-end" : "items-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
{nestLabel ? tooltipLabel : null}
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{itemConfig?.label || item.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{item.value && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"ml-2 font-medium text-foreground tabular-nums",
|
||||||
|
payload.length === 1 && "-ml-9",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{typeof item.value === "number"
|
||||||
|
? item.value.toFixed(2).toLocaleString()
|
||||||
|
: item.value}{" "}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ChartTooltipContent.displayName = "ChartTooltip";
|
||||||
|
|
||||||
|
const ChartLegend = RechartsPrimitive.Legend;
|
||||||
|
|
||||||
const ChartLegendContent = React.forwardRef<
|
const ChartLegendContent = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<"div"> &
|
React.ComponentProps<"div"> & {
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
payload?: any[];
|
||||||
hideIcon?: boolean
|
verticalAlign?: "top" | "bottom" | "middle";
|
||||||
nameKey?: string
|
hideIcon?: boolean;
|
||||||
}
|
nameKey?: string;
|
||||||
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
|
}
|
||||||
const { config } = useChart()
|
>(
|
||||||
|
(
|
||||||
|
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const { config } = useChart();
|
||||||
|
|
||||||
if (!payload?.length) {
|
if (!payload?.length) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={cn("flex flex-wrap items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}>
|
<div
|
||||||
{payload.map((item) => {
|
ref={ref}
|
||||||
const key = `${nameKey || item.dataKey || "value"}`
|
className={cn(
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
"flex flex-wrap items-center justify-center gap-4",
|
||||||
|
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{payload.map((item) => {
|
||||||
|
const key = `${nameKey || item.dataKey || "value"}`;
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.value} className={cn("flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground")}>
|
<div
|
||||||
{itemConfig?.icon && !hideIcon ? (
|
key={item.value}
|
||||||
<itemConfig.icon />
|
className={cn(
|
||||||
) : (
|
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
|
||||||
<div
|
)}
|
||||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
>
|
||||||
style={{
|
{itemConfig?.icon && !hideIcon ? (
|
||||||
backgroundColor: item.color,
|
<itemConfig.icon />
|
||||||
}}
|
) : (
|
||||||
/>
|
<div
|
||||||
)}
|
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||||
{itemConfig?.label}
|
style={{
|
||||||
</div>
|
backgroundColor: item.color,
|
||||||
)
|
}}
|
||||||
})}
|
/>
|
||||||
</div>
|
)}
|
||||||
)
|
{key}
|
||||||
})
|
</div>
|
||||||
ChartLegendContent.displayName = "ChartLegend"
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ChartLegendContent.displayName = "ChartLegend";
|
||||||
|
|
||||||
// Helper to extract item config from a payload.
|
// Helper to extract item config from a payload.
|
||||||
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
|
function getPayloadConfigFromPayload(
|
||||||
if (typeof payload !== "object" || payload === null) {
|
config: ChartConfig,
|
||||||
return undefined
|
payload: unknown,
|
||||||
}
|
key: string,
|
||||||
|
) {
|
||||||
|
if (typeof payload !== "object" || payload === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const payloadPayload = "payload" in payload && typeof payload.payload === "object" && payload.payload !== null ? payload.payload : undefined
|
const payloadPayload =
|
||||||
|
"payload" in payload &&
|
||||||
|
typeof payload.payload === "object" &&
|
||||||
|
payload.payload !== null
|
||||||
|
? payload.payload
|
||||||
|
: undefined;
|
||||||
|
|
||||||
let configLabelKey: string = key
|
let configLabelKey: string = key;
|
||||||
|
|
||||||
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
|
if (
|
||||||
configLabelKey = payload[key as keyof typeof payload] as string
|
key in payload &&
|
||||||
} else if (payloadPayload && key in payloadPayload && typeof payloadPayload[key as keyof typeof payloadPayload] === "string") {
|
typeof payload[key as keyof typeof payload] === "string"
|
||||||
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string
|
) {
|
||||||
}
|
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||||
|
} else if (
|
||||||
|
payloadPayload &&
|
||||||
|
key in payloadPayload &&
|
||||||
|
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||||
|
) {
|
||||||
|
configLabelKey = payloadPayload[
|
||||||
|
key as keyof typeof payloadPayload
|
||||||
|
] as string;
|
||||||
|
}
|
||||||
|
|
||||||
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
|
return configLabelKey in config
|
||||||
|
? config[configLabelKey]
|
||||||
|
: config[key as keyof typeof config];
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle }
|
export {
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
ChartLegend,
|
||||||
|
ChartLegendContent,
|
||||||
|
ChartStyle,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
import { Check } from "lucide-react";
|
||||||
import { Check } from "lucide-react"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<React.ElementRef<typeof CheckboxPrimitive.Root>, React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>>(
|
const Checkbox = React.forwardRef<
|
||||||
({ className, ...props }, ref) => (
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
<CheckboxPrimitive.Root
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
ref={ref}
|
>(({ className, ...props }, ref) => (
|
||||||
className={cn(
|
<CheckboxPrimitive.Root
|
||||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
ref={ref}
|
||||||
className,
|
className={cn(
|
||||||
)}
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
{...props}
|
className,
|
||||||
>
|
)}
|
||||||
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
{...props}
|
||||||
<Check className="h-4 w-4" />
|
>
|
||||||
</CheckboxPrimitive.Indicator>
|
<CheckboxPrimitive.Indicator
|
||||||
</CheckboxPrimitive.Root>
|
className={cn("flex items-center justify-center text-current")}
|
||||||
),
|
>
|
||||||
)
|
<Check className="h-4 w-4" />
|
||||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
));
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Checkbox }
|
export { Checkbox };
|
||||||
|
|||||||
+141
-89
@@ -1,107 +1,159 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
import { type DialogProps, DialogTitle } from "@radix-ui/react-dialog";
|
||||||
import { cn } from "@/lib/utils"
|
import { Command as CommandPrimitive } from "cmdk";
|
||||||
import { type DialogProps, DialogTitle } from "@radix-ui/react-dialog"
|
import { Search } from "lucide-react";
|
||||||
import { Command as CommandPrimitive } from "cmdk"
|
import * as React from "react";
|
||||||
import { Search } from "lucide-react"
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Command = React.forwardRef<React.ElementRef<typeof CommandPrimitive>, React.ComponentPropsWithoutRef<typeof CommandPrimitive>>(
|
const Command = React.forwardRef<
|
||||||
({ className, ...props }, ref) => (
|
React.ElementRef<typeof CommandPrimitive>,
|
||||||
<CommandPrimitive
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||||
ref={ref}
|
>(({ className, ...props }, ref) => (
|
||||||
className={cn("flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", className)}
|
<CommandPrimitive
|
||||||
{...props}
|
ref={ref}
|
||||||
/>
|
className={cn(
|
||||||
),
|
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||||
)
|
className,
|
||||||
Command.displayName = CommandPrimitive.displayName
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Command.displayName = CommandPrimitive.displayName;
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||||
return (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogTitle />
|
<DialogTitle />
|
||||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-4 [&_[cmdk-input-wrapper]_svg]:w-4 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4">
|
<Command className="**:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 **:[[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-4 [&_[cmdk-input-wrapper]_svg]:w-4 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4">
|
||||||
{children}
|
{children}
|
||||||
</Command>
|
</Command>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const CommandInput = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Input>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>>(
|
const CommandInput = React.forwardRef<
|
||||||
({ className, ...props }, ref) => (
|
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||||
<div className="flex items-center bg-stone-100 dark:bg-stone-900 px-3" cmdk-input-wrapper="">
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
>(({ className, ...props }, ref) => (
|
||||||
<CommandPrimitive.Input
|
<div
|
||||||
ref={ref}
|
className="flex items-center bg-stone-100 dark:bg-stone-900 px-3"
|
||||||
className={cn(
|
cmdk-input-wrapper=""
|
||||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
>
|
||||||
className,
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
)}
|
<CommandPrimitive.Input
|
||||||
{...props}
|
ref={ref}
|
||||||
/>
|
className={cn(
|
||||||
</div>
|
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
),
|
className,
|
||||||
)
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||||
|
|
||||||
const CommandList = React.forwardRef<React.ElementRef<typeof CommandPrimitive.List>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>>(
|
const CommandList = React.forwardRef<
|
||||||
({ className, ...props }, ref) => (
|
React.ElementRef<typeof CommandPrimitive.List>,
|
||||||
<CommandPrimitive.List ref={ref} className={cn("max-h-[300px] mb-1 overflow-y-auto overflow-x-hidden", className)} {...props} />
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||||
),
|
>(({ className, ...props }, ref) => (
|
||||||
)
|
<CommandPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"max-h-[300px] mb-1 overflow-y-auto overflow-x-hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
CommandList.displayName = CommandPrimitive.List.displayName
|
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||||
|
|
||||||
const CommandEmpty = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Empty>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>>(
|
const CommandEmpty = React.forwardRef<
|
||||||
(props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />,
|
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||||
)
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||||
|
>((props, ref) => (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
ref={ref}
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||||
|
|
||||||
const CommandGroup = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Group>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>>(
|
const CommandGroup = React.forwardRef<
|
||||||
({ className, ...props }, ref) => (
|
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||||
<CommandPrimitive.Group
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||||
ref={ref}
|
>(({ className, ...props }, ref) => (
|
||||||
className={cn(
|
<CommandPrimitive.Group
|
||||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
ref={ref}
|
||||||
className,
|
className={cn(
|
||||||
)}
|
"overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
|
||||||
{...props}
|
className,
|
||||||
/>
|
)}
|
||||||
),
|
{...props}
|
||||||
)
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||||
|
|
||||||
const CommandSeparator = React.forwardRef<
|
const CommandSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||||
>(({ className, ...props }, ref) => <CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />)
|
>(({ className, ...props }, ref) => (
|
||||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
<CommandPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||||
|
|
||||||
const CommandItem = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Item>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>>(
|
const CommandItem = React.forwardRef<
|
||||||
({ className, ...props }, ref) => (
|
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||||
<CommandPrimitive.Item
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||||
ref={ref}
|
>(({ className, ...props }, ref) => (
|
||||||
className={cn(
|
<CommandPrimitive.Item
|
||||||
"relative flex cursor-default gap-2 select-none items-center rounded-[8px] px-2 py-1.5 text-xs outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-stone-100 dark:data-[selected='true']:bg-stone-900 data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
ref={ref}
|
||||||
className,
|
className={cn(
|
||||||
)}
|
"relative flex cursor-default gap-2 select-none items-center rounded-[8px] px-2 py-1.5 text-xs outline-hidden data-[disabled=true]:pointer-events-none data-[selected='true']:bg-stone-100 dark:data-[selected='true']:bg-stone-900 data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
{...props}
|
className,
|
||||||
/>
|
)}
|
||||||
),
|
{...props}
|
||||||
)
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||||
|
|
||||||
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
const CommandShortcut = ({
|
||||||
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
|
className,
|
||||||
}
|
...props
|
||||||
CommandShortcut.displayName = "CommandShortcut"
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
CommandShortcut.displayName = "CommandShortcut";
|
||||||
|
|
||||||
export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator }
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
};
|
||||||
|
|||||||
+104
-60
@@ -1,76 +1,120 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import { X } from "lucide-react";
|
||||||
import { X } from "lucide-react"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
const Dialog = DialogPrimitive.Root
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const DialogTrigger = DialogPrimitive.Trigger
|
const Dialog = DialogPrimitive.Root;
|
||||||
|
|
||||||
const DialogPortal = DialogPrimitive.Portal
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
|
||||||
const DialogClose = DialogPrimitive.Close
|
const DialogPortal = DialogPrimitive.Portal;
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close;
|
||||||
|
|
||||||
const DialogOverlay = React.forwardRef<
|
const DialogOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
))
|
));
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
const DialogHeader = ({
|
||||||
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
className,
|
||||||
)
|
...props
|
||||||
DialogHeader.displayName = "DialogHeader"
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
DialogHeader.displayName = "DialogHeader";
|
||||||
|
|
||||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
const DialogFooter = ({
|
||||||
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
className,
|
||||||
)
|
...props
|
||||||
DialogFooter.displayName = "DialogFooter"
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
DialogFooter.displayName = "DialogFooter";
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Title>, React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>>(
|
const DialogTitle = React.forwardRef<
|
||||||
({ className, ...props }, ref) => (
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
<DialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
),
|
>(({ className, ...props }, ref) => (
|
||||||
)
|
<DialogPrimitive.Title
|
||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
const DialogDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
>(({ className, ...props }, ref) => <DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />)
|
>(({ className, ...props }, ref) => (
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription }
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogClose,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
};
|
||||||
|
|||||||
+165
-140
@@ -1,172 +1,197 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||||
|
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||||
|
|
||||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||||
|
|
||||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef<
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent data-[state=open]:bg-accent",
|
||||||
inset && "pl-8",
|
inset && "pl-8",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
))
|
));
|
||||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
|
DropdownMenuSubTrigger.displayName =
|
||||||
|
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
|
DropdownMenuSubContent.displayName =
|
||||||
|
DropdownMenuPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
const DropdownMenuContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
))
|
));
|
||||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DropdownMenuItem = React.forwardRef<
|
const DropdownMenuItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default justify-between select-none items-center gap-2 rounded-[10px] px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
"relative flex cursor-default justify-between select-none items-center gap-2 rounded-[10px] px-2 py-1.5 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
inset && "pl-8",
|
inset && "pl-8",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
))
|
));
|
||||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
|
DropdownMenuCheckboxItem.displayName =
|
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
))
|
));
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||||
|
|
||||||
const DropdownMenuLabel = React.forwardRef<
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
inset?: boolean
|
inset?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)} {...props} />
|
<DropdownMenuPrimitive.Label
|
||||||
))
|
ref={ref}
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
>(({ className, ...props }, ref) => <DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />)
|
>(({ className, ...props }, ref) => (
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
const DropdownMenuShortcut = ({
|
||||||
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
|
className,
|
||||||
}
|
...props
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuPortal,
|
DropdownMenuPortal,
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuRadioGroup,
|
||||||
}
|
};
|
||||||
|
|||||||
+21
-18
@@ -1,21 +1,24 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
return (
|
({ className, type, ...props }, ref) => {
|
||||||
<input
|
return (
|
||||||
type={type}
|
<input
|
||||||
className={cn(
|
type={type}
|
||||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
className={cn(
|
||||||
className,
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
)}
|
className,
|
||||||
ref={ref}
|
)}
|
||||||
{...props}
|
ref={ref}
|
||||||
/>
|
{...props}
|
||||||
)
|
/>
|
||||||
})
|
);
|
||||||
Input.displayName = "Input"
|
},
|
||||||
|
);
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
export { Input }
|
export { Input };
|
||||||
|
|||||||
+19
-10
@@ -1,14 +1,23 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { type VariantProps, cva } from "class-variance-authority"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70")
|
const labelVariants = cva(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||||
|
);
|
||||||
|
|
||||||
const Label = React.forwardRef<
|
const Label = React.forwardRef<
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
>(({ className, ...props }, ref) => <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />)
|
VariantProps<typeof labelVariants>
|
||||||
Label.displayName = LabelPrimitive.Root.displayName
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(labelVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Label }
|
export { Label };
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Popover = PopoverPrimitive.Root
|
const Popover = PopoverPrimitive.Root;
|
||||||
|
|
||||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||||
|
|
||||||
const PopoverContent = React.forwardRef<
|
const PopoverContent = React.forwardRef<
|
||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
<PopoverPrimitive.Portal>
|
<PopoverPrimitive.Portal>
|
||||||
<PopoverPrimitive.Content
|
<PopoverPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-2xl outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-2xl outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</PopoverPrimitive.Portal>
|
</PopoverPrimitive.Portal>
|
||||||
))
|
));
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent }
|
export { Popover, PopoverTrigger, PopoverContent };
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Progress = React.forwardRef<
|
const Progress = React.forwardRef<
|
||||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
|
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
|
||||||
indicatorClassName?: string
|
indicatorClassName?: string;
|
||||||
}
|
}
|
||||||
>(({ className, value, indicatorClassName, ...props }, ref) => (
|
>(({ className, value, indicatorClassName, ...props }, ref) => (
|
||||||
<ProgressPrimitive.Root ref={ref} className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)} {...props}>
|
<ProgressPrimitive.Root
|
||||||
<ProgressPrimitive.Indicator
|
ref={ref}
|
||||||
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
|
className={cn(
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
||||||
/>
|
className,
|
||||||
</ProgressPrimitive.Root>
|
)}
|
||||||
))
|
{...props}
|
||||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
className={cn(
|
||||||
|
"h-full w-full flex-1 bg-primary transition-all",
|
||||||
|
indicatorClassName,
|
||||||
|
)}
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
));
|
||||||
|
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Progress }
|
export { Progress };
|
||||||
|
|||||||
+135
-104
@@ -1,126 +1,157 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root
|
const Select = SelectPrimitive.Root;
|
||||||
|
|
||||||
const SelectGroup = SelectPrimitive.Group
|
const SelectGroup = SelectPrimitive.Group;
|
||||||
|
|
||||||
const SelectValue = SelectPrimitive.Value
|
const SelectValue = SelectPrimitive.Value;
|
||||||
|
|
||||||
const SelectTrigger = React.forwardRef<
|
const SelectTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<SelectPrimitive.Icon asChild>
|
<SelectPrimitive.Icon asChild>
|
||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
))
|
));
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
const SelectScrollUpButton = React.forwardRef<
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.ScrollUpButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}>
|
<SelectPrimitive.ScrollUpButton
|
||||||
<ChevronUp className="h-4 w-4" />
|
ref={ref}
|
||||||
</SelectPrimitive.ScrollUpButton>
|
className={cn(
|
||||||
))
|
"flex cursor-default items-center justify-center py-1",
|
||||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
));
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||||
|
|
||||||
const SelectScrollDownButton = React.forwardRef<
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SelectPrimitive.ScrollDownButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}>
|
<SelectPrimitive.ScrollDownButton
|
||||||
<ChevronDown className="h-4 w-4" />
|
ref={ref}
|
||||||
</SelectPrimitive.ScrollDownButton>
|
className={cn(
|
||||||
))
|
"flex cursor-default items-center justify-center py-1",
|
||||||
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
));
|
||||||
|
SelectScrollDownButton.displayName =
|
||||||
|
SelectPrimitive.ScrollDownButton.displayName;
|
||||||
|
|
||||||
const SelectContent = React.forwardRef<
|
const SelectContent = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
<SelectPrimitive.Portal>
|
<SelectPrimitive.Portal>
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SelectScrollUpButton />
|
<SelectScrollUpButton />
|
||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Viewport
|
||||||
className={cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")}
|
className={cn(
|
||||||
>
|
"p-1",
|
||||||
{children}
|
position === "popper" &&
|
||||||
</SelectPrimitive.Viewport>
|
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)",
|
||||||
<SelectScrollDownButton />
|
)}
|
||||||
</SelectPrimitive.Content>
|
>
|
||||||
</SelectPrimitive.Portal>
|
{children}
|
||||||
))
|
</SelectPrimitive.Viewport>
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
));
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Label>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>>(
|
const SelectLabel = React.forwardRef<
|
||||||
({ className, ...props }, ref) => (
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
),
|
>(({ className, ...props }, ref) => (
|
||||||
)
|
<SelectPrimitive.Label
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
ref={ref}
|
||||||
|
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>>(
|
const SelectItem = React.forwardRef<
|
||||||
({ className, children, ...props }, ref) => (
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
<SelectPrimitive.Item
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
ref={ref}
|
>(({ className, children, ...props }, ref) => (
|
||||||
className={cn(
|
<SelectPrimitive.Item
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
ref={ref}
|
||||||
className,
|
className={cn(
|
||||||
)}
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||||
{...props}
|
className,
|
||||||
>
|
)}
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
{...props}
|
||||||
<SelectPrimitive.ItemIndicator>
|
>
|
||||||
<Check className="h-4 w-4" />
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
</SelectPrimitive.ItemIndicator>
|
<SelectPrimitive.ItemIndicator>
|
||||||
</span>
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
</SelectPrimitive.Item>
|
</SelectPrimitive.Item>
|
||||||
),
|
));
|
||||||
)
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const SelectSeparator = React.forwardRef<
|
const SelectSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
>(({ className, ...props }, ref) => <SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />)
|
>(({ className, ...props }, ref) => (
|
||||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
<SelectPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Select,
|
Select,
|
||||||
SelectGroup,
|
SelectGroup,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectLabel,
|
SelectLabel,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectSeparator,
|
SelectSeparator,
|
||||||
SelectScrollUpButton,
|
SelectScrollUpButton,
|
||||||
SelectScrollDownButton,
|
SelectScrollDownButton,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Separator = React.forwardRef<React.ElementRef<typeof SeparatorPrimitive.Root>, React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>>(
|
const Separator = React.forwardRef<
|
||||||
({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
|
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||||
<SeparatorPrimitive.Root
|
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||||
ref={ref}
|
>(
|
||||||
decorative={decorative}
|
(
|
||||||
orientation={orientation}
|
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||||
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
|
ref,
|
||||||
{...props}
|
) => (
|
||||||
/>
|
<SeparatorPrimitive.Root
|
||||||
),
|
ref={ref}
|
||||||
)
|
decorative={decorative}
|
||||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border",
|
||||||
|
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Separator }
|
export { Separator };
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
function Skeleton({
|
||||||
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Skeleton }
|
export { Skeleton };
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>, React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>>(
|
const Switch = React.forwardRef<
|
||||||
({ className, ...props }, ref) => (
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
<SwitchPrimitives.Root
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
className={cn(
|
>(({ className, ...props }, ref) => (
|
||||||
"peer inline-flex h-3 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
<SwitchPrimitives.Root
|
||||||
className,
|
className={cn(
|
||||||
)}
|
"peer inline-flex h-3 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
{...props}
|
className,
|
||||||
ref={ref}
|
)}
|
||||||
>
|
{...props}
|
||||||
<SwitchPrimitives.Thumb
|
ref={ref}
|
||||||
className={cn(
|
>
|
||||||
"pointer-events-none block h-2 w-2 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0",
|
<SwitchPrimitives.Thumb
|
||||||
)}
|
className={cn(
|
||||||
/>
|
"pointer-events-none block h-2 w-2 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0",
|
||||||
</SwitchPrimitives.Root>
|
)}
|
||||||
),
|
/>
|
||||||
)
|
</SwitchPrimitives.Root>
|
||||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
));
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||||
|
|
||||||
export { Switch }
|
export { Switch };
|
||||||
|
|||||||
+107
-41
@@ -1,50 +1,116 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(({ className, ...props }, ref) => (
|
const Table = React.forwardRef<
|
||||||
<div className="relative w-full overflow-auto">
|
HTMLTableElement,
|
||||||
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
</div>
|
>(({ className, ...props }, ref) => (
|
||||||
))
|
<div className="relative w-full overflow-auto">
|
||||||
Table.displayName = "Table"
|
<table
|
||||||
|
ref={ref}
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
Table.displayName = "Table";
|
||||||
|
|
||||||
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
|
const TableHeader = React.forwardRef<
|
||||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
HTMLTableSectionElement,
|
||||||
))
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
TableHeader.displayName = "TableHeader"
|
>(({ className, ...props }, ref) => (
|
||||||
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||||
|
));
|
||||||
|
TableHeader.displayName = "TableHeader";
|
||||||
|
|
||||||
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
|
const TableBody = React.forwardRef<
|
||||||
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
HTMLTableSectionElement,
|
||||||
))
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
TableBody.displayName = "TableBody"
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tbody
|
||||||
|
ref={ref}
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableBody.displayName = "TableBody";
|
||||||
|
|
||||||
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
|
const TableFooter = React.forwardRef<
|
||||||
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} />
|
HTMLTableSectionElement,
|
||||||
))
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
TableFooter.displayName = "TableFooter"
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tfoot
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-t bg-muted/50 font-medium last:[&>tr]:border-b-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableFooter.displayName = "TableFooter";
|
||||||
|
|
||||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(({ className, ...props }, ref) => (
|
const TableRow = React.forwardRef<
|
||||||
<tr ref={ref} className={cn("border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", className)} {...props} />
|
HTMLTableRowElement,
|
||||||
))
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
TableRow.displayName = "TableRow"
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => (
|
const TableHead = React.forwardRef<
|
||||||
<th
|
HTMLTableCellElement,
|
||||||
ref={ref}
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
className={cn("h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", className)}
|
>(({ className, ...props }, ref) => (
|
||||||
{...props}
|
<th
|
||||||
/>
|
ref={ref}
|
||||||
))
|
className={cn(
|
||||||
TableHead.displayName = "TableHead"
|
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => (
|
const TableCell = React.forwardRef<
|
||||||
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
|
HTMLTableCellElement,
|
||||||
))
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
TableCell.displayName = "TableCell"
|
>(({ className, ...props }, ref) => (
|
||||||
|
<td
|
||||||
|
ref={ref}
|
||||||
|
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableCell.displayName = "TableCell";
|
||||||
|
|
||||||
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(({ className, ...props }, ref) => (
|
const TableCaption = React.forwardRef<
|
||||||
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
|
HTMLTableCaptionElement,
|
||||||
))
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
TableCaption.displayName = "TableCaption"
|
>(({ className, ...props }, ref) => (
|
||||||
|
<caption
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TableCaption.displayName = "TableCaption";
|
||||||
|
|
||||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
import * as React from "react";
|
||||||
import * as React from "react"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const TooltipProvider = TooltipPrimitive.Provider
|
const TooltipProvider = TooltipPrimitive.Provider;
|
||||||
|
|
||||||
const Tooltip = TooltipPrimitive.Root
|
const Tooltip = TooltipPrimitive.Root;
|
||||||
|
|
||||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||||
|
|
||||||
const TooltipContent = React.forwardRef<
|
const TooltipContent = React.forwardRef<
|
||||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
<TooltipPrimitive.Content
|
<TooltipPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 overflow-hidden rounded-[8px] border font-medium bg-popover px-1.5 py-0.5 text-xs text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"z-50 overflow-hidden rounded-[8px] border font-medium bg-popover px-1.5 py-0.5 text-xs text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
export interface CommandContextType {
|
||||||
|
isOpen: boolean;
|
||||||
|
openCommand: () => void;
|
||||||
|
closeCommand: () => void;
|
||||||
|
toggleCommand: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommandContext = createContext<CommandContextType | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { type ReactNode, useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { CommandContext } from "./command-context";
|
||||||
|
|
||||||
|
export function CommandProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const openCommand = useCallback(() => setIsOpen(true), []);
|
||||||
|
const closeCommand = useCallback(() => setIsOpen(false), []);
|
||||||
|
const toggleCommand = useCallback(() => setIsOpen((prev) => !prev), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommandContext.Provider
|
||||||
|
value={{
|
||||||
|
isOpen,
|
||||||
|
openCommand,
|
||||||
|
closeCommand,
|
||||||
|
toggleCommand,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CommandContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
+35
-10
@@ -1,18 +1,43 @@
|
|||||||
import { createContext } from "react"
|
import { createContext } from "react";
|
||||||
|
|
||||||
export type SortType = "default" | "name" | "uptime" | "system" | "cpu" | "mem" | "disk" | "up" | "down" | "up total" | "down total"
|
export type SortType =
|
||||||
|
| "default"
|
||||||
|
| "name"
|
||||||
|
| "uptime"
|
||||||
|
| "system"
|
||||||
|
| "cpu"
|
||||||
|
| "mem"
|
||||||
|
| "disk"
|
||||||
|
| "up"
|
||||||
|
| "down"
|
||||||
|
| "up total"
|
||||||
|
| "down total";
|
||||||
|
|
||||||
export const SORT_TYPES: SortType[] = ["default", "name", "uptime", "system", "cpu", "mem", "disk", "up", "down", "up total", "down total"]
|
export const SORT_TYPES: SortType[] = [
|
||||||
|
"default",
|
||||||
|
"name",
|
||||||
|
"uptime",
|
||||||
|
"system",
|
||||||
|
"cpu",
|
||||||
|
"mem",
|
||||||
|
"disk",
|
||||||
|
"up",
|
||||||
|
"down",
|
||||||
|
"up total",
|
||||||
|
"down total",
|
||||||
|
];
|
||||||
|
|
||||||
export type SortOrder = "asc" | "desc"
|
export type SortOrder = "asc" | "desc";
|
||||||
|
|
||||||
export const SORT_ORDERS: SortOrder[] = ["desc", "asc"]
|
export const SORT_ORDERS: SortOrder[] = ["desc", "asc"];
|
||||||
|
|
||||||
export interface SortContextType {
|
export interface SortContextType {
|
||||||
sortType: SortType
|
sortType: SortType;
|
||||||
sortOrder: SortOrder
|
sortOrder: SortOrder;
|
||||||
setSortType: (sortType: SortType) => void
|
setSortType: (sortType: SortType) => void;
|
||||||
setSortOrder: (sortOrder: SortOrder) => void
|
setSortOrder: (sortOrder: SortOrder) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SortContext = createContext<SortContextType | undefined>(undefined)
|
export const SortContext = createContext<SortContextType | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { ReactNode, useState } from "react"
|
import { type ReactNode, useState } from "react";
|
||||||
|
|
||||||
import { SortContext, SortOrder, SortType } from "./sort-context"
|
import { SortContext, type SortOrder, type SortType } from "./sort-context";
|
||||||
|
|
||||||
export function SortProvider({ children }: { children: ReactNode }) {
|
export function SortProvider({ children }: { children: ReactNode }) {
|
||||||
const [sortType, setSortType] = useState<SortType>("default")
|
const [sortType, setSortType] = useState<SortType>("default");
|
||||||
const [sortOrder, setSortOrder] = useState<SortOrder>("desc")
|
const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
|
||||||
|
|
||||||
return <SortContext.Provider value={{ sortType, setSortType, sortOrder, setSortOrder }}>{children}</SortContext.Provider>
|
return (
|
||||||
|
<SortContext.Provider
|
||||||
|
value={{ sortType, setSortType, sortOrder, setSortOrder }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SortContext.Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { createContext } from "react"
|
import { createContext } from "react";
|
||||||
|
|
||||||
export type Status = "all" | "online" | "offline"
|
export type Status = "all" | "online" | "offline";
|
||||||
|
|
||||||
export interface StatusContextType {
|
export interface StatusContextType {
|
||||||
status: Status
|
status: Status;
|
||||||
setStatus: (status: Status) => void
|
setStatus: (status: Status) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatusContext = createContext<StatusContextType | undefined>(undefined)
|
export const StatusContext = createContext<StatusContextType | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { ReactNode, useState } from "react"
|
import { type ReactNode, useState } from "react";
|
||||||
|
|
||||||
import { Status, StatusContext } from "./status-context"
|
import { type Status, StatusContext } from "./status-context";
|
||||||
|
|
||||||
export function StatusProvider({ children }: { children: ReactNode }) {
|
export function StatusProvider({ children }: { children: ReactNode }) {
|
||||||
const [status, setStatus] = useState<Status>("all")
|
const [status, setStatus] = useState<Status>("all");
|
||||||
|
|
||||||
return <StatusContext.Provider value={{ status, setStatus }}>{children}</StatusContext.Provider>
|
return (
|
||||||
|
<StatusContext.Provider value={{ status, setStatus }}>
|
||||||
|
{children}
|
||||||
|
</StatusContext.Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import { createContext } from "react"
|
import { createContext } from "react";
|
||||||
|
|
||||||
export interface TooltipData {
|
export interface TooltipData {
|
||||||
centroid: [number, number]
|
centroid: [number, number];
|
||||||
country: string
|
country: string;
|
||||||
count: number
|
count: number;
|
||||||
servers: Array<{
|
servers: Array<{
|
||||||
name: string
|
id: number;
|
||||||
status: boolean
|
name: string;
|
||||||
}>
|
status: boolean;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TooltipContextType {
|
interface TooltipContextType {
|
||||||
tooltipData: TooltipData | null
|
tooltipData: TooltipData | null;
|
||||||
setTooltipData: (data: TooltipData | null) => void
|
setTooltipData: (data: TooltipData | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TooltipContext = createContext<TooltipContextType | undefined>(undefined)
|
export const TooltipContext = createContext<TooltipContextType | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { ReactNode, useState } from "react"
|
import { type ReactNode, useState } from "react";
|
||||||
|
|
||||||
import { TooltipContext, TooltipData } from "./tooltip-context"
|
import { TooltipContext, type TooltipData } from "./tooltip-context";
|
||||||
|
|
||||||
export function TooltipProvider({ children }: { children: ReactNode }) {
|
export function TooltipProvider({ children }: { children: ReactNode }) {
|
||||||
const [tooltipData, setTooltipData] = useState<TooltipData | null>(null)
|
const [tooltipData, setTooltipData] = useState<TooltipData | null>(null);
|
||||||
|
|
||||||
return <TooltipContext.Provider value={{ tooltipData, setTooltipData }}>{children}</TooltipContext.Provider>
|
return (
|
||||||
|
<TooltipContext.Provider value={{ tooltipData, setTooltipData }}>
|
||||||
|
{children}
|
||||||
|
</TooltipContext.Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { createContext } from "react"
|
import { createContext } from "react";
|
||||||
|
|
||||||
export interface WebSocketContextType {
|
export interface WebSocketContextType {
|
||||||
lastMessage: { data: string } | null
|
lastMessage: { data: string } | null;
|
||||||
connected: boolean
|
connected: boolean;
|
||||||
messageHistory: { data: string }[]
|
messageHistory: { data: string }[];
|
||||||
reconnect: () => void
|
reconnect: () => void;
|
||||||
needReconnect: boolean
|
needReconnect: boolean;
|
||||||
setNeedReconnect: (needReconnect: boolean) => void
|
setNeedReconnect: (needReconnect: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WebSocketContext = createContext<WebSocketContextType>({
|
export const WebSocketContext = createContext<WebSocketContextType>({
|
||||||
lastMessage: null,
|
lastMessage: null,
|
||||||
connected: false,
|
connected: false,
|
||||||
messageHistory: [],
|
messageHistory: [],
|
||||||
reconnect: () => {},
|
reconnect: () => {},
|
||||||
needReconnect: false,
|
needReconnect: false,
|
||||||
setNeedReconnect: () => {},
|
setNeedReconnect: () => {},
|
||||||
})
|
});
|
||||||
|
|||||||
+123
-109
@@ -1,132 +1,146 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react"
|
import type React from "react";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { WebSocketContext, WebSocketContextType } from "./websocket-context"
|
import {
|
||||||
|
WebSocketContext,
|
||||||
|
type WebSocketContextType,
|
||||||
|
} from "./websocket-context";
|
||||||
|
|
||||||
interface WebSocketProviderProps {
|
interface WebSocketProviderProps {
|
||||||
url: string
|
url: string;
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, children }) => {
|
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
|
||||||
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null)
|
url,
|
||||||
const [messageHistory, setMessageHistory] = useState<{ data: string }[]>([]) // 新增历史消息状态
|
children,
|
||||||
const [connected, setConnected] = useState(false)
|
}) => {
|
||||||
const [needReconnect, setNeedReconnect] = useState(false)
|
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null);
|
||||||
const ws = useRef<WebSocket | null>(null)
|
const [messageHistory, setMessageHistory] = useState<{ data: string }[]>([]); // 新增历史消息状态
|
||||||
const reconnectTimeout = useRef<NodeJS.Timeout>(null)
|
const [connected, setConnected] = useState(false);
|
||||||
const maxReconnectAttempts = 30
|
const [needReconnect, setNeedReconnect] = useState(false);
|
||||||
const reconnectAttempts = useRef(0)
|
const ws = useRef<WebSocket | null>(null);
|
||||||
const isConnecting = useRef(false)
|
const reconnectTimeout = useRef<NodeJS.Timeout>(null);
|
||||||
|
const maxReconnectAttempts = 30;
|
||||||
|
const reconnectAttempts = useRef(0);
|
||||||
|
const isConnecting = useRef(false);
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = useCallback(() => {
|
||||||
if (ws.current) {
|
if (ws.current) {
|
||||||
// 移除所有事件监听器
|
// 移除所有事件监听器
|
||||||
ws.current.onopen = null
|
ws.current.onopen = null;
|
||||||
ws.current.onclose = null
|
ws.current.onclose = null;
|
||||||
ws.current.onmessage = null
|
ws.current.onmessage = null;
|
||||||
ws.current.onerror = null
|
ws.current.onerror = null;
|
||||||
|
|
||||||
if (ws.current.readyState === WebSocket.OPEN || ws.current.readyState === WebSocket.CONNECTING) {
|
if (
|
||||||
ws.current.close()
|
ws.current.readyState === WebSocket.OPEN ||
|
||||||
}
|
ws.current.readyState === WebSocket.CONNECTING
|
||||||
ws.current = null
|
) {
|
||||||
}
|
ws.current.close();
|
||||||
if (reconnectTimeout.current) {
|
}
|
||||||
clearTimeout(reconnectTimeout.current)
|
ws.current = null;
|
||||||
reconnectTimeout.current = null
|
}
|
||||||
}
|
if (reconnectTimeout.current) {
|
||||||
setConnected(false)
|
clearTimeout(reconnectTimeout.current);
|
||||||
}
|
reconnectTimeout.current = null;
|
||||||
|
}
|
||||||
|
setConnected(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const connect = () => {
|
const connect = useCallback(() => {
|
||||||
if (isConnecting.current) {
|
if (isConnecting.current) {
|
||||||
console.log("Connection already in progress")
|
console.log("Connection already in progress");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup()
|
cleanup();
|
||||||
isConnecting.current = true
|
isConnecting.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const wsUrl = new URL(url, window.location.origin)
|
const wsUrl = new URL(url, window.location.origin);
|
||||||
wsUrl.protocol = wsUrl.protocol.replace("http", "ws")
|
wsUrl.protocol = wsUrl.protocol.replace("http", "ws");
|
||||||
|
|
||||||
ws.current = new WebSocket(wsUrl.toString())
|
ws.current = new WebSocket(wsUrl.toString());
|
||||||
|
|
||||||
ws.current.onopen = () => {
|
ws.current.onopen = () => {
|
||||||
console.log("WebSocket connected")
|
console.log("WebSocket connected");
|
||||||
setConnected(true)
|
setConnected(true);
|
||||||
reconnectAttempts.current = 0
|
reconnectAttempts.current = 0;
|
||||||
isConnecting.current = false
|
isConnecting.current = false;
|
||||||
}
|
};
|
||||||
|
|
||||||
ws.current.onclose = () => {
|
ws.current.onclose = () => {
|
||||||
console.log("WebSocket disconnected")
|
console.log("WebSocket disconnected");
|
||||||
setConnected(false)
|
setConnected(false);
|
||||||
ws.current = null
|
ws.current = null;
|
||||||
isConnecting.current = false
|
isConnecting.current = false;
|
||||||
|
|
||||||
if (reconnectAttempts.current < maxReconnectAttempts) {
|
if (reconnectAttempts.current < maxReconnectAttempts) {
|
||||||
reconnectTimeout.current = setTimeout(() => {
|
reconnectTimeout.current = setTimeout(() => {
|
||||||
reconnectAttempts.current++
|
reconnectAttempts.current++;
|
||||||
connect()
|
connect();
|
||||||
}, 3000)
|
}, 3000);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
ws.current.onmessage = (event) => {
|
ws.current.onmessage = (event) => {
|
||||||
const newMessage = { data: event.data }
|
const newMessage = { data: event.data };
|
||||||
setLastMessage(newMessage)
|
setLastMessage(newMessage);
|
||||||
// 更新历史消息,保持最新的30条记录
|
// 更新历史消息,保持最新的30条记录
|
||||||
setMessageHistory((prev) => {
|
setMessageHistory((prev) => {
|
||||||
const updated = [newMessage, ...prev]
|
const updated = [newMessage, ...prev];
|
||||||
return updated.slice(0, 30)
|
return updated.slice(0, 30);
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
ws.current.onerror = (error) => {
|
ws.current.onerror = (error) => {
|
||||||
console.error("WebSocket error:", error)
|
console.error("WebSocket error:", error);
|
||||||
isConnecting.current = false
|
isConnecting.current = false;
|
||||||
}
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("WebSocket connection error:", error)
|
console.error("WebSocket connection error:", error);
|
||||||
isConnecting.current = false
|
isConnecting.current = false;
|
||||||
}
|
}
|
||||||
}
|
}, [cleanup, url]);
|
||||||
|
|
||||||
const reconnect = () => {
|
const reconnect = () => {
|
||||||
reconnectAttempts.current = 0
|
reconnectAttempts.current = 0;
|
||||||
// 等待一个小延时确保清理完成
|
// 等待一个小延时确保清理完成
|
||||||
cleanup()
|
cleanup();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
connect()
|
connect();
|
||||||
}, 1000)
|
}, 1000);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
connect()
|
connect();
|
||||||
|
|
||||||
// 添加页面卸载事件监听
|
// 添加页面卸载事件监听
|
||||||
const handleBeforeUnload = () => {
|
const handleBeforeUnload = () => {
|
||||||
cleanup()
|
cleanup();
|
||||||
}
|
};
|
||||||
|
|
||||||
window.addEventListener("beforeunload", handleBeforeUnload)
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cleanup()
|
cleanup();
|
||||||
window.removeEventListener("beforeunload", handleBeforeUnload)
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||||
}
|
};
|
||||||
}, [url])
|
}, [cleanup, connect]);
|
||||||
|
|
||||||
const contextValue: WebSocketContextType = {
|
const contextValue: WebSocketContextType = {
|
||||||
lastMessage,
|
lastMessage,
|
||||||
connected,
|
connected,
|
||||||
messageHistory,
|
messageHistory,
|
||||||
reconnect,
|
reconnect,
|
||||||
needReconnect,
|
needReconnect,
|
||||||
setNeedReconnect,
|
setNeedReconnect,
|
||||||
}
|
};
|
||||||
|
|
||||||
return <WebSocketContext.Provider value={contextValue}>{children}</WebSocketContext.Provider>
|
return (
|
||||||
}
|
<WebSocketContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</WebSocketContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+54
-46
@@ -1,60 +1,68 @@
|
|||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
CustomBackgroundImage: string
|
CustomBackgroundImage: string;
|
||||||
CustomMobileBackgroundImage: string
|
CustomMobileBackgroundImage: string;
|
||||||
ForceShowServices: boolean
|
ForceShowServices: boolean;
|
||||||
ForceCardInline: boolean
|
ForceCardInline: boolean;
|
||||||
ForceShowMap: boolean
|
ForceShowMap: boolean;
|
||||||
ForcePeakCutEnabled: boolean
|
ForcePeakCutEnabled: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const BACKGROUND_CHANGE_EVENT = "backgroundChange"
|
const BACKGROUND_CHANGE_EVENT = "backgroundChange";
|
||||||
|
|
||||||
export function useBackground() {
|
export function useBackground() {
|
||||||
const [backgroundImage, setBackgroundImage] = useState<string | undefined>(undefined)
|
const [backgroundImage, setBackgroundImage] = useState<string | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 监听背景变化
|
// 监听背景变化
|
||||||
const handleBackgroundChange = () => {
|
const handleBackgroundChange = () => {
|
||||||
setBackgroundImage(window.CustomBackgroundImage || undefined)
|
setBackgroundImage(window.CustomBackgroundImage || undefined);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 初始化检查
|
// 初始化检查
|
||||||
const checkInitialBackground = () => {
|
const checkInitialBackground = () => {
|
||||||
if (window.CustomBackgroundImage) {
|
if (window.CustomBackgroundImage) {
|
||||||
setBackgroundImage(window.CustomBackgroundImage)
|
setBackgroundImage(window.CustomBackgroundImage);
|
||||||
} else {
|
} else {
|
||||||
const savedImage = sessionStorage.getItem("savedBackgroundImage")
|
const savedImage = sessionStorage.getItem("savedBackgroundImage");
|
||||||
if (savedImage) {
|
if (savedImage) {
|
||||||
window.CustomBackgroundImage = savedImage
|
window.CustomBackgroundImage = savedImage;
|
||||||
setBackgroundImage(savedImage)
|
setBackgroundImage(savedImage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// 设置一个轮询来检查初始背景
|
// 设置一个轮询来检查初始背景
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
if (window.CustomBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) {
|
if (
|
||||||
checkInitialBackground()
|
window.CustomBackgroundImage ||
|
||||||
clearInterval(intervalId)
|
sessionStorage.getItem("savedBackgroundImage")
|
||||||
}
|
) {
|
||||||
}, 100)
|
checkInitialBackground();
|
||||||
|
clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
window.addEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange)
|
window.addEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange)
|
window.removeEventListener(
|
||||||
clearInterval(intervalId)
|
BACKGROUND_CHANGE_EVENT,
|
||||||
}
|
handleBackgroundChange,
|
||||||
}, [])
|
);
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const updateBackground = (newBackground: string | undefined) => {
|
const updateBackground = (newBackground: string | undefined) => {
|
||||||
window.CustomBackgroundImage = newBackground || ""
|
window.CustomBackgroundImage = newBackground || "";
|
||||||
window.dispatchEvent(new Event(BACKGROUND_CHANGE_EVENT))
|
window.dispatchEvent(new Event(BACKGROUND_CHANGE_EVENT));
|
||||||
}
|
};
|
||||||
|
|
||||||
return { backgroundImage, updateBackground }
|
return { backgroundImage, updateBackground };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
import { useEffect, useState } from "react";
|
||||||
import { useEffect, useState } from "react"
|
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
|
||||||
|
|
||||||
export function useChartHistory<T>(
|
export function useChartHistory<T>(
|
||||||
messageHistory: { data: string }[],
|
messageHistory: { data: string }[],
|
||||||
serverId: number,
|
serverId: number,
|
||||||
formatFn: (wsData: NezhaWebsocketResponse, serverId: number) => T | null,
|
formatFn: (wsData: NezhaWebsocketResponse, serverId: number) => T | null,
|
||||||
) {
|
) {
|
||||||
const [data, setData] = useState<T[]>([])
|
const [data, setData] = useState<T[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messageHistory.length > 0 && data.length === 0) {
|
if (messageHistory.length > 0 && data.length === 0) {
|
||||||
const historyData = messageHistory
|
const historyData = messageHistory
|
||||||
.map((msg) => {
|
.map((msg) => {
|
||||||
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse
|
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse;
|
||||||
return formatFn(wsData, serverId)
|
return formatFn(wsData, serverId);
|
||||||
})
|
})
|
||||||
.filter((item): item is T => item !== null)
|
.filter((item): item is T => item !== null)
|
||||||
.reverse()
|
.reverse();
|
||||||
|
|
||||||
setData(historyData)
|
setData(historyData);
|
||||||
}
|
}
|
||||||
}, [messageHistory])
|
}, [messageHistory, data.length, formatFn, serverId]);
|
||||||
|
|
||||||
return data
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { CommandContext } from "@/context/command-context";
|
||||||
|
|
||||||
|
export function useCommand() {
|
||||||
|
const context = useContext(CommandContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useCommand must be used within a CommandProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { SortContext } from "@/context/sort-context"
|
import { useContext } from "react";
|
||||||
import { useContext } from "react"
|
import { SortContext } from "@/context/sort-context";
|
||||||
|
|
||||||
export function useSort() {
|
export function useSort() {
|
||||||
const context = useContext(SortContext)
|
const context = useContext(SortContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error("useStatus must be used within a SortProvider")
|
throw new Error("useStatus must be used within a SortProvider");
|
||||||
}
|
}
|
||||||
return context
|
return context;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useContext } from "react"
|
import { useContext } from "react";
|
||||||
|
|
||||||
import { StatusContext } from "../context/status-context"
|
import { StatusContext } from "../context/status-context";
|
||||||
|
|
||||||
export function useStatus() {
|
export function useStatus() {
|
||||||
const context = useContext(StatusContext)
|
const context = useContext(StatusContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error("useStatus must be used within a StatusProvider")
|
throw new Error("useStatus must be used within a StatusProvider");
|
||||||
}
|
}
|
||||||
return context
|
return context;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useContext } from "react"
|
import { useContext } from "react";
|
||||||
|
|
||||||
import { ThemeProviderContext } from "../components/ThemeProvider"
|
import { ThemeProviderContext } from "../components/ThemeProvider";
|
||||||
|
|
||||||
export const useTheme = () => {
|
export const useTheme = () => {
|
||||||
const context = useContext(ThemeProviderContext)
|
const context = useContext(ThemeProviderContext);
|
||||||
|
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error("useTheme must be used within a ThemeProvider")
|
throw new Error("useTheme must be used within a ThemeProvider");
|
||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { TooltipContext } from "@/context/tooltip-context"
|
import { useContext } from "react";
|
||||||
import { useContext } from "react"
|
import { TooltipContext } from "@/context/tooltip-context";
|
||||||
|
|
||||||
export const useTooltip = () => {
|
export const useTooltip = () => {
|
||||||
const context = useContext(TooltipContext)
|
const context = useContext(TooltipContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error("useTooltip must be used within a TooltipProvider")
|
throw new Error("useTooltip must be used within a TooltipProvider");
|
||||||
}
|
}
|
||||||
return context
|
return context;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default useTooltip
|
export default useTooltip;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { useContext } from "react"
|
import { useContext } from "react";
|
||||||
|
|
||||||
import { WebSocketContext } from "../context/websocket-context"
|
import { WebSocketContext } from "../context/websocket-context";
|
||||||
|
|
||||||
export const useWebSocketContext = () => {
|
export const useWebSocketContext = () => {
|
||||||
const context = useContext(WebSocketContext)
|
const context = useContext(WebSocketContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error("useWebSocketContext must be used within a WebSocketProvider")
|
throw new Error(
|
||||||
}
|
"useWebSocketContext must be used within a WebSocketProvider",
|
||||||
return context
|
);
|
||||||
}
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|||||||
+43
-43
@@ -1,54 +1,54 @@
|
|||||||
import i18n from "i18next"
|
import i18n from "i18next";
|
||||||
import { initReactI18next } from "react-i18next"
|
import { initReactI18next } from "react-i18next";
|
||||||
|
|
||||||
import deTranslation from "./locales/de/translation.json"
|
import deTranslation from "./locales/de/translation.json";
|
||||||
import enTranslation from "./locales/en/translation.json"
|
import enTranslation from "./locales/en/translation.json";
|
||||||
import esTranslation from "./locales/es/translation.json"
|
import esTranslation from "./locales/es/translation.json";
|
||||||
import ruTranslation from "./locales/ru/translation.json"
|
import ruTranslation from "./locales/ru/translation.json";
|
||||||
import taTranslation from "./locales/ta/translation.json"
|
import taTranslation from "./locales/ta/translation.json";
|
||||||
import zhCNTranslation from "./locales/zh-CN/translation.json"
|
import zhCNTranslation from "./locales/zh-CN/translation.json";
|
||||||
import zhTWTranslation from "./locales/zh-TW/translation.json"
|
import zhTWTranslation from "./locales/zh-TW/translation.json";
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
"en-US": {
|
"en-US": {
|
||||||
translation: enTranslation,
|
translation: enTranslation,
|
||||||
},
|
},
|
||||||
"zh-CN": {
|
"zh-CN": {
|
||||||
translation: zhCNTranslation,
|
translation: zhCNTranslation,
|
||||||
},
|
},
|
||||||
"zh-TW": {
|
"zh-TW": {
|
||||||
translation: zhTWTranslation,
|
translation: zhTWTranslation,
|
||||||
},
|
},
|
||||||
"de-DE": {
|
"de-DE": {
|
||||||
translation: deTranslation,
|
translation: deTranslation,
|
||||||
},
|
},
|
||||||
"es-ES": {
|
"es-ES": {
|
||||||
translation: esTranslation,
|
translation: esTranslation,
|
||||||
},
|
},
|
||||||
"ru-RU": {
|
"ru-RU": {
|
||||||
translation: ruTranslation,
|
translation: ruTranslation,
|
||||||
},
|
},
|
||||||
"ta-IN": {
|
"ta-IN": {
|
||||||
translation: taTranslation,
|
translation: taTranslation,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
const getStoredLanguage = () => {
|
const getStoredLanguage = () => {
|
||||||
return localStorage.getItem("language") || "en-US"
|
return localStorage.getItem("language") || "en-US";
|
||||||
}
|
};
|
||||||
|
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
resources,
|
resources,
|
||||||
lng: getStoredLanguage(), // 使用localStorage中存储的语言或默认值
|
lng: getStoredLanguage(), // 使用localStorage中存储的语言或默认值
|
||||||
fallbackLng: "en-US", // 当前语言的翻译没有找到时,使用的备选语言
|
fallbackLng: "en-US", // 当前语言的翻译没有找到时,使用的备选语言
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false, // react已经安全地转义
|
escapeValue: false, // react已经安全地转义
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
// 添加语言改变时的处理函数
|
// 添加语言改变时的处理函数
|
||||||
i18n.on("languageChanged", (lng) => {
|
i18n.on("languageChanged", (lng) => {
|
||||||
localStorage.setItem("language", lng)
|
localStorage.setItem("language", lng);
|
||||||
})
|
});
|
||||||
|
|
||||||
export default i18n
|
export default i18n;
|
||||||
|
|||||||
+337
-164
@@ -1,234 +1,407 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
@import url("https://cdn.jsdelivr.net/npm/lxgw-wenkai-screen-webfont@1.7.0/style.css");
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
:root {
|
@plugin "tailwindcss-animate";
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
@custom-variant dark (&:is(.dark *));
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
@theme {
|
||||||
text-rendering: optimizeLegibility;
|
--font-sans: var(--font-sans);
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
--radius-lg: var(--radius);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
|
||||||
|
--color-background: hsl(var(--background));
|
||||||
|
--color-foreground: hsl(var(--foreground));
|
||||||
|
|
||||||
|
--color-card: hsl(var(--card));
|
||||||
|
--color-card-foreground: hsl(var(--card-foreground));
|
||||||
|
|
||||||
|
--color-popover: hsl(var(--popover));
|
||||||
|
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||||
|
|
||||||
|
--color-primary: hsl(var(--primary));
|
||||||
|
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||||
|
|
||||||
|
--color-secondary: hsl(var(--secondary));
|
||||||
|
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||||
|
|
||||||
|
--color-muted: hsl(var(--muted));
|
||||||
|
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||||
|
|
||||||
|
--color-accent: hsl(var(--accent));
|
||||||
|
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||||
|
|
||||||
|
--color-destructive: hsl(var(--destructive));
|
||||||
|
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||||
|
|
||||||
|
--color-border: hsl(var(--border));
|
||||||
|
--color-input: hsl(var(--input));
|
||||||
|
--color-ring: hsl(var(--ring));
|
||||||
|
|
||||||
|
--color-chart-1: hsl(var(--chart-1));
|
||||||
|
--color-chart-2: hsl(var(--chart-2));
|
||||||
|
--color-chart-3: hsl(var(--chart-3));
|
||||||
|
--color-chart-4: hsl(var(--chart-4));
|
||||||
|
--color-chart-5: hsl(var(--chart-5));
|
||||||
|
|
||||||
|
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||||
|
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||||
|
|
||||||
|
@keyframes accordion-down {
|
||||||
|
from {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes accordion-up {
|
||||||
|
from {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
/*
|
||||||
:root {
|
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||||
--background: 0 0% 98%;
|
so we've added these compatibility styles to make sure everything still
|
||||||
--foreground: 20 14.3% 4.1%;
|
looks the same as it did with Tailwind CSS v3.
|
||||||
--card: 0 0% 100%;
|
|
||||||
--card-foreground: 20 14.3% 4.1%;
|
|
||||||
--popover: 0 0% 100%;
|
|
||||||
--popover-foreground: 20 14.3% 4.1%;
|
|
||||||
--primary: 24 9.8% 10%;
|
|
||||||
--primary-foreground: 60 9.1% 97.8%;
|
|
||||||
--secondary: 60 4.8% 95.9%;
|
|
||||||
--secondary-foreground: 24 9.8% 10%;
|
|
||||||
--muted: 60 4.8% 95.9%;
|
|
||||||
--muted-foreground: 25 5.3% 44.7%;
|
|
||||||
--accent: 60 4.8% 95.9%;
|
|
||||||
--accent-foreground: 24 9.8% 10%;
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
|
||||||
--destructive-foreground: 60 9.1% 97.8%;
|
|
||||||
--border: 20 5.9% 90%;
|
|
||||||
--input: 20 5.9% 90%;
|
|
||||||
--ring: 20 14.3% 4.1%;
|
|
||||||
--radius: 1rem;
|
|
||||||
--chart-1: 220 70% 50%;
|
|
||||||
--chart-2: 340 75% 55%;
|
|
||||||
--chart-3: 30 80% 55%;
|
|
||||||
--chart-4: 280 65% 60%;
|
|
||||||
--chart-5: 160 60% 45%;
|
|
||||||
--chart-6: 180 50% 50%;
|
|
||||||
--chart-7: 216 50% 50%;
|
|
||||||
--chart-8: 252 50% 50%;
|
|
||||||
--chart-9: 288 50% 50%;
|
|
||||||
--chart-10: 324 50% 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
If we ever want to remove these styles, we need to add an explicit border
|
||||||
--background: 30 15% 8%;
|
color utility to any element that depends on these defaults.
|
||||||
--foreground: 60 9.1% 97.8%;
|
*/
|
||||||
--card: 20 14.3% 4.1%;
|
@layer base {
|
||||||
--card-foreground: 60 9.1% 97.8%;
|
*,
|
||||||
--popover: 20 14.3% 4.1%;
|
::after,
|
||||||
--popover-foreground: 60 9.1% 97.8%;
|
::before,
|
||||||
--primary: 60 9.1% 97.8%;
|
::backdrop,
|
||||||
--primary-foreground: 24 9.8% 10%;
|
::file-selector-button {
|
||||||
--secondary: 12 6.5% 15.1%;
|
border-color: var(--color-gray-200, currentcolor);
|
||||||
--secondary-foreground: 60 9.1% 97.8%;
|
}
|
||||||
--muted: 12 6.5% 15.1%;
|
|
||||||
--muted-foreground: 24 5.4% 63.9%;
|
|
||||||
--accent: 12 6.5% 15.1%;
|
|
||||||
--accent-foreground: 60 9.1% 97.8%;
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
|
||||||
--destructive-foreground: 60 9.1% 97.8%;
|
|
||||||
--border: 12 6.5% 15.1%;
|
|
||||||
--input: 12 6.5% 15.1%;
|
|
||||||
--ring: 24 5.7% 82.9%;
|
|
||||||
--chart-1: 220 70% 50%;
|
|
||||||
--chart-2: 340 75% 55%;
|
|
||||||
--chart-3: 30 80% 55%;
|
|
||||||
--chart-4: 280 65% 60%;
|
|
||||||
--chart-5: 160 60% 45%;
|
|
||||||
--chart-6: 180 50% 50%;
|
|
||||||
--chart-7: 216 50% 50%;
|
|
||||||
--chart-8: 252 50% 50%;
|
|
||||||
--chart-9: 288 50% 50%;
|
|
||||||
--chart-10: 324 50% 50%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@utility step {
|
||||||
* {
|
counter-increment: step;
|
||||||
@apply border-border;
|
|
||||||
}
|
&:before {
|
||||||
html {
|
@apply border-background bg-muted absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 text-center -indent-px font-mono text-base font-medium;
|
||||||
@apply scroll-smooth;
|
@apply mt-[-4px] ml-[-50px];
|
||||||
}
|
content: counter(step);
|
||||||
body {
|
}
|
||||||
@apply bg-background text-foreground;
|
|
||||||
/* font-feature-settings: "rlig" 1, "calt" 1; */
|
|
||||||
font-synthesis-weight: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.step {
|
:root {
|
||||||
counter-increment: step;
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
}
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
.step:before {
|
color-scheme: light dark;
|
||||||
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
|
color: rgba(255, 255, 255, 0.87);
|
||||||
@apply ml-[-50px] mt-[-4px];
|
background-color: #242424;
|
||||||
content: counter(step);
|
|
||||||
}
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
button:not(:disabled),
|
||||||
|
[role="button"]:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 98%;
|
||||||
|
--foreground: 20 14.3% 4.1%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 20 14.3% 4.1%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 20 14.3% 4.1%;
|
||||||
|
--primary: 24 9.8% 10%;
|
||||||
|
--primary-foreground: 60 9.1% 97.8%;
|
||||||
|
--secondary: 60 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 24 9.8% 10%;
|
||||||
|
--muted: 60 4.8% 95.9%;
|
||||||
|
--muted-foreground: 25 5.3% 44.7%;
|
||||||
|
--accent: 60 4.8% 95.9%;
|
||||||
|
--accent-foreground: 24 9.8% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
--border: 20 5.9% 90%;
|
||||||
|
--input: 20 5.9% 90%;
|
||||||
|
--ring: 20 14.3% 4.1%;
|
||||||
|
--radius: 1rem;
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 340 75% 55%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 160 60% 45%;
|
||||||
|
--chart-6: 180 50% 50%;
|
||||||
|
--chart-7: 216 50% 50%;
|
||||||
|
--chart-8: 252 50% 50%;
|
||||||
|
--chart-9: 288 50% 50%;
|
||||||
|
--chart-10: 324 50% 50%;
|
||||||
|
--timing: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 30 15% 8%;
|
||||||
|
--foreground: 60 9.1% 97.8%;
|
||||||
|
--card: 20 14.3% 4.1%;
|
||||||
|
--card-foreground: 60 9.1% 97.8%;
|
||||||
|
--popover: 20 14.3% 4.1%;
|
||||||
|
--popover-foreground: 60 9.1% 97.8%;
|
||||||
|
--primary: 60 9.1% 97.8%;
|
||||||
|
--primary-foreground: 24 9.8% 10%;
|
||||||
|
--secondary: 12 6.5% 15.1%;
|
||||||
|
--secondary-foreground: 60 9.1% 97.8%;
|
||||||
|
--muted: 12 6.5% 15.1%;
|
||||||
|
--muted-foreground: 24 5.4% 63.9%;
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 60 9.1% 97.8%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
--border: 12 6.5% 15.1%;
|
||||||
|
--input: 12 6.5% 15.1%;
|
||||||
|
--ring: 24 5.7% 82.9%;
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 340 75% 55%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 160 60% 45%;
|
||||||
|
--chart-6: 180 50% 50%;
|
||||||
|
--chart-7: 216 50% 50%;
|
||||||
|
--chart-8: 252 50% 50%;
|
||||||
|
--chart-9: 288 50% 50%;
|
||||||
|
--chart-10: 324 50% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
@apply scroll-smooth;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
/* font-feature-settings: "rlig" 1, "calt" 1; */
|
||||||
|
font-synthesis-weight: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
font-family: 'LXGW WenKai Screen', sans-serif;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: 'LXGW WenKai Screen', sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
/* Avoid color fade when toggling themes. */
|
||||||
|
html.disable-transitions *,
|
||||||
|
html.disable-transitions *::before,
|
||||||
|
html.disable-transitions *::after {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.container {
|
.container {
|
||||||
@apply px-4;
|
@apply px-4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
@apply bg-stone-300 dark:bg-stone-800;
|
@apply bg-stone-300 dark:bg-stone-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamster-loading-wrapper {
|
.hamster-loading-wrapper {
|
||||||
--size: 12px;
|
--size: 12px;
|
||||||
height: var(--size);
|
height: var(--size);
|
||||||
width: var(--size);
|
width: var(--size);
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamster-loading-wrapper[data-visible="false"] {
|
.hamster-loading-wrapper[data-visible="false"] {
|
||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
animation: hamster-fade-out 0.2s ease forwards;
|
animation: hamster-fade-out 0.2s ease forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamster-spinner {
|
.hamster-spinner {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
height: var(--size);
|
height: var(--size);
|
||||||
width: var(--size);
|
width: var(--size);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamster-loading-bar {
|
.hamster-loading-bar {
|
||||||
--gray11: hsl(0, 0%, 43.5%);
|
--gray11: hsl(0, 0%, 43.5%);
|
||||||
animation: hamster-spin 0.8s linear infinite;
|
animation: hamster-spin 0.8s linear infinite;
|
||||||
background: var(--gray11);
|
background: var(--gray11);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
height: 13%;
|
height: 13%;
|
||||||
left: -10%;
|
left: -10%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -3.9%;
|
top: -3.9%;
|
||||||
width: 30%;
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamster-loading-bar:nth-child(1) {
|
.hamster-loading-bar:nth-child(1) {
|
||||||
animation-delay: -0.8s;
|
animation-delay: -0.8s;
|
||||||
transform: rotate(0deg) translate(120%);
|
transform: rotate(0deg) translate(120%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamster-loading-bar:nth-child(2) {
|
.hamster-loading-bar:nth-child(2) {
|
||||||
animation-delay: -0.7s;
|
animation-delay: -0.7s;
|
||||||
transform: rotate(45deg) translate(120%);
|
transform: rotate(45deg) translate(120%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamster-loading-bar:nth-child(3) {
|
.hamster-loading-bar:nth-child(3) {
|
||||||
animation-delay: -0.6s;
|
animation-delay: -0.6s;
|
||||||
transform: rotate(90deg) translate(120%);
|
transform: rotate(90deg) translate(120%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamster-loading-bar:nth-child(4) {
|
.hamster-loading-bar:nth-child(4) {
|
||||||
animation-delay: -0.5s;
|
animation-delay: -0.5s;
|
||||||
transform: rotate(135deg) translate(120%);
|
transform: rotate(135deg) translate(120%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamster-loading-bar:nth-child(5) {
|
.hamster-loading-bar:nth-child(5) {
|
||||||
animation-delay: -0.4s;
|
animation-delay: -0.4s;
|
||||||
transform: rotate(180deg) translate(120%);
|
transform: rotate(180deg) translate(120%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamster-loading-bar:nth-child(6) {
|
.hamster-loading-bar:nth-child(6) {
|
||||||
animation-delay: -0.3s;
|
animation-delay: -0.3s;
|
||||||
transform: rotate(225deg) translate(120%);
|
transform: rotate(225deg) translate(120%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamster-loading-bar:nth-child(7) {
|
.hamster-loading-bar:nth-child(7) {
|
||||||
animation-delay: -0.2s;
|
animation-delay: -0.2s;
|
||||||
transform: rotate(270deg) translate(120%);
|
transform: rotate(270deg) translate(120%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamster-loading-bar:nth-child(8) {
|
.hamster-loading-bar:nth-child(8) {
|
||||||
animation-delay: -0.1s;
|
animation-delay: -0.1s;
|
||||||
transform: rotate(315deg) translate(120%);
|
transform: rotate(315deg) translate(120%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes hamster-fade-in {
|
@keyframes hamster-fade-in {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.8);
|
transform: scale(0.8);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes hamster-fade-out {
|
@keyframes hamster-fade-out {
|
||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.8);
|
transform: scale(0.8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes hamster-spin {
|
@keyframes hamster-spin {
|
||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
opacity: 0.15;
|
opacity: 0.15;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-hidden {
|
.scrollbar-hidden {
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-hidden::-webkit-scrollbar {
|
.scrollbar-hidden::-webkit-scrollbar {
|
||||||
display: none; /* Chrome, Safari 和 Opera */
|
display: none; /* Chrome, Safari 和 Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thanks to next.js. */
|
||||||
|
[data-issues-count-animation] {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-issues-count-animation] > div {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-issues-count-exit].animate {
|
||||||
|
animation: fadeOut 300ms var(--timing) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-issues-count-enter].animate {
|
||||||
|
animation: fadeIn 300ms var(--timing) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-issues-count-plural] {
|
||||||
|
display: inline-block;
|
||||||
|
animation: fadeIn 300ms var(--timing) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(2px);
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
filter: blur(0px);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
filter: blur(0px);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-12px);
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom background overlays */
|
||||||
|
.bg-cover::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: backdrop-filter 0.3s ease, background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .bg-cover::after {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
background: rgba(0, 0, 0, .6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light .bg-cover::after {
|
||||||
|
backdrop-filter: blur(0);
|
||||||
|
background: rgba(255, 255, 255, .3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
|
export function initCustomConfig() {
|
||||||
|
try {
|
||||||
|
const hour = DateTime.now().hour;
|
||||||
|
const isNight = hour >= 18 || hour < 6;
|
||||||
|
|
||||||
|
// Use default values if window variables are not already set (e.g. by backend custom_code)
|
||||||
|
// although the goal is to hardcode these for "consistency".
|
||||||
|
|
||||||
|
window.CustomBackgroundImage = isNight
|
||||||
|
? 'https://loohui.com/wp-content/uploads/images/background.jpg'
|
||||||
|
: 'https://loohui.com/wp-content/uploads/images/background_day.jpg';
|
||||||
|
|
||||||
|
window.CustomMobileBackgroundImage = window.CustomBackgroundImage;
|
||||||
|
window.ForceTheme = isNight ? 'dark' : 'light';
|
||||||
|
|
||||||
|
/* LOGO / 副标题 / 链接 */
|
||||||
|
window.CustomLogo = 'https://loohui.com/wp-content/uploads/images/pet.png';
|
||||||
|
window.CustomDesc = '树树皆秋色,山山唯落晖';
|
||||||
|
window.CustomLinks = JSON.stringify([
|
||||||
|
{ "link": "https://loohui.com/", "name": "返回Blog", "blank": false }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Handle internal redirects if needed
|
||||||
|
document.addEventListener('click', (e: MouseEvent) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const a = target.closest('a');
|
||||||
|
if (a && a.href === 'https://loohui.com/') {
|
||||||
|
e.preventDefault();
|
||||||
|
window.location.href = a.href;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Nezha custom_config] crash:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-6
@@ -1,11 +1,21 @@
|
|||||||
export function formatBytes(bytes: number, decimals: number = 2) {
|
export function formatBytes(bytes: number, decimals: number = 2) {
|
||||||
if (!+bytes) return "0 Bytes"
|
if (!+bytes) return "0 Bytes";
|
||||||
|
|
||||||
const k = 1024
|
const k = 1024;
|
||||||
const dm = decimals < 0 ? 0 : decimals
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
const sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
|
const sizes = [
|
||||||
|
"Bytes",
|
||||||
|
"KiB",
|
||||||
|
"MiB",
|
||||||
|
"GiB",
|
||||||
|
"TiB",
|
||||||
|
"PiB",
|
||||||
|
"EiB",
|
||||||
|
"ZiB",
|
||||||
|
"YiB",
|
||||||
|
];
|
||||||
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
|
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+207
-203
@@ -1,208 +1,212 @@
|
|||||||
export const countryCoordinates: Record<string, { lat: number; lng: number; name: string }> = {
|
export const countryCoordinates: Record<
|
||||||
// 亚洲
|
string,
|
||||||
AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗
|
{ lat: number; lng: number; name: string }
|
||||||
AM: { lat: 40.0, lng: 45.0, name: "Armenia" }, // 亚美尼亚
|
> = {
|
||||||
AZ: { lat: 40.5, lng: 47.5, name: "Azerbaijan" }, // 阿塞拜疆
|
// 亚洲
|
||||||
BD: { lat: 24.0, lng: 90.0, name: "Bangladesh" }, // 孟加拉国
|
AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗
|
||||||
BH: { lat: 26.0, lng: 50.55, name: "Bahrain" }, // 巴林
|
AM: { lat: 40.0, lng: 45.0, name: "Armenia" }, // 亚美尼亚
|
||||||
BT: { lat: 27.5, lng: 90.5, name: "Bhutan" }, // 不丹
|
AZ: { lat: 40.5, lng: 47.5, name: "Azerbaijan" }, // 阿塞拜疆
|
||||||
BN: { lat: 4.5, lng: 114.6667, name: "Brunei" }, // 文莱
|
BD: { lat: 24.0, lng: 90.0, name: "Bangladesh" }, // 孟加拉国
|
||||||
KH: { lat: 13.0, lng: 105.0, name: "Cambodia" }, // 柬埔寨
|
BH: { lat: 26.0, lng: 50.55, name: "Bahrain" }, // 巴林
|
||||||
CN: { lat: 35.0, lng: 105.0, name: "China" }, // 中国
|
BT: { lat: 27.5, lng: 90.5, name: "Bhutan" }, // 不丹
|
||||||
HK: { lat: 22.0, lng: 114.0, name: "Hong Kong" }, // 香港
|
BN: { lat: 4.5, lng: 114.6667, name: "Brunei" }, // 文莱
|
||||||
CY: { lat: 35.0, lng: 33.0, name: "Cyprus" }, // 塞浦路斯
|
KH: { lat: 13.0, lng: 105.0, name: "Cambodia" }, // 柬埔寨
|
||||||
GE: { lat: 42.0, lng: 43.5, name: "Georgia" }, // 格鲁吉亚
|
CN: { lat: 35.0, lng: 105.0, name: "China" }, // 中国
|
||||||
IN: { lat: 20.0, lng: 77.0, name: "India" }, // 印度
|
HK: { lat: 22.0, lng: 114.0, name: "Hong Kong" }, // 香港
|
||||||
ID: { lat: -5.0, lng: 120.0, name: "Indonesia" }, // 印度尼西亚
|
MO: { lat: 22.1667, lng: 113.55, name: "Macau" }, // 澳门
|
||||||
IR: { lat: 32.0, lng: 53.0, name: "Iran" }, // 伊朗
|
CY: { lat: 35.0, lng: 33.0, name: "Cyprus" }, // 塞浦路斯
|
||||||
IQ: { lat: 33.0, lng: 44.0, name: "Iraq" }, // 伊拉克
|
GE: { lat: 42.0, lng: 43.5, name: "Georgia" }, // 格鲁吉亚
|
||||||
IL: { lat: 31.5, lng: 34.75, name: "Israel" }, // 以色列
|
IN: { lat: 20.0, lng: 77.0, name: "India" }, // 印度
|
||||||
JP: { lat: 36.0, lng: 138.0, name: "Japan" }, // 日本
|
ID: { lat: -5.0, lng: 120.0, name: "Indonesia" }, // 印度尼西亚
|
||||||
JO: { lat: 31.0, lng: 36.0, name: "Jordan" }, // 约旦
|
IR: { lat: 32.0, lng: 53.0, name: "Iran" }, // 伊朗
|
||||||
KZ: { lat: 48.0, lng: 68.0, name: "Kazakhstan" }, // 哈萨克斯坦
|
IQ: { lat: 33.0, lng: 44.0, name: "Iraq" }, // 伊拉克
|
||||||
KW: { lat: 29.3375, lng: 47.6581, name: "Kuwait" }, // 科威特
|
IL: { lat: 31.5, lng: 34.75, name: "Israel" }, // 以色列
|
||||||
KG: { lat: 41.0, lng: 75.0, name: "Kyrgyzstan" }, // 吉尔吉斯斯坦
|
JP: { lat: 36.0, lng: 138.0, name: "Japan" }, // 日本
|
||||||
LA: { lat: 18.0, lng: 105.0, name: "Laos" }, // 老挝
|
JO: { lat: 31.0, lng: 36.0, name: "Jordan" }, // 约旦
|
||||||
LB: { lat: 33.8333, lng: 35.8333, name: "Lebanon" }, // 黎巴嫩
|
KZ: { lat: 48.0, lng: 68.0, name: "Kazakhstan" }, // 哈萨克斯坦
|
||||||
MY: { lat: 2.5, lng: 112.5, name: "Malaysia" }, // 马来西亚
|
KW: { lat: 29.3375, lng: 47.6581, name: "Kuwait" }, // 科威特
|
||||||
MV: { lat: 3.25, lng: 73.0, name: "Maldives" }, // 马尔代夫
|
KG: { lat: 41.0, lng: 75.0, name: "Kyrgyzstan" }, // 吉尔吉斯斯坦
|
||||||
MN: { lat: 46.0, lng: 105.0, name: "Mongolia" }, // 蒙古
|
LA: { lat: 18.0, lng: 105.0, name: "Laos" }, // 老挝
|
||||||
MM: { lat: 22.0, lng: 98.0, name: "Myanmar" }, // 缅甸
|
LB: { lat: 33.8333, lng: 35.8333, name: "Lebanon" }, // 黎巴嫩
|
||||||
NP: { lat: 28.0, lng: 84.0, name: "Nepal" }, // 尼泊尔
|
MY: { lat: 2.5, lng: 112.5, name: "Malaysia" }, // 马来西亚
|
||||||
OM: { lat: 21.0, lng: 57.0, name: "Oman" }, // 阿曼
|
MV: { lat: 3.25, lng: 73.0, name: "Maldives" }, // 马尔代夫
|
||||||
PK: { lat: 30.0, lng: 70.0, name: "Pakistan" }, // 巴基斯坦
|
MN: { lat: 46.0, lng: 105.0, name: "Mongolia" }, // 蒙古
|
||||||
PH: { lat: 13.0, lng: 122.0, name: "Philippines" }, // 菲律宾
|
MM: { lat: 22.0, lng: 98.0, name: "Myanmar" }, // 缅甸
|
||||||
QA: { lat: 25.5, lng: 51.25, name: "Qatar" }, // 卡塔尔
|
NP: { lat: 28.0, lng: 84.0, name: "Nepal" }, // 尼泊尔
|
||||||
SA: { lat: 25.0, lng: 45.0, name: "Saudi Arabia" }, // 沙特阿拉伯
|
OM: { lat: 21.0, lng: 57.0, name: "Oman" }, // 阿曼
|
||||||
SG: { lat: 1.3667, lng: 103.8, name: "Singapore" }, // 新加坡
|
PK: { lat: 30.0, lng: 70.0, name: "Pakistan" }, // 巴基斯坦
|
||||||
KR: { lat: 37.0, lng: 127.5, name: "South Korea" }, // 韩国
|
PH: { lat: 13.0, lng: 122.0, name: "Philippines" }, // 菲律宾
|
||||||
LK: { lat: 7.0, lng: 81.0, name: "Sri Lanka" }, // 斯里兰卡
|
QA: { lat: 25.5, lng: 51.25, name: "Qatar" }, // 卡塔尔
|
||||||
SY: { lat: 35.0, lng: 38.0, name: "Syria" }, // 叙利亚
|
SA: { lat: 25.0, lng: 45.0, name: "Saudi Arabia" }, // 沙特阿拉伯
|
||||||
TW: { lat: 23.5, lng: 121.0, name: "Taiwan" }, // 台湾
|
SG: { lat: 1.3667, lng: 103.8, name: "Singapore" }, // 新加坡
|
||||||
TJ: { lat: 39.0, lng: 71.0, name: "Tajikistan" }, // 塔吉克斯坦
|
KR: { lat: 37.0, lng: 127.5, name: "South Korea" }, // 韩国
|
||||||
TH: { lat: 15.0, lng: 100.0, name: "Thailand" }, // 泰国
|
LK: { lat: 7.0, lng: 81.0, name: "Sri Lanka" }, // 斯里兰卡
|
||||||
TR: { lat: 39.0, lng: 35.0, name: "Turkey" }, // 土耳其
|
SY: { lat: 35.0, lng: 38.0, name: "Syria" }, // 叙利亚
|
||||||
TM: { lat: 40.0, lng: 60.0, name: "Turkmenistan" }, // 土库曼斯坦
|
TW: { lat: 23.5, lng: 121.0, name: "Taiwan" }, // 台湾
|
||||||
AE: { lat: 24.0, lng: 54.0, name: "United Arab Emirates" }, // 阿联酋
|
TJ: { lat: 39.0, lng: 71.0, name: "Tajikistan" }, // 塔吉克斯坦
|
||||||
UZ: { lat: 41.0, lng: 64.0, name: "Uzbekistan" }, // 乌兹别克斯坦
|
TH: { lat: 15.0, lng: 100.0, name: "Thailand" }, // 泰国
|
||||||
VN: { lat: 16.0, lng: 106.0, name: "Vietnam" }, // 越南
|
TR: { lat: 39.0, lng: 35.0, name: "Turkey" }, // 土耳其
|
||||||
YE: { lat: 15.0, lng: 48.0, name: "Yemen" }, // 也门
|
TM: { lat: 40.0, lng: 60.0, name: "Turkmenistan" }, // 土库曼斯坦
|
||||||
PS: { lat: 32.0, lng: 35.25, name: "Palestine" }, // 巴勒斯坦
|
AE: { lat: 24.0, lng: 54.0, name: "United Arab Emirates" }, // 阿联酋
|
||||||
|
UZ: { lat: 41.0, lng: 64.0, name: "Uzbekistan" }, // 乌兹别克斯坦
|
||||||
|
VN: { lat: 16.0, lng: 106.0, name: "Vietnam" }, // 越南
|
||||||
|
YE: { lat: 15.0, lng: 48.0, name: "Yemen" }, // 也门
|
||||||
|
PS: { lat: 32.0, lng: 35.25, name: "Palestine" }, // 巴勒斯坦
|
||||||
|
|
||||||
// 欧洲
|
// 欧洲
|
||||||
AL: { lat: 41.0, lng: 20.0, name: "Albania" }, // 阿尔巴尼亚
|
AL: { lat: 41.0, lng: 20.0, name: "Albania" }, // 阿尔巴尼亚
|
||||||
AD: { lat: 42.5, lng: 1.6, name: "Andorra" }, // 安道尔
|
AD: { lat: 42.5, lng: 1.6, name: "Andorra" }, // 安道尔
|
||||||
AT: { lat: 47.3333, lng: 13.3333, name: "Austria" }, // 奥地利
|
AT: { lat: 47.3333, lng: 13.3333, name: "Austria" }, // 奥地利
|
||||||
BY: { lat: 53.0, lng: 28.0, name: "Belarus" }, // 白俄罗斯
|
BY: { lat: 53.0, lng: 28.0, name: "Belarus" }, // 白俄罗斯
|
||||||
BE: { lat: 50.8333, lng: 4.0, name: "Belgium" }, // 比利时
|
BE: { lat: 50.8333, lng: 4.0, name: "Belgium" }, // 比利时
|
||||||
BA: { lat: 44.0, lng: 18.0, name: "Bosnia and Herzegovina" }, // 波黑
|
BA: { lat: 44.0, lng: 18.0, name: "Bosnia and Herzegovina" }, // 波黑
|
||||||
BG: { lat: 43.0, lng: 25.0, name: "Bulgaria" }, // 保加利亚
|
BG: { lat: 43.0, lng: 25.0, name: "Bulgaria" }, // 保加利亚
|
||||||
HR: { lat: 45.1667, lng: 15.5, name: "Croatia" }, // 克罗地亚
|
HR: { lat: 45.1667, lng: 15.5, name: "Croatia" }, // 克罗地亚
|
||||||
CZ: { lat: 49.75, lng: 15.5, name: "Czech Republic" }, // 捷克
|
CZ: { lat: 49.75, lng: 15.5, name: "Czech Republic" }, // 捷克
|
||||||
DK: { lat: 56.0, lng: 10.0, name: "Denmark" }, // 丹麦
|
DK: { lat: 56.0, lng: 10.0, name: "Denmark" }, // 丹麦
|
||||||
EE: { lat: 59.0, lng: 26.0, name: "Estonia" }, // 爱沙尼亚
|
EE: { lat: 59.0, lng: 26.0, name: "Estonia" }, // 爱沙尼亚
|
||||||
FI: { lat: 64.0, lng: 26.0, name: "Finland" }, // 芬兰
|
FI: { lat: 64.0, lng: 26.0, name: "Finland" }, // 芬兰
|
||||||
FR: { lat: 46.0, lng: 2.0, name: "France" }, // 法国
|
FR: { lat: 46.0, lng: 2.0, name: "France" }, // 法国
|
||||||
DE: { lat: 51.0, lng: 9.0, name: "Germany" }, // 德国
|
DE: { lat: 51.0, lng: 9.0, name: "Germany" }, // 德国
|
||||||
GR: { lat: 39.0, lng: 22.0, name: "Greece" }, // 希腊
|
GR: { lat: 39.0, lng: 22.0, name: "Greece" }, // 希腊
|
||||||
HU: { lat: 47.0, lng: 20.0, name: "Hungary" }, // 匈牙利
|
HU: { lat: 47.0, lng: 20.0, name: "Hungary" }, // 匈牙利
|
||||||
IS: { lat: 65.0, lng: -18.0, name: "Iceland" }, // 冰岛
|
IS: { lat: 65.0, lng: -18.0, name: "Iceland" }, // 冰岛
|
||||||
IE: { lat: 53.0, lng: -8.0, name: "Ireland" }, // 爱尔兰
|
IE: { lat: 53.0, lng: -8.0, name: "Ireland" }, // 爱尔兰
|
||||||
IT: { lat: 42.8333, lng: 12.8333, name: "Italy" }, // 意大利
|
IT: { lat: 42.8333, lng: 12.8333, name: "Italy" }, // 意大利
|
||||||
LV: { lat: 57.0, lng: 25.0, name: "Latvia" }, // 拉脱维亚
|
LV: { lat: 57.0, lng: 25.0, name: "Latvia" }, // 拉脱维亚
|
||||||
LI: { lat: 47.1667, lng: 9.5333, name: "Liechtenstein" }, // 列支敦士登
|
LI: { lat: 47.1667, lng: 9.5333, name: "Liechtenstein" }, // 列支敦士登
|
||||||
LT: { lat: 56.0, lng: 24.0, name: "Lithuania" }, // 立陶宛
|
LT: { lat: 56.0, lng: 24.0, name: "Lithuania" }, // 立陶宛
|
||||||
LU: { lat: 49.75, lng: 6.1667, name: "Luxembourg" }, // 卢森堡
|
LU: { lat: 49.75, lng: 6.1667, name: "Luxembourg" }, // 卢森堡
|
||||||
MT: { lat: 35.8333, lng: 14.5833, name: "Malta" }, // 马耳他
|
MT: { lat: 35.8333, lng: 14.5833, name: "Malta" }, // 马耳他
|
||||||
MD: { lat: 47.0, lng: 29.0, name: "Moldova" }, // 摩尔多瓦
|
MD: { lat: 47.0, lng: 29.0, name: "Moldova" }, // 摩尔多瓦
|
||||||
MC: { lat: 43.7333, lng: 7.4, name: "Monaco" }, // 摩纳哥
|
MC: { lat: 43.7333, lng: 7.4, name: "Monaco" }, // 摩纳哥
|
||||||
ME: { lat: 42.0, lng: 19.0, name: "Montenegro" }, // 黑山
|
ME: { lat: 42.0, lng: 19.0, name: "Montenegro" }, // 黑山
|
||||||
NL: { lat: 52.5, lng: 5.75, name: "Netherlands" }, // 荷兰
|
NL: { lat: 52.5, lng: 5.75, name: "Netherlands" }, // 荷兰
|
||||||
NO: { lat: 62.0, lng: 10.0, name: "Norway" }, // 挪威
|
NO: { lat: 62.0, lng: 10.0, name: "Norway" }, // 挪威
|
||||||
PL: { lat: 52.0, lng: 20.0, name: "Poland" }, // 波兰
|
PL: { lat: 52.0, lng: 20.0, name: "Poland" }, // 波兰
|
||||||
PT: { lat: 39.5, lng: -8.0, name: "Portugal" }, // 葡萄牙
|
PT: { lat: 39.5, lng: -8.0, name: "Portugal" }, // 葡萄牙
|
||||||
RO: { lat: 46.0, lng: 25.0, name: "Romania" }, // 罗马尼亚
|
RO: { lat: 46.0, lng: 25.0, name: "Romania" }, // 罗马尼亚
|
||||||
RU: { lat: 60.0, lng: 100.0, name: "Russia" }, // 俄罗斯
|
RU: { lat: 60.0, lng: 100.0, name: "Russia" }, // 俄罗斯
|
||||||
SM: { lat: 43.7667, lng: 12.4167, name: "San Marino" }, // 圣马力诺
|
SM: { lat: 43.7667, lng: 12.4167, name: "San Marino" }, // 圣马力诺
|
||||||
RS: { lat: 44.0, lng: 21.0, name: "Serbia" }, // 塞尔维亚
|
RS: { lat: 44.0, lng: 21.0, name: "Serbia" }, // 塞尔维亚
|
||||||
SK: { lat: 48.6667, lng: 19.5, name: "Slovakia" }, // 斯洛伐克
|
SK: { lat: 48.6667, lng: 19.5, name: "Slovakia" }, // 斯洛伐克
|
||||||
SI: { lat: 46.0, lng: 15.0, name: "Slovenia" }, // 斯洛文尼亚
|
SI: { lat: 46.0, lng: 15.0, name: "Slovenia" }, // 斯洛文尼亚
|
||||||
ES: { lat: 40.0, lng: -4.0, name: "Spain" }, // 西班牙
|
ES: { lat: 40.0, lng: -4.0, name: "Spain" }, // 西班牙
|
||||||
SE: { lat: 62.0, lng: 15.0, name: "Sweden" }, // 瑞典
|
SE: { lat: 62.0, lng: 15.0, name: "Sweden" }, // 瑞典
|
||||||
CH: { lat: 47.0, lng: 8.0, name: "Switzerland" }, // 瑞士
|
CH: { lat: 47.0, lng: 8.0, name: "Switzerland" }, // 瑞士
|
||||||
UA: { lat: 49.0, lng: 32.0, name: "Ukraine" }, // 乌克兰
|
UA: { lat: 49.0, lng: 32.0, name: "Ukraine" }, // 乌克兰
|
||||||
GB: { lat: 54.0, lng: -2.0, name: "United Kingdom" }, // 英国
|
GB: { lat: 54.0, lng: -2.0, name: "United Kingdom" }, // 英国
|
||||||
VA: { lat: 41.9, lng: 12.45, name: "Vatican City" }, // 梵蒂冈
|
VA: { lat: 41.9, lng: 12.45, name: "Vatican City" }, // 梵蒂冈
|
||||||
|
|
||||||
// 北美洲
|
// 北美洲
|
||||||
AG: { lat: 17.05, lng: -61.8, name: "Antigua and Barbuda" }, // 安提瓜和巴布达
|
AG: { lat: 17.05, lng: -61.8, name: "Antigua and Barbuda" }, // 安提瓜和巴布达
|
||||||
BS: { lat: 24.25, lng: -76.0, name: "Bahamas" }, // 巴哈马
|
BS: { lat: 24.25, lng: -76.0, name: "Bahamas" }, // 巴哈马
|
||||||
BB: { lat: 13.1667, lng: -59.5333, name: "Barbados" }, // 巴巴多斯
|
BB: { lat: 13.1667, lng: -59.5333, name: "Barbados" }, // 巴巴多斯
|
||||||
BZ: { lat: 17.25, lng: -88.75, name: "Belize" }, // 伯利兹
|
BZ: { lat: 17.25, lng: -88.75, name: "Belize" }, // 伯利兹
|
||||||
CA: { lat: 60.0, lng: -95.0, name: "Canada" }, // 加拿大
|
CA: { lat: 60.0, lng: -95.0, name: "Canada" }, // 加拿大
|
||||||
CR: { lat: 10.0, lng: -84.0, name: "Costa Rica" }, // 哥斯达黎加
|
CR: { lat: 10.0, lng: -84.0, name: "Costa Rica" }, // 哥斯达黎加
|
||||||
CU: { lat: 21.5, lng: -80.0, name: "Cuba" }, // 古巴
|
CU: { lat: 21.5, lng: -80.0, name: "Cuba" }, // 古巴
|
||||||
DM: { lat: 15.4167, lng: -61.3333, name: "Dominica" }, // 多米尼克
|
DM: { lat: 15.4167, lng: -61.3333, name: "Dominica" }, // 多米尼克
|
||||||
DO: { lat: 19.0, lng: -70.6667, name: "Dominican Republic" }, // 多米尼加共和国
|
DO: { lat: 19.0, lng: -70.6667, name: "Dominican Republic" }, // 多米尼加共和国
|
||||||
SV: { lat: 13.8333, lng: -88.9167, name: "El Salvador" }, // 萨尔瓦多
|
SV: { lat: 13.8333, lng: -88.9167, name: "El Salvador" }, // 萨尔瓦多
|
||||||
GD: { lat: 12.1167, lng: -61.6667, name: "Grenada" }, // 格林纳达
|
GD: { lat: 12.1167, lng: -61.6667, name: "Grenada" }, // 格林纳达
|
||||||
GT: { lat: 15.5, lng: -90.25, name: "Guatemala" }, // 危地马拉
|
GT: { lat: 15.5, lng: -90.25, name: "Guatemala" }, // 危地马拉
|
||||||
HT: { lat: 19.0, lng: -72.4167, name: "Haiti" }, // 海地
|
HT: { lat: 19.0, lng: -72.4167, name: "Haiti" }, // 海地
|
||||||
HN: { lat: 15.0, lng: -86.5, name: "Honduras" }, // 洪都拉斯
|
HN: { lat: 15.0, lng: -86.5, name: "Honduras" }, // 洪都拉斯
|
||||||
JM: { lat: 18.25, lng: -77.5, name: "Jamaica" }, // 牙买加
|
JM: { lat: 18.25, lng: -77.5, name: "Jamaica" }, // 牙买加
|
||||||
MX: { lat: 23.0, lng: -102.0, name: "Mexico" }, // 墨西哥
|
MX: { lat: 23.0, lng: -102.0, name: "Mexico" }, // 墨西哥
|
||||||
NI: { lat: 13.0, lng: -85.0, name: "Nicaragua" }, // 尼加拉瓜
|
NI: { lat: 13.0, lng: -85.0, name: "Nicaragua" }, // 尼加拉瓜
|
||||||
PA: { lat: 9.0, lng: -80.0, name: "Panama" }, // 巴拿马
|
PA: { lat: 9.0, lng: -80.0, name: "Panama" }, // 巴拿马
|
||||||
KN: { lat: 17.3333, lng: -62.75, name: "Saint Kitts and Nevis" }, // 圣基茨和尼维斯
|
KN: { lat: 17.3333, lng: -62.75, name: "Saint Kitts and Nevis" }, // 圣基茨和尼维斯
|
||||||
LC: { lat: 13.8833, lng: -61.1333, name: "Saint Lucia" }, // 圣卢西亚
|
LC: { lat: 13.8833, lng: -61.1333, name: "Saint Lucia" }, // 圣卢西亚
|
||||||
VC: { lat: 13.25, lng: -61.2, name: "Saint Vincent and the Grenadines" }, // 圣文森特和格林纳丁斯
|
VC: { lat: 13.25, lng: -61.2, name: "Saint Vincent and the Grenadines" }, // 圣文森特和格林纳丁斯
|
||||||
TT: { lat: 11.0, lng: -61.0, name: "Trinidad and Tobago" }, // 特立尼达和多巴哥
|
TT: { lat: 11.0, lng: -61.0, name: "Trinidad and Tobago" }, // 特立尼达和多巴哥
|
||||||
US: { lat: 38.0, lng: -97.0, name: "United States" }, // 美国
|
US: { lat: 38.0, lng: -97.0, name: "United States" }, // 美国
|
||||||
|
|
||||||
// 南美洲
|
// 南美洲
|
||||||
AR: { lat: -34.0, lng: -64.0, name: "Argentina" }, // 阿根廷
|
AR: { lat: -34.0, lng: -64.0, name: "Argentina" }, // 阿根廷
|
||||||
BO: { lat: -17.0, lng: -65.0, name: "Bolivia" }, // 玻利维亚
|
BO: { lat: -17.0, lng: -65.0, name: "Bolivia" }, // 玻利维亚
|
||||||
BR: { lat: -10.0, lng: -55.0, name: "Brazil" }, // 巴西
|
BR: { lat: -10.0, lng: -55.0, name: "Brazil" }, // 巴西
|
||||||
CL: { lat: -30.0, lng: -71.0, name: "Chile" }, // 智利
|
CL: { lat: -30.0, lng: -71.0, name: "Chile" }, // 智利
|
||||||
CO: { lat: 4.0, lng: -72.0, name: "Colombia" }, // 哥伦比亚
|
CO: { lat: 4.0, lng: -72.0, name: "Colombia" }, // 哥伦比亚
|
||||||
EC: { lat: -2.0, lng: -77.5, name: "Ecuador" }, // 厄瓜多尔
|
EC: { lat: -2.0, lng: -77.5, name: "Ecuador" }, // 厄瓜多尔
|
||||||
GY: { lat: 5.0, lng: -59.0, name: "Guyana" }, // 圭亚那
|
GY: { lat: 5.0, lng: -59.0, name: "Guyana" }, // 圭亚那
|
||||||
PY: { lat: -23.0, lng: -58.0, name: "Paraguay" }, // 巴拉圭
|
PY: { lat: -23.0, lng: -58.0, name: "Paraguay" }, // 巴拉圭
|
||||||
PE: { lat: -10.0, lng: -76.0, name: "Peru" }, // 秘鲁
|
PE: { lat: -10.0, lng: -76.0, name: "Peru" }, // 秘鲁
|
||||||
SR: { lat: 4.0, lng: -56.0, name: "Suriname" }, // 苏里南
|
SR: { lat: 4.0, lng: -56.0, name: "Suriname" }, // 苏里南
|
||||||
UY: { lat: -33.0, lng: -56.0, name: "Uruguay" }, // 乌拉圭
|
UY: { lat: -33.0, lng: -56.0, name: "Uruguay" }, // 乌拉圭
|
||||||
VE: { lat: 8.0, lng: -66.0, name: "Venezuela" }, // 委内瑞拉
|
VE: { lat: 8.0, lng: -66.0, name: "Venezuela" }, // 委内瑞拉
|
||||||
|
|
||||||
// 大洋洲
|
// 大洋洲
|
||||||
AU: { lat: -27.0, lng: 133.0, name: "Australia" }, // 澳大利亚
|
AU: { lat: -27.0, lng: 133.0, name: "Australia" }, // 澳大利亚
|
||||||
FJ: { lat: -18.0, lng: 175.0, name: "Fiji" }, // 斐济
|
FJ: { lat: -18.0, lng: 175.0, name: "Fiji" }, // 斐济
|
||||||
KI: { lat: 1.4167, lng: 173.0, name: "Kiribati" }, // 基里巴斯
|
KI: { lat: 1.4167, lng: 173.0, name: "Kiribati" }, // 基里巴斯
|
||||||
MH: { lat: 9.0, lng: 168.0, name: "Marshall Islands" }, // 马绍尔群岛
|
MH: { lat: 9.0, lng: 168.0, name: "Marshall Islands" }, // 马绍尔群岛
|
||||||
FM: { lat: 6.9167, lng: 158.25, name: "Micronesia" }, // 密克罗尼西亚
|
FM: { lat: 6.9167, lng: 158.25, name: "Micronesia" }, // 密克罗尼西亚
|
||||||
NR: { lat: -0.5333, lng: 166.9167, name: "Nauru" }, // 瑙鲁
|
NR: { lat: -0.5333, lng: 166.9167, name: "Nauru" }, // 瑙鲁
|
||||||
NZ: { lat: -41.0, lng: 174.0, name: "New Zealand" }, // 新西兰
|
NZ: { lat: -41.0, lng: 174.0, name: "New Zealand" }, // 新西兰
|
||||||
PW: { lat: 7.5, lng: 134.5, name: "Palau" }, // 帕劳
|
PW: { lat: 7.5, lng: 134.5, name: "Palau" }, // 帕劳
|
||||||
PG: { lat: -6.0, lng: 147.0, name: "Papua New Guinea" }, // 巴布亚新几内亚
|
PG: { lat: -6.0, lng: 147.0, name: "Papua New Guinea" }, // 巴布亚新几内亚
|
||||||
WS: { lat: -13.5833, lng: -172.3333, name: "Samoa" }, // 萨摩亚
|
WS: { lat: -13.5833, lng: -172.3333, name: "Samoa" }, // 萨摩亚
|
||||||
SB: { lat: -8.0, lng: 159.0, name: "Solomon Islands" }, // 所罗门群岛
|
SB: { lat: -8.0, lng: 159.0, name: "Solomon Islands" }, // 所罗门群岛
|
||||||
TO: { lat: -20.0, lng: -175.0, name: "Tonga" }, // 汤加
|
TO: { lat: -20.0, lng: -175.0, name: "Tonga" }, // 汤加
|
||||||
TV: { lat: -8.0, lng: 178.0, name: "Tuvalu" }, // 图瓦卢
|
TV: { lat: -8.0, lng: 178.0, name: "Tuvalu" }, // 图瓦卢
|
||||||
VU: { lat: -16.0, lng: 167.0, name: "Vanuatu" }, // 瓦努阿图
|
VU: { lat: -16.0, lng: 167.0, name: "Vanuatu" }, // 瓦努阿图
|
||||||
|
|
||||||
// 非洲
|
// 非洲
|
||||||
DZ: { lat: 28.0, lng: 3.0, name: "Algeria" }, // 阿尔及利亚
|
DZ: { lat: 28.0, lng: 3.0, name: "Algeria" }, // 阿尔及利亚
|
||||||
AO: { lat: -12.5, lng: 18.5, name: "Angola" }, // 安哥拉
|
AO: { lat: -12.5, lng: 18.5, name: "Angola" }, // 安哥拉
|
||||||
BJ: { lat: 9.5, lng: 2.25, name: "Benin" }, // 贝宁
|
BJ: { lat: 9.5, lng: 2.25, name: "Benin" }, // 贝宁
|
||||||
BW: { lat: -22.0, lng: 24.0, name: "Botswana" }, // 博茨瓦纳
|
BW: { lat: -22.0, lng: 24.0, name: "Botswana" }, // 博茨瓦纳
|
||||||
BF: { lat: 13.0, lng: -2.0, name: "Burkina Faso" }, // 布基纳法索
|
BF: { lat: 13.0, lng: -2.0, name: "Burkina Faso" }, // 布基纳法索
|
||||||
BI: { lat: -3.5, lng: 30.0, name: "Burundi" }, // 布隆迪
|
BI: { lat: -3.5, lng: 30.0, name: "Burundi" }, // 布隆迪
|
||||||
CM: { lat: 6.0, lng: 12.0, name: "Cameroon" }, // 喀麦隆
|
CM: { lat: 6.0, lng: 12.0, name: "Cameroon" }, // 喀麦隆
|
||||||
CV: { lat: 16.0, lng: -24.0, name: "Cape Verde" }, // 佛得角
|
CV: { lat: 16.0, lng: -24.0, name: "Cape Verde" }, // 佛得角
|
||||||
CF: { lat: 7.0, lng: 21.0, name: "Central African Republic" }, // 中非共和国
|
CF: { lat: 7.0, lng: 21.0, name: "Central African Republic" }, // 中非共和国
|
||||||
TD: { lat: 15.0, lng: 19.0, name: "Chad" }, // 乍得
|
TD: { lat: 15.0, lng: 19.0, name: "Chad" }, // 乍得
|
||||||
KM: { lat: -12.1667, lng: 44.25, name: "Comoros" }, // 科摩罗
|
KM: { lat: -12.1667, lng: 44.25, name: "Comoros" }, // 科摩罗
|
||||||
CG: { lat: -1.0, lng: 15.0, name: "Congo" }, // 刚果
|
CG: { lat: -1.0, lng: 15.0, name: "Congo" }, // 刚果
|
||||||
CD: { lat: 0.0, lng: 25.0, name: "Democratic Republic of the Congo" }, // 刚果民主共和国
|
CD: { lat: 0.0, lng: 25.0, name: "Democratic Republic of the Congo" }, // 刚果民主共和国
|
||||||
CI: { lat: 8.0, lng: -5.0, name: "Côte d'Ivoire" }, // 科特迪瓦
|
CI: { lat: 8.0, lng: -5.0, name: "Côte d'Ivoire" }, // 科特迪瓦
|
||||||
DJ: { lat: 11.5, lng: 43.0, name: "Djibouti" }, // 吉布提
|
DJ: { lat: 11.5, lng: 43.0, name: "Djibouti" }, // 吉布提
|
||||||
EG: { lat: 27.0, lng: 30.0, name: "Egypt" }, // 埃及
|
EG: { lat: 27.0, lng: 30.0, name: "Egypt" }, // 埃及
|
||||||
GQ: { lat: 2.0, lng: 10.0, name: "Equatorial Guinea" }, // 赤道几内亚
|
GQ: { lat: 2.0, lng: 10.0, name: "Equatorial Guinea" }, // 赤道几内亚
|
||||||
ER: { lat: 15.0, lng: 39.0, name: "Eritrea" }, // 厄立特里亚
|
ER: { lat: 15.0, lng: 39.0, name: "Eritrea" }, // 厄立特里亚
|
||||||
ET: { lat: 8.0, lng: 38.0, name: "Ethiopia" }, // 埃塞俄比亚
|
ET: { lat: 8.0, lng: 38.0, name: "Ethiopia" }, // 埃塞俄比亚
|
||||||
GA: { lat: -1.0, lng: 11.75, name: "Gabon" }, // 加蓬
|
GA: { lat: -1.0, lng: 11.75, name: "Gabon" }, // 加蓬
|
||||||
GM: { lat: 13.4667, lng: -16.5667, name: "Gambia" }, // 冈比亚
|
GM: { lat: 13.4667, lng: -16.5667, name: "Gambia" }, // 冈比亚
|
||||||
GH: { lat: 8.0, lng: -2.0, name: "Ghana" }, // 加纳
|
GH: { lat: 8.0, lng: -2.0, name: "Ghana" }, // 加纳
|
||||||
GN: { lat: 11.0, lng: -10.0, name: "Guinea" }, // 几内亚
|
GN: { lat: 11.0, lng: -10.0, name: "Guinea" }, // 几内亚
|
||||||
GW: { lat: 12.0, lng: -15.0, name: "Guinea-Bissau" }, // 几内亚比绍
|
GW: { lat: 12.0, lng: -15.0, name: "Guinea-Bissau" }, // 几内亚比绍
|
||||||
KE: { lat: 1.0, lng: 38.0, name: "Kenya" }, // 肯尼亚
|
KE: { lat: 1.0, lng: 38.0, name: "Kenya" }, // 肯尼亚
|
||||||
LS: { lat: -29.5, lng: 28.5, name: "Lesotho" }, // 莱索托
|
LS: { lat: -29.5, lng: 28.5, name: "Lesotho" }, // 莱索托
|
||||||
LR: { lat: 6.5, lng: -9.5, name: "Liberia" }, // 利比里亚
|
LR: { lat: 6.5, lng: -9.5, name: "Liberia" }, // 利比里亚
|
||||||
LY: { lat: 25.0, lng: 17.0, name: "Libya" }, // 利比亚
|
LY: { lat: 25.0, lng: 17.0, name: "Libya" }, // 利比亚
|
||||||
MG: { lat: -20.0, lng: 47.0, name: "Madagascar" }, // 马达加斯加
|
MG: { lat: -20.0, lng: 47.0, name: "Madagascar" }, // 马达加斯加
|
||||||
MW: { lat: -13.5, lng: 34.0, name: "Malawi" }, // 马拉维
|
MW: { lat: -13.5, lng: 34.0, name: "Malawi" }, // 马拉维
|
||||||
ML: { lat: 17.0, lng: -4.0, name: "Mali" }, // 马里
|
ML: { lat: 17.0, lng: -4.0, name: "Mali" }, // 马里
|
||||||
MR: { lat: 20.0, lng: -12.0, name: "Mauritania" }, // 毛里塔尼亚
|
MR: { lat: 20.0, lng: -12.0, name: "Mauritania" }, // 毛里塔尼亚
|
||||||
MU: { lat: -20.2833, lng: 57.55, name: "Mauritius" }, // 毛里求斯
|
MU: { lat: -20.2833, lng: 57.55, name: "Mauritius" }, // 毛里求斯
|
||||||
YT: { lat: -12.8333, lng: 45.1667, name: "Mayotte" }, // 马约特
|
YT: { lat: -12.8333, lng: 45.1667, name: "Mayotte" }, // 马约特
|
||||||
MA: { lat: 32.0, lng: -5.0, name: "Morocco" }, // 摩洛哥
|
MA: { lat: 32.0, lng: -5.0, name: "Morocco" }, // 摩洛哥
|
||||||
MZ: { lat: -18.25, lng: 35.0, name: "Mozambique" }, // 莫桑比克
|
MZ: { lat: -18.25, lng: 35.0, name: "Mozambique" }, // 莫桑比克
|
||||||
NA: { lat: -22.0, lng: 17.0, name: "Namibia" }, // 纳米比亚
|
NA: { lat: -22.0, lng: 17.0, name: "Namibia" }, // 纳米比亚
|
||||||
NE: { lat: 16.0, lng: 8.0, name: "Niger" }, // 尼日尔
|
NE: { lat: 16.0, lng: 8.0, name: "Niger" }, // 尼日尔
|
||||||
NG: { lat: 10.0, lng: 8.0, name: "Nigeria" }, // 尼日利亚
|
NG: { lat: 10.0, lng: 8.0, name: "Nigeria" }, // 尼日利亚
|
||||||
RW: { lat: -2.0, lng: 30.0, name: "Rwanda" }, // 卢旺达
|
RW: { lat: -2.0, lng: 30.0, name: "Rwanda" }, // 卢旺达
|
||||||
ST: { lat: 1.0, lng: 7.0, name: "São Tomé and Principe" }, // 圣多美和普林西比
|
ST: { lat: 1.0, lng: 7.0, name: "São Tomé and Principe" }, // 圣多美和普林西比
|
||||||
SN: { lat: 14.0, lng: -14.0, name: "Senegal" }, // 塞内加尔
|
SN: { lat: 14.0, lng: -14.0, name: "Senegal" }, // 塞内加尔
|
||||||
SC: { lat: -4.5833, lng: 55.6667, name: "Seychelles" }, // 塞舌尔
|
SC: { lat: -4.5833, lng: 55.6667, name: "Seychelles" }, // 塞舌尔
|
||||||
SL: { lat: 8.5, lng: -11.5, name: "Sierra Leone" }, // 塞拉利昂
|
SL: { lat: 8.5, lng: -11.5, name: "Sierra Leone" }, // 塞拉利昂
|
||||||
SO: { lat: 10.0, lng: 49.0, name: "Somalia" }, // 索马里
|
SO: { lat: 10.0, lng: 49.0, name: "Somalia" }, // 索马里
|
||||||
ZA: { lat: -29.0, lng: 24.0, name: "South Africa" }, // 南非
|
ZA: { lat: -29.0, lng: 24.0, name: "South Africa" }, // 南非
|
||||||
SD: { lat: 15.0, lng: 30.0, name: "Sudan" }, // 苏丹
|
SD: { lat: 15.0, lng: 30.0, name: "Sudan" }, // 苏丹
|
||||||
SZ: { lat: -26.5, lng: 31.5, name: "Swaziland" }, // 斯威士兰
|
SZ: { lat: -26.5, lng: 31.5, name: "Swaziland" }, // 斯威士兰
|
||||||
TZ: { lat: -6.0, lng: 35.0, name: "Tanzania" }, // 坦桑尼亚
|
TZ: { lat: -6.0, lng: 35.0, name: "Tanzania" }, // 坦桑尼亚
|
||||||
TG: { lat: 8.0, lng: 1.1667, name: "Togo" }, // 多哥
|
TG: { lat: 8.0, lng: 1.1667, name: "Togo" }, // 多哥
|
||||||
TN: { lat: 34.0, lng: 9.0, name: "Tunisia" }, // 突尼斯
|
TN: { lat: 34.0, lng: 9.0, name: "Tunisia" }, // 突尼斯
|
||||||
UG: { lat: 1.0, lng: 32.0, name: "Uganda" }, // 乌干达
|
UG: { lat: 1.0, lng: 32.0, name: "Uganda" }, // 乌干达
|
||||||
EH: { lat: 24.5, lng: -13.0, name: "Western Sahara" }, // 西撒哈拉
|
EH: { lat: 24.5, lng: -13.0, name: "Western Sahara" }, // 西撒哈拉
|
||||||
ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚
|
ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚
|
||||||
ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦
|
ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦
|
||||||
}
|
};
|
||||||
|
|||||||
+97
-89
@@ -1,99 +1,107 @@
|
|||||||
export const InjectContext = (content: string) => {
|
export const InjectContext = (content: string) => {
|
||||||
const tempDiv = document.createElement("div")
|
const tempDiv = document.createElement("div");
|
||||||
tempDiv.innerHTML = content
|
tempDiv.innerHTML = content;
|
||||||
|
|
||||||
const INJECTION_MARK = "data-injected" // 自定义属性标识
|
const INJECTION_MARK = "data-injected"; // 自定义属性标识
|
||||||
|
|
||||||
// 清理已有的注入资源
|
// 清理已有的注入资源
|
||||||
const cleanInjectedResources = () => {
|
const cleanInjectedResources = () => {
|
||||||
document.querySelectorAll(`[${INJECTION_MARK}]`).forEach((node) => node.remove())
|
document.querySelectorAll(`[${INJECTION_MARK}]`).forEach((node) => {
|
||||||
}
|
node.remove();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const loadExternalScript = (scriptElement: HTMLScriptElement): Promise<void> => {
|
const loadExternalScript = (
|
||||||
return new Promise((resolve, reject) => {
|
scriptElement: HTMLScriptElement,
|
||||||
const script = document.createElement("script")
|
): Promise<void> => {
|
||||||
script.src = scriptElement.src
|
return new Promise((resolve, reject) => {
|
||||||
script.async = false // 保持顺序执行
|
const script = document.createElement("script");
|
||||||
script.setAttribute(INJECTION_MARK, "true") // 添加标识
|
script.src = scriptElement.src;
|
||||||
script.onload = () => resolve()
|
script.async = false; // 保持顺序执行
|
||||||
script.onerror = () => reject(new Error(`Failed to load script: ${scriptElement.src}`))
|
script.setAttribute(INJECTION_MARK, "true"); // 添加标识
|
||||||
document.head.appendChild(script)
|
script.onload = () => resolve();
|
||||||
})
|
script.onerror = () =>
|
||||||
}
|
reject(new Error(`Failed to load script: ${scriptElement.src}`));
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const executeInlineScript = (scriptContent: string): Promise<void> => {
|
const executeInlineScript = (scriptContent: string): Promise<void> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const script = document.createElement("script")
|
const script = document.createElement("script");
|
||||||
script.textContent = scriptContent
|
script.textContent = scriptContent;
|
||||||
script.setAttribute(INJECTION_MARK, "true") // 添加标识
|
script.setAttribute(INJECTION_MARK, "true"); // 添加标识
|
||||||
document.body.appendChild(script)
|
document.body.appendChild(script);
|
||||||
resolve()
|
resolve();
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const loadStyle = (styleElement: HTMLStyleElement): Promise<void> => {
|
const loadStyle = (styleElement: HTMLStyleElement): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if ((styleElement as any).href) {
|
if ((styleElement as any).href) {
|
||||||
// 处理 <link>
|
// 处理 <link>
|
||||||
const link = document.createElement("link")
|
const link = document.createElement("link");
|
||||||
link.rel = "stylesheet"
|
link.rel = "stylesheet";
|
||||||
link.href = (styleElement as any).href
|
link.href = (styleElement as any).href;
|
||||||
link.setAttribute(INJECTION_MARK, "true") // 添加标识
|
link.setAttribute(INJECTION_MARK, "true"); // 添加标识
|
||||||
link.onload = () => resolve()
|
link.onload = () => resolve();
|
||||||
link.onerror = () => reject(new Error(`Failed to load stylesheet: ${link.href}`))
|
link.onerror = () =>
|
||||||
document.head.appendChild(link)
|
reject(new Error(`Failed to load stylesheet: ${link.href}`));
|
||||||
} else {
|
document.head.appendChild(link);
|
||||||
const style = document.createElement("style")
|
} else {
|
||||||
style.textContent = styleElement.textContent
|
const style = document.createElement("style");
|
||||||
style.setAttribute(INJECTION_MARK, "true") // 添加标识
|
style.textContent = styleElement.textContent;
|
||||||
document.head.appendChild(style)
|
style.setAttribute(INJECTION_MARK, "true"); // 添加标识
|
||||||
resolve()
|
document.head.appendChild(style);
|
||||||
}
|
resolve();
|
||||||
})
|
}
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handlers: { [key: string]: (element: HTMLElement) => Promise<void> } = {
|
const handlers: { [key: string]: (element: HTMLElement) => Promise<void> } = {
|
||||||
SCRIPT: (element) => {
|
SCRIPT: (element) => {
|
||||||
const scriptElement = element as HTMLScriptElement
|
const scriptElement = element as HTMLScriptElement;
|
||||||
if (scriptElement.src) {
|
if (scriptElement.src) {
|
||||||
// 加载外部脚本
|
// 加载外部脚本
|
||||||
return loadExternalScript(scriptElement)
|
return loadExternalScript(scriptElement);
|
||||||
} else {
|
} else {
|
||||||
// 执行内联脚本
|
// 执行内联脚本
|
||||||
return executeInlineScript(scriptElement.textContent || "")
|
return executeInlineScript(scriptElement.textContent || "");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
STYLE: (element) => loadStyle(element as HTMLStyleElement),
|
STYLE: (element) => loadStyle(element as HTMLStyleElement),
|
||||||
META: (element) => {
|
META: (element) => {
|
||||||
const meta = element.cloneNode(true) as HTMLElement
|
const meta = element.cloneNode(true) as HTMLElement;
|
||||||
meta.setAttribute(INJECTION_MARK, "true") // 添加标识
|
meta.setAttribute(INJECTION_MARK, "true"); // 添加标识
|
||||||
document.head.appendChild(meta) // 将 meta 标签插入到 <head>
|
document.head.appendChild(meta); // 将 meta 标签插入到 <head>
|
||||||
return Promise.resolve()
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
DEFAULT: (element) => {
|
DEFAULT: (element) => {
|
||||||
element.setAttribute(INJECTION_MARK, "true") // 添加标识
|
element.setAttribute(INJECTION_MARK, "true"); // 添加标识
|
||||||
document.body.appendChild(element)
|
document.body.appendChild(element);
|
||||||
return Promise.resolve()
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
// 开始注入前清理已有资源
|
// 开始注入前清理已有资源
|
||||||
cleanInjectedResources()
|
cleanInjectedResources();
|
||||||
|
|
||||||
const executeSequentially = async () => {
|
const executeSequentially = async () => {
|
||||||
for (const node of Array.from(tempDiv.childNodes)) {
|
for (const node of Array.from(tempDiv.childNodes)) {
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
const element = node as HTMLElement
|
const element = node as HTMLElement;
|
||||||
const handler = handlers[element.tagName] || handlers.DEFAULT
|
const handler = handlers[element.tagName] || handlers.DEFAULT;
|
||||||
await handler(element) // 按顺序等待当前脚本或资源完成处理
|
await handler(element); // 按顺序等待当前脚本或资源完成处理
|
||||||
} else if (node.nodeType === Node.TEXT_NODE) {
|
} else if (node.nodeType === Node.TEXT_NODE) {
|
||||||
document.body.appendChild(document.createTextNode(node.textContent || ""))
|
document.body.appendChild(
|
||||||
}
|
document.createTextNode(node.textContent || ""),
|
||||||
}
|
);
|
||||||
console.log("All resources have been injected and executed in sequence.")
|
}
|
||||||
}
|
}
|
||||||
|
console.log("All resources have been injected and executed in sequence.");
|
||||||
|
};
|
||||||
|
|
||||||
return executeSequentially().catch((error) => {
|
return executeSequentially().catch((error) => {
|
||||||
console.error("Error during resource injection:", error)
|
console.error("Error during resource injection:", error);
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user