Compare commits

...

52 Commits

Author SHA1 Message Date
Bot 42f62f7d5b feat: synchronize UI branding with backend settings dynamically 2026-04-16 17:18:52 +08:00
Bot b668063c52 fix: resolve theme switching conflicts and layout shaking by refining Font CSS and ForceTheme logic 2026-04-16 17:10:54 +08:00
Bot cb436904ea feat: integrate custom branding, time-based themes, and LXGW font directly into frontend 2026-04-16 16:59:01 +08:00
Bot 191d44bfd0 fix: remove duplicate upSpeed field in ServerOverview type 2026-04-16 16:25:08 +08:00
Bot cc1678a21e Enhance domain remaining progress bar color logic 2026-04-16 16:21:24 +08:00
Buriburizaemon 6398f73f7d Merge upstream v2 with Domain tracking 2026-04-16 11:57:15 +08:00
hamster1963 2dcf6fb9d8 fix: remove unused prerelease condition from GitHub Actions workflow 2026-04-02 17:31:50 +08:00
hamster1963 9a21a664c9 fix: add draft condition for release based on tag naming conventions 2026-04-02 17:29:48 +08:00
Weblate (bot) ffb6102211 Translations update from Hosted Weblate (#62)
* Translated using Weblate (Ukrainian)

Currently translated at 2.9% (3 of 103 strings)

Added translation using Weblate (Ukrainian)

Added translation using Weblate (Japanese)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Zilong Liu <2821624044@qq.com>
Co-authored-by: Руслан Пузич <visp80@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/nezha/user-frontend/uk/
Translation: Nezha/User frontend

* Translated using Weblate (Ukrainian)

Currently translated at 11.6% (12 of 103 strings)

Translation: Nezha/User frontend
Translate-URL: https://hosted.weblate.org/projects/nezha/user-frontend/uk/

* Added translation using Weblate (Galician)

* Translated using Weblate (Galician)

Currently translated at 100.0% (103 of 103 strings)

Translation: Nezha/User frontend
Translate-URL: https://hosted.weblate.org/projects/nezha/user-frontend/gl/

* Translated using Weblate (Ukrainian)

Currently translated at 50.4% (52 of 103 strings)

Translation: Nezha/User frontend
Translate-URL: https://hosted.weblate.org/projects/nezha/user-frontend/uk/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (103 of 103 strings)

Translation: Nezha/User frontend
Translate-URL: https://hosted.weblate.org/projects/nezha/user-frontend/id/

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: Zilong Liu <2821624044@qq.com>
Co-authored-by: Руслан Пузич <visp80@gmail.com>
Co-authored-by: nlimeres <dynosaurioprogramador@gmail.com>
Co-authored-by: Arif Budiman <arifpedia@gmail.com>
Co-authored-by: weblate <1607653+weblate@users.noreply.github.com>
Co-authored-by: 仓鼠 <71394853+hamster1963@users.noreply.github.com>
2026-04-02 17:19:50 +08:00
hamster1963 f7662d2751 chore: add CI workflow and update dependencies in package.json and vite.config.ts 2026-04-02 17:17:16 +08:00
Copilot e4ba96ea76 fix: default network chart period to 1d instead of 30d (#61)
* Initial plan

* fix: change network chart default period from 30d to 1d

Co-authored-by: hamster1963 <71394853+hamster1963@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hamster1963 <71394853+hamster1963@users.noreply.github.com>
2026-03-10 15:30:42 +08:00
hamster1963 d7f4f61001 chore: bump version to 2.0.2 in package.json 2026-02-25 17:22:27 +08:00
胡说丷刂 eceadb6bff fix: Update service to use display_index as the main sort order (#60)
* fix: Update service to use display_index as the main sort order

* chore: auto-fix linting and formatting issues

* Update src/types/nezha-api.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: huYang <306061454@qq.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-25 17:20:42 +08:00
Weblate (bot) b030cd45d6 Translations update from Hosted Weblate (#59)
* Translated using Weblate (Ukrainian)

Currently translated at 2.9% (3 of 103 strings)

Added translation using Weblate (Ukrainian)

Added translation using Weblate (Japanese)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Zilong Liu <2821624044@qq.com>
Co-authored-by: Руслан Пузич <visp80@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/nezha/user-frontend/uk/
Translation: Nezha/User frontend

* Translated using Weblate (Ukrainian)

Currently translated at 11.6% (12 of 103 strings)

Translation: Nezha/User frontend
Translate-URL: https://hosted.weblate.org/projects/nezha/user-frontend/uk/

* Added translation using Weblate (Galician)

* Translated using Weblate (Galician)

Currently translated at 100.0% (103 of 103 strings)

Translation: Nezha/User frontend
Translate-URL: https://hosted.weblate.org/projects/nezha/user-frontend/gl/

* Translated using Weblate (Ukrainian)

Currently translated at 50.4% (52 of 103 strings)

Translation: Nezha/User frontend
Translate-URL: https://hosted.weblate.org/projects/nezha/user-frontend/uk/

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: Zilong Liu <2821624044@qq.com>
Co-authored-by: Руслан Пузич <visp80@gmail.com>
Co-authored-by: nlimeres <dynosaurioprogramador@gmail.com>
Co-authored-by: weblate <1607653+weblate@users.noreply.github.com>
Co-authored-by: 仓鼠 <71394853+hamster1963@users.noreply.github.com>
2026-02-22 13:50:33 +08:00
Weblate (bot) 3be914cecc Translations update from Hosted Weblate (#58)
* Translated using Weblate (Ukrainian)

Currently translated at 2.9% (3 of 103 strings)

Added translation using Weblate (Ukrainian)

Added translation using Weblate (Japanese)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Zilong Liu <2821624044@qq.com>
Co-authored-by: Руслан Пузич <visp80@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/nezha/user-frontend/uk/
Translation: Nezha/User frontend

* Translated using Weblate (Ukrainian)

Currently translated at 11.6% (12 of 103 strings)

Translation: Nezha/User frontend
Translate-URL: https://hosted.weblate.org/projects/nezha/user-frontend/uk/

* Added translation using Weblate (Galician)

* Translated using Weblate (Galician)

Currently translated at 100.0% (103 of 103 strings)

Translation: Nezha/User frontend
Translate-URL: https://hosted.weblate.org/projects/nezha/user-frontend/gl/

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: Zilong Liu <2821624044@qq.com>
Co-authored-by: Руслан Пузич <visp80@gmail.com>
Co-authored-by: nlimeres <dynosaurioprogramador@gmail.com>
Co-authored-by: weblate <1607653+weblate@users.noreply.github.com>
Co-authored-by: 仓鼠 <71394853+hamster1963@users.noreply.github.com>
2026-02-18 00:26:17 +08:00
Weblate (bot) d82c4c4aeb Translations update from Hosted Weblate (#57)
* Translated using Weblate (Ukrainian)

Currently translated at 2.9% (3 of 103 strings)

Added translation using Weblate (Ukrainian)

Added translation using Weblate (Japanese)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Zilong Liu <2821624044@qq.com>
Co-authored-by: Руслан Пузич <visp80@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/nezha/user-frontend/uk/
Translation: Nezha/User frontend

* Translated using Weblate (Ukrainian)

Currently translated at 11.6% (12 of 103 strings)

Translation: Nezha/User frontend
Translate-URL: https://hosted.weblate.org/projects/nezha/user-frontend/uk/

* Added translation using Weblate (Galician)

* chore: auto-fix linting and formatting issues

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: Zilong Liu <2821624044@qq.com>
Co-authored-by: Руслан Пузич <visp80@gmail.com>
Co-authored-by: nlimeres <dynosaurioprogramador@gmail.com>
Co-authored-by: weblate <1607653+weblate@users.noreply.github.com>
Co-authored-by: 仓鼠 <71394853+hamster1963@users.noreply.github.com>
2026-02-17 17:42:56 +08:00
hamster1963 7b085915d9 chore: bump version to 2.0.1 in package.json 2026-02-16 12:36:22 +08:00
hamster1963 e700ef40d5 chore: update react-router-dom to version ^7.13.0 in package.json and pnpm-lock.yaml 2026-02-16 12:34:50 +08:00
dependabot[bot] caf2e20c58 chore(deps-dev): bump vite from 6.1.1 to 6.4.1 (#56)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.1.1 to 6.4.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-16 12:25:12 +08:00
hamster1963 816f6e7f9e fix: Remove redundant text label for average packet loss in NetworkChart component 2026-02-16 00:19:54 +08:00
仓鼠 abb39c3dc1 Fix typo in README.md description 2026-02-15 12:34:56 +08:00
hamster1963 7e4bed4f6c chore: update project name to nazha-dash-v2 and version to 2.0.0 2026-02-15 12:30:41 +08:00
hamster1963 6172c641a0 chore: update @radix-ui/react-dialog to version ^1.1.15 and improve dialog component styles 2026-02-14 21:56:16 +08:00
hamster1963 0c7c6a1378 feat: enhance tooltips and loading states in NetworkChart and ServerDetailChart; add translations for TSDB and login requirements 2026-02-14 15:04:37 +08:00
hamster1963 7d59371ee3 feat: add additional time period translations in serverDetailChart 2026-02-09 09:27:04 +08:00
hamster1963 a303a5add2 fix: adjust margin and layout for PeriodSelector and avg packet loss display in NetworkChartClient 2026-01-30 11:48:58 +08:00
hamster1963 18e3c74178 feat: add chart loading skeletons and enhance translation for time periods 2026-01-30 11:42:14 +08:00
hamster1963 6dae6cce8f feat: enhance ServerDetailChart with new chart tooltips and sync functionality 2026-01-30 09:30:50 +08:00
hamster1963 65b32ec81e feat: add server metrics fetching and update translations
- Implemented `fetchServerMetrics` function in `nezha-api.ts` to retrieve server metrics based on metric type and period.
- Added new metric types and periods to `nezha-api.ts` type definitions.
- Updated English and Chinese translations to include new terms for metrics and periods.
- Commented out `ServerDetailSummary` component in `ServerDetail.tsx` for future use.
2026-01-30 09:14:41 +08:00
仓鼠 1aa66f98ed fix: enhance NetworkChart with user login state and period selection (#54) 2026-01-29 09:29:00 +08:00
hamster1963 76590a6bd0 fix: swap ProcessChart and MemChart components in ServerDetailChart 2026-01-07 17:46:32 +08:00
hamster1963 af9926e774 fix: update styling and layout for ServerDetail components 2026-01-07 17:31:55 +08:00
hamster1963 cc0f5c0a2e fix: ensure billing information displays correctly based on available dates 2026-01-02 23:20:59 +08:00
Weblate (bot) ad4455e61d Translations update from Hosted Weblate (#52)
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Zilong Liu <2821624044@qq.com>
Co-authored-by: Руслан Пузич <visp80@gmail.com>
Co-authored-by: weblate <1607653+weblate@users.noreply.github.com>
Co-authored-by: 仓鼠 <71394853+hamster1963@users.noreply.github.com>
2026-01-02 23:11:56 +08:00
Moraxyc Xu a6e45eed46 fix: update framer-motion to fix build (#53) 2026-01-02 23:09:33 +08:00
hamster1963 92fada4792 refactor: simplify network route handling in PlanInfo component 2025-12-28 18:09:22 +08:00
hamster1963 29e349505d perf: use biome 2025-12-28 18:05:02 +08:00
hamster1963 3bfd4ef4d2 fix: prevent color fade during theme transitions 2025-12-28 16:33:57 +08:00
hamster1963 5a0c873dc8 feat: add ServerDetailSummary component for enhanced server usage display 2025-12-28 16:30:16 +08:00
hamster1963 c989a67265 fix: update GroupSwitch and Server components for improved tab handling and styling 2025-12-28 16:21:17 +08:00
hamster1963 42f99a6f84 feat: implement animated count component and update header for real-time display 2025-12-28 16:11:52 +08:00
hamster1963 b4f2abb885 fix: enhance text size for LanguageSwitcher and ThemeSwitcher components 2025-12-28 15:26:39 +08:00
hamster1963 746f890d65 refactor: update component styles and improve accessibility
- Adjusted hover shadow effects in CycleTransferStatsClient for better visual feedback.
- Fixed aspect ratio class in GlobalMap for consistent rendering.
- Updated font weight in GroupSwitch for improved readability.
- Modified Separator width class in Header for consistency across components.
- Enhanced button hover effects in NetworkChartClient for better user interaction.
- Adjusted margin class in ServerDetailOverview for better alignment.
- Updated margin classes in ServerOverview for consistent spacing.
- Refined background gradient classes in ServiceTrackerClient for better color management.
- Standardized font weight in TabSwitch for consistency.
- Corrected stroke color class in AnimatedCircularProgressBar for better theming.
- Improved focus outline handling in badge and button components for better accessibility.
- Updated chart component styles for improved visual hierarchy.
- Adjusted checkbox focus outline for better accessibility.
- Enhanced command dialog styles for improved usability.
- Updated dialog close button focus outline for better accessibility.
- Refined dropdown menu styles for improved usability and consistency.
- Adjusted input focus outline for better accessibility.
- Improved popover content styles for better visibility.
- Updated select component styles for improved usability.
- Refined separator height classes for consistency.
- Enhanced switch component focus outline for better accessibility.
- Updated table footer styles for improved visual consistency.
- Refactored global CSS to use new Tailwind CSS configuration and improved theming.
- Removed outdated Tailwind configuration file.
2025-12-28 15:24:04 +08:00
hamster1963 4d4c3f1639 fix: add server ID to tooltip data and update MapTooltip component for navigation 2025-12-28 15:01:27 +08:00
hamster1963 4f9db466a3 fix: enhance styling of LanguageSwitcher and ThemeSwitcher components 2025-12-28 14:40:44 +08:00
hamster1963 409ec0b62c fix: update SearchButton styling 2025-12-28 14:37:02 +08:00
hamster1963 15424513b5 fix: enhance styling of CardHeader in NetworkChartClient component 2025-12-28 14:36:45 +08:00
Weblate (bot) ae58d24ba8 Translations update from Hosted Weblate (#50)
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Zilong Liu <2821624044@qq.com>
Co-authored-by: Руслан Пузич <visp80@gmail.com>
Co-authored-by: weblate <1607653+weblate@users.noreply.github.com>
2025-12-25 15:15:04 +08:00
hamster1963 9aa83c1a9d feat: add Macau coordinates to countryCoordinates in geo-limit.ts 2025-12-01 00:34:08 +08:00
hamster1963 2cc5926fbe fix: remove unused useLocation import from App.tsx 2025-10-09 11:31:00 +08:00
hamster1963 1fda5ada9f feat: implement command context and provider for command handling; add search button component; enhance network chart with packet loss calculation and display; update translations for new features 2025-10-09 11:28:42 +08:00
hamster1963 48704b1135 fix: update cmdk dependency version to 1.1.1 2025-10-09 11:28:42 +08:00
127 changed files with 13284 additions and 10056 deletions
+1
View File
@@ -34,6 +34,7 @@ jobs:
- name: Release
uses: softprops/action-gh-release@v2
with:
draft: ${{ contains(github.ref_name, '-alpha') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-rc') }}
files: dist.zip
- name: Changelog
+38
View File
@@ -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
-12
View File
@@ -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"]
}
+1 -1
View File
@@ -3,5 +3,5 @@
> 交流
> [!NOTE]
> 此项目为 nezha-dash 的官方实现,作为哪吒监控 V1 版本的默认前端,功能上可能与 nezha-dash 有所不同。
> 此项目为 nezha-dash 的官方实现,作为哪吒监控的默认前端,功能上可能与 nezha-dash 有所不同。
> https://github.com/hamster1963/nezha-dash
+52
View File
@@ -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"
}
}
}
}
+709
View File
@@ -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=="],
}
}
BIN
View File
Binary file not shown.
-28
View File
@@ -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
View File
@@ -10,7 +10,7 @@
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
}
document.documentElement.classList.add(theme)
} catch (e) {
} catch (_e) {
document.documentElement.classList.add("light")
}
</script>
@@ -36,15 +36,15 @@
}
html {
background-color: var(--bg) !important;
background-color: var(--bg);
}
body {
background-color: var(--bg) !important;
background-color: var(--bg);
}
#root {
background-color: var(--bg) !important;
background-color: var(--bg);
visibility: hidden;
}
@@ -63,7 +63,7 @@
}
</style>
<script>
;(function () {
;(() => {
const storageKey = "vite-ui-theme"
const theme = localStorage.getItem(storageKey) || "system"
const root = document.documentElement
+16 -20
View File
@@ -1,14 +1,16 @@
{
"name": "nazha-dash-v1",
"name": "nazha-dash-v2",
"private": true,
"version": "1.0.0",
"version": "2.0.2",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"format": "prettier --write .",
"lint": "biome lint",
"lint:fix": "biome lint --fix",
"format": "biome format --write .",
"check": "biome check",
"check:fix": "biome check --fix --unsafe",
"preview": "vite preview"
},
"dependencies": {
@@ -17,7 +19,7 @@
"@number-flow/react": "0.5.5",
"@radix-ui/react-accordion": "1.2.3",
"@radix-ui/react-checkbox": "1.1.4",
"@radix-ui/react-dialog": "1.1.6",
"@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",
@@ -30,7 +32,6 @@
"@tanstack/react-query": "5.66.7",
"@tanstack/react-query-devtools": "5.66.7",
"@tanstack/react-table": "8.21.2",
"@trivago/prettier-plugin-sort-imports": "5.2.2",
"@types/d3-geo": "3.1.0",
"@types/luxon": "3.4.2",
"class-variance-authority": "0.7.1",
@@ -39,36 +40,31 @@
"country-flag-icons": "1.5.18",
"d3-geo": "3.1.1",
"dayjs": "1.11.13",
"framer-motion": "11.18.2",
"framer-motion": "12.23.26",
"i18n-iso-countries": "7.14.0",
"i18next": "24.2.2",
"lucide-react": "0.460.0",
"luxon": "3.5.0",
"prettier-plugin-tailwindcss": "0.6.11",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-i18next": "15.4.1",
"react-router-dom": "7.2.0",
"react-router-dom": "^7.13.2",
"recharts": "2.15.1",
"sonner": "1.7.4",
"tailwind-merge": "2.6.0",
"tailwindcss-animate": "1.0.7"
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/js": "9.20.0",
"@biomejs/biome": "2.3.10",
"@tailwindcss/postcss": "^4.2.2",
"@types/node": "22.13.4",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@vitejs/plugin-react-swc": "3.8.0",
"autoprefixer": "10.4.20",
"eslint": "9.20.1",
"eslint-plugin-react-hooks": "5.1.0",
"eslint-plugin-react-refresh": "0.4.19",
"@vitejs/plugin-react": "^6.0.1",
"globals": "15.15.0",
"postcss": "8.5.3",
"tailwindcss": "3.4.17",
"tailwindcss": "^4.2.2",
"typescript": "~5.6.3",
"typescript-eslint": "8.24.1",
"vite": "6.1.1"
"vite": "8.0.3"
}
}
+911 -2355
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -1,6 +1,5 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
"@tailwindcss/postcss": {},
},
}
};
+3 -3
View File
@@ -1,7 +1,7 @@
const { execSync } = require("child_process")
const { execSync } = require("node:child_process");
// 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
console.log(gitHash)
console.log(gitHash);
+93 -45
View File
@@ -1,85 +1,125 @@
import { useQuery } from "@tanstack/react-query"
import React, { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { Route, BrowserRouter as Router, Routes } from "react-router-dom"
import { useQuery } from "@tanstack/react-query";
import { DateTime } from "luxon";
import type React from "react";
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 ErrorBoundary from "./components/ErrorBoundary"
import Footer from "./components/Footer"
import Header, { RefreshToast } from "./components/Header"
import { useBackground } from "./hooks/use-background"
import { useTheme } from "./hooks/use-theme"
import { InjectContext } from "./lib/inject"
import { fetchSetting } from "./lib/nezha-api"
import { cn } from "./lib/utils"
import ErrorPage from "./pages/ErrorPage"
import NotFound from "./pages/NotFound"
import Server from "./pages/Server"
import ServerDetail from "./pages/ServerDetail"
import { DashCommand } from "./components/DashCommand";
import ErrorBoundary from "./components/ErrorBoundary";
import Footer from "./components/Footer";
import Header, { RefreshToast } from "./components/Header";
import { useBackground } from "./hooks/use-background";
import { useTheme } from "./hooks/use-theme";
import { InjectContext } from "./lib/inject";
import { fetchSetting } from "./lib/nezha-api";
import { cn } from "./lib/utils";
import ErrorPage from "./pages/ErrorPage";
import NotFound from "./pages/NotFound";
import Server from "./pages/Server";
import ServerDetail from "./pages/ServerDetail";
const App: React.FC = () => {
// 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()
});
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)
InjectContext(settingData?.data?.config?.custom_code);
setIsCustomCodeInjected(true);
}
}, [settingData?.data?.config?.custom_code])
// 同步自定义配置到全局变量
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 =
// @ts-expect-error ForceTheme is a global variable
(window.ForceTheme as string) !== "" ? window.ForceTheme : undefined
(window.ForceTheme as string) !== "" ? window.ForceTheme : undefined;
useEffect(() => {
if (forceTheme === "dark" || forceTheme === "light") {
setTheme(forceTheme)
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])
}, [forceTheme, setTheme]);
if (error) {
return <ErrorPage code={500} message={error.message} />
return <ErrorPage code={500} message={error.message} />;
}
if (!settingData) {
return null
return null;
}
if (settingData?.data?.config?.custom_code && !isCustomCodeInjected) {
return null
return null;
}
if (settingData?.data?.config?.language && !localStorage.getItem("language")) {
i18n.changeLanguage(settingData?.data?.config?.language)
if (
settingData?.data?.config?.language &&
!localStorage.getItem("language")
) {
i18n.changeLanguage(settingData?.data?.config?.language);
}
const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined
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", {
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")}
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})` }}
/>
)}
@@ -102,8 +142,16 @@ const App: React.FC = () => {
</main>
</div>
</ErrorBoundary>
</Router>
)
}
);
};
export default App
// Main App wrapper with router
const App: React.FC = () => {
return (
<Router basename={import.meta.env.BASE_URL}>
<RouteChecker />
</Router>
);
};
export default App;
+109
View File
@@ -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>
);
}
+26 -20
View File
@@ -1,38 +1,43 @@
import { CycleTransferStats, NezhaServer } from "@/types/nezha-api"
import React from "react"
import type React from "react";
import type { CycleTransferStats, NezhaServer } from "@/types/nezha-api";
import { CycleTransferStatsClient } from "./CycleTransferStatsClient"
import { CycleTransferStatsClient } from "./CycleTransferStatsClient";
interface CycleTransferStatsProps {
serverList: NezhaServer[]
cycleStats: CycleTransferStats
className?: string
serverList: NezhaServer[];
cycleStats: CycleTransferStats;
className?: string;
}
export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({ serverList, cycleStats, className }) => {
export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({
serverList,
cycleStats,
className,
}) => {
if (serverList.length === 0) {
return null
return null;
}
const serverIdList = serverList.map((server) => server.id.toString())
const serverIdList = serverList.map((server) => server.id.toString());
return (
<section className="grid grid-cols-1 md:grid-cols-3 gap-3">
{Object.entries(cycleStats).map(([cycleId, cycleData]) => {
if (!cycleData.server_name) {
return null
return null;
}
return Object.entries(cycleData.server_name).map(([serverId, serverName]) => {
const transfer = cycleData.transfer?.[serverId] || 0
const nextUpdate = cycleData.next_update?.[serverId]
return Object.entries(cycleData.server_name).map(
([serverId, serverName]) => {
const transfer = cycleData.transfer?.[serverId] || 0;
const nextUpdate = cycleData.next_update?.[serverId];
if (!serverIdList.includes(serverId)) {
return null
return null;
}
if (!transfer && !nextUpdate) {
return null
return null;
}
return (
@@ -52,11 +57,12 @@ export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({ serv
]}
className={className}
/>
)
})
);
},
);
})}
</section>
)
}
);
};
export default CycleTransferStatsCard
export default CycleTransferStatsCard;
+47 -30
View File
@@ -1,29 +1,34 @@
import { formatBytes } from "@/lib/format"
import { cn } from "@/lib/utils"
import React from "react"
import { useTranslation } from "react-i18next"
import type React from "react";
import { useTranslation } from "react-i18next";
import { formatBytes } from "@/lib/format";
import { cn } from "@/lib/utils";
interface CycleTransferStatsClientProps {
name: string
from: string
to: string
max: number
name: string;
from: string;
to: string;
max: number;
serverStats: Array<{
serverId: string
serverName: string
transfer: number
nextUpdate: string
}>
className?: string
serverId: string;
serverName: string;
transfer: number;
nextUpdate: string;
}>;
className?: string;
}
export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> = ({ name, from, to, max, serverStats, className }) => {
const { t } = useTranslation()
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
export const CycleTransferStatsClient: React.FC<
CycleTransferStatsClientProps
> = ({ name, from, to, max, serverStats, className }) => {
const { t } = useTranslation();
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
return (
<div
className={cn(
"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",
"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,
{
"bg-card/70": customBackgroundImage,
@@ -31,24 +36,34 @@ export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> =
)}
>
{serverStats.map(({ serverId, serverName, transfer, nextUpdate }) => {
const progress = (transfer / max) * 100
const progress = (transfer / max) * 100;
return (
<div key={serverId} className="space-y-3">
{/* Header */}
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{serverName}</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>
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
{serverName}
</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 */}
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<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-xs text-neutral-500 dark:text-neutral-400">/ {formatBytes(max)}</span>
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
{formatBytes(transfer)}
</span>
<span className="text-xs text-neutral-500 dark:text-neutral-400">
/ {formatBytes(max)}
</span>
</div>
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">{progress.toFixed(1)}%</span>
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">
{progress.toFixed(1)}%
</span>
</div>
<div className="relative h-1.5">
@@ -63,17 +78,19 @@ export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> =
{/* Footer */}
<div className="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400">
<span>
{new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()}
{new Date(from).toLocaleDateString()} -{" "}
{new Date(to).toLocaleDateString()}
</span>
<span>
{t("cycleTransfer.nextUpdate")}: {new Date(nextUpdate).toLocaleString()}
{t("cycleTransfer.nextUpdate")}:{" "}
{new Date(nextUpdate).toLocaleString()}
</span>
</div>
</div>
)
);
})}
</div>
)
}
);
};
export default CycleTransferStatsClient
export default CycleTransferStatsClient;
+47 -36
View File
@@ -1,39 +1,50 @@
"use client"
"use client";
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command"
import { useTheme } from "@/hooks/use-theme"
import { useWebSocketContext } from "@/hooks/use-websocket-context"
import { formatNezhaInfo } from "@/lib/utils"
import { NezhaWebsocketResponse } from "@/types/nezha-api"
import { Home, Moon, Sun, SunMoon } from "lucide-react"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { Home, Moon, Sun, SunMoon } from "lucide-react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
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() {
const [open, setOpen] = useState(false)
const [search, setSearch] = useState("")
const navigate = useNavigate()
const { t } = useTranslation()
const { setTheme } = useTheme()
const { isOpen, closeCommand, toggleCommand } = useCommand();
const [search, setSearch] = useState("");
const navigate = useNavigate();
const { t } = useTranslation();
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(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
e.preventDefault();
toggleCommand();
}
};
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, [toggleCommand]);
if (!connected || !nezhaWsData) return null
if (!connected || !nezhaWsData) return null;
const shortcuts = [
{
@@ -63,24 +74,26 @@ export function DashCommand() {
].map((item) => ({
...item,
value: `${item.keywords.join(" ")} ${item.label}`,
}))
}));
return (
<>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder={t("TypeCommand")} value={search} onValueChange={setSearch} />
<CommandDialog open={isOpen} onOpenChange={closeCommand}>
<CommandInput
placeholder={t("TypeCommand")}
value={search}
onValueChange={setSearch}
/>
<CommandList className="border-t">
<CommandEmpty>{t("NoResults")}</CommandEmpty>
{nezhaWsData.servers && nezhaWsData.servers.length > 0 && (
<>
<CommandGroup heading={t("Servers")}>
{nezhaWsData.servers.map((server) => (
<CommandItem
key={server.id}
value={server.name}
onSelect={() => {
navigate(`/server/${server.id}`)
setOpen(false)
navigate(`/server/${server.id}`);
closeCommand();
}}
>
{formatNezhaInfo(nezhaWsData.now, server).online ? (
@@ -92,7 +105,6 @@ export function DashCommand() {
</CommandItem>
))}
</CommandGroup>
</>
)}
<CommandSeparator />
@@ -102,8 +114,8 @@ export function DashCommand() {
key={item.label}
value={item.value}
onSelect={() => {
item.action()
setOpen(false)
item.action();
closeCommand();
}}
>
{item.icon}
@@ -113,6 +125,5 @@ export function DashCommand() {
</CommandGroup>
</CommandList>
</CommandDialog>
</>
)
);
}
+2 -17
View File
@@ -5,6 +5,7 @@ import { getDomains, Domain } from '@/api/domain';
import { CalendarDays, DollarSign } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useEffect, useState } from 'react';
import RemainPercentBar from "./RemainPercentBar";
// =======================================================
// 彩色备注标签组件
@@ -98,21 +99,7 @@ const DomainCard = ({ domain }: { domain: Domain }) => {
const billingData = domain.BillingData || {};
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 (
<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>
</div>
<div className="flex-1">
<div className="w-full bg-muted rounded-full h-1.5 overflow-hidden">
<div className={cn("h-1.5 rounded-full transition-all duration-500", progressBarColor)} style={{ width: progressBarWidth }} />
</div>
<RemainPercentBar value={expiresIn ? Math.max(0, Math.min(100, (expiresIn / 365) * 100)) : 100} className="w-full h-1.5" />
</div>
<span className="font-medium text-muted-foreground w-12 text-right">{expiresIn !== undefined ? `${expiresIn}` : 'N/A'}</span>
</div>
+16 -11
View File
@@ -1,36 +1,41 @@
import React from "react"
import React from "react";
import ErrorPage from "../pages/ErrorPage"
import ErrorPage from "../pages/ErrorPage";
interface Props {
children: React.ReactNode
children: React.ReactNode;
}
interface State {
hasError: boolean
error?: Error
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return {
hasError: true,
error,
}
};
}
render() {
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;
+31 -17
View File
@@ -1,20 +1,18 @@
// src/components/Footer.tsx (已添加您的署名)
import { fetchSetting } from "@/lib/nezha-api"
import { useQuery } from "@tanstack/react-query"
import React from "react"
import { useTranslation } from "react-i18next"
import { useQuery } from "@tanstack/react-query";
import type React from "react";
import { useTranslation } from "react-i18next";
import { fetchSetting } from "@/lib/nezha-api";
const Footer: React.FC = () => {
const { t } = useTranslation()
const isMac = /macintosh|mac os x/i.test(navigator.userAgent)
const { t } = useTranslation();
const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
const { data: settingData } = useQuery({
queryKey: ["setting"],
queryFn: () => fetchSetting(),
refetchOnMount: true,
refetchOnWindowFocus: true,
})
});
return (
<footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4 server-footer">
@@ -22,7 +20,11 @@ const Footer: React.FC = () => {
<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">
&copy;2020-{new Date().getFullYear()}{" "}
<a href={"https://github.com/naiba/nezha"} target="_blank" className="hover:underline">
<a
href={"https://github.com/naiba/nezha"}
target="_blank"
rel="noopener"
>
Nezha
</a>
<p>{settingData?.data?.version || ""}</p>
@@ -34,19 +36,31 @@ const Footer: React.FC = () => {
</kbd>
</p>
<section>
{t("footer.themeBy")}{" "}
<a href={"https://github.com/hamster1963/nezha-dash"} target="_blank" className="hover:underline">
{t("footer.themeBy")}
<a
href={"https://github.com/hamster1963/nezha-dash"}
target="_blank"
rel="noopener"
>
nezha-dash
</a>
{import.meta.env.VITE_GIT_HASH && (
<a href={"https://github.com/hamster1963/nezha-dash-v1/commit/" + import.meta.env.VITE_GIT_HASH} className="ml-1 hover:underline">
<a
href={`https://github.com/hamster1963/nezha-dash-v1/commit/${import.meta.env.VITE_GIT_HASH}`}
className="ml-1"
>
({import.meta.env.VITE_GIT_HASH})
</a>
)}
</section>
<section className="mt-1">
{"Modified by "}
<a href={"https://github.com/buriburizaem0n"} target="_blank" className="hover:underline font-medium">
<a
href={"https://github.com/buriburizaem0n"}
target="_blank"
rel="noopener"
className="hover:underline font-medium"
>
buriburizaem0n
</a>
</section>
@@ -54,7 +68,7 @@ const Footer: React.FC = () => {
</section>
</section>
</footer>
)
}
);
};
export default Footer
export default Footer;
+110 -61
View File
@@ -1,35 +1,47 @@
import useTooltip from "@/hooks/use-tooltip"
import { geoJsonString } from "@/lib/geo-json-string"
import { countryCoordinates } from "@/lib/geo-limit"
import { cn, formatNezhaInfo } from "@/lib/utils"
import { NezhaServer } from "@/types/nezha-api"
import { geoEquirectangular, geoPath } from "d3-geo"
import { useTranslation } from "react-i18next"
import { geoEquirectangular, geoPath } from "d3-geo";
import { useTranslation } from "react-i18next";
import useTooltip from "@/hooks/use-tooltip";
import { geoJsonString } from "@/lib/geo-json-string";
import { countryCoordinates } from "@/lib/geo-limit";
import { cn, formatNezhaInfo } from "@/lib/utils";
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 }) {
const { t } = useTranslation()
const countryList: string[] = []
const serverCounts: { [key: string]: number } = {}
export default function GlobalMap({
serverList,
now,
}: {
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) => {
if (server.country_code) {
const countryCode = server.country_code.toUpperCase()
const countryCode = server.country_code.toUpperCase();
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 height = 500
const width = 900;
const height = 500;
const geoJson = JSON.parse(geoJsonString)
const filteredFeatures = geoJson.features.filter((feature: { properties: { iso_a3_eh: string } }) => feature.properties.iso_a3_eh !== "")
const geoJson = JSON.parse(geoJsonString);
const filteredFeatures = geoJson.features.filter(
(feature: { properties: { iso_a3_eh: string } }) =>
feature.properties.iso_a3_eh !== "",
);
return (
<section
@@ -52,39 +64,56 @@ export default function GlobalMap({ serverList, now }: { serverList: NezhaServer
/>
</div>
</section>
)
);
}
interface InteractiveMapProps {
countries: string[]
serverCounts: { [key: string]: number }
width: number
height: number
countries: string[];
serverCounts: { [key: string]: number };
width: number;
height: number;
filteredFeatures: {
type: "Feature"
type: "Feature";
properties: {
iso_a2_eh: string
[key: string]: string
}
geometry: never
}[]
nezhaServerList: NezhaServer[]
now: number
iso_a2_eh: string;
[key: string]: string;
};
geometry: never;
}[];
nezhaServerList: NezhaServer[];
now: number;
}
export function InteractiveMap({ countries, serverCounts, width, height, filteredFeatures, nezhaServerList, now }: InteractiveMapProps) {
const { setTooltipData } = useTooltip()
export function InteractiveMap({
countries,
serverCounts,
width,
height,
filteredFeatures,
nezhaServerList,
now,
}: InteractiveMapProps) {
const { setTooltipData } = useTooltip();
const projection = geoEquirectangular()
.scale(140)
.translate([width / 2, height / 2])
.rotate([-12, 0, 0])
.rotate([-12, 0, 0]);
const path = geoPath().projection(projection)
const path = geoPath().projection(projection);
return (
<div className="relative w-full aspect-[2/1]" onMouseLeave={() => setTooltipData(null)}>
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} xmlns="http://www.w3.org/2000/svg" className="w-full h-auto">
<div
className="relative w-full aspect-2/1"
onMouseLeave={() => setTooltipData(null)}
>
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
xmlns="http://www.w3.org/2000/svg"
className="w-full h-auto"
>
<defs>
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.5" fill="currentColor" />
@@ -92,11 +121,20 @@ export function InteractiveMap({ countries, serverCounts, width, height, filtere
</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)} />
<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 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 (
<path
@@ -109,61 +147,72 @@ export function InteractiveMap({ countries, serverCounts, width, height, filtere
}
onMouseEnter={() => {
if (!isHighlighted) {
setTooltipData(null)
return
setTooltipData(null);
return;
}
if (path.centroid(feature)) {
const countryCode = feature.properties.iso_a2_eh
const countryCode = feature.properties.iso_a2_eh;
const countryServers = nezhaServerList
.filter((server: NezhaServer) => server.country_code?.toUpperCase() === countryCode)
.filter(
(server: NezhaServer) =>
server.country_code?.toUpperCase() === countryCode,
)
.map((server: NezhaServer) => ({
id: server.id,
name: server.name,
status: formatNezhaInfo(now, server).online,
}))
}));
setTooltipData({
centroid: path.centroid(feature),
country: feature.properties.name,
count: serverCount,
servers: countryServers,
})
});
}
}}
/>
)
);
})}
{/* 渲染不在 filteredFeatures 中的国家标记点 */}
{countries.map((countryCode) => {
// 检查该国家是否已经在 filteredFeatures 中
const isInFilteredFeatures = filteredFeatures.some((feature) => feature.properties.iso_a2_eh === countryCode)
const isInFilteredFeatures = filteredFeatures.some(
(feature) => feature.properties.iso_a2_eh === countryCode,
);
// 如果已经在 filteredFeatures 中,跳过
if (isInFilteredFeatures) return null
if (isInFilteredFeatures) return null;
// 获取国家的经纬度
const coords = countryCoordinates[countryCode]
if (!coords) return null
const coords = countryCoordinates[countryCode];
if (!coords) return null;
// 使用投影函数将经纬度转换为 SVG 坐标
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0]
const serverCount = serverCounts[countryCode] || 0
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0];
const serverCount = serverCounts[countryCode] || 0;
return (
<g
key={countryCode}
onMouseEnter={() => {
const countryServers = nezhaServerList
.filter((server: NezhaServer) => server.country_code?.toUpperCase() === countryCode.toUpperCase())
.filter(
(server: NezhaServer) =>
server.country_code?.toUpperCase() ===
countryCode.toUpperCase(),
)
.map((server: NezhaServer) => ({
id: server.id,
name: server.name,
status: formatNezhaInfo(now, server).online,
}))
}));
setTooltipData({
centroid: [x, y],
country: coords.name,
count: serverCount,
servers: countryServers,
})
});
}}
className="cursor-pointer"
>
@@ -174,11 +223,11 @@ export function InteractiveMap({ countries, serverCounts, width, height, filtere
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
/>
</g>
)
);
})}
</g>
</svg>
<MapTooltip />
</div>
)
);
}
+52 -33
View File
@@ -1,65 +1,82 @@
import { cn } from "@/lib/utils"
import { m } from "framer-motion"
import { createRef, useEffect, useRef } from "react"
import { m } from "framer-motion";
import { createRef, useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
export default function GroupSwitch({
tabs,
currentTab,
setCurrentTab,
}: {
tabs: string[]
currentTab: string
setCurrentTab: (tab: string) => void
tabs: string[];
currentTab: string;
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 tagRefs = useRef(tabs.map(() => createRef<HTMLDivElement>()))
const scrollRef = useRef<HTMLDivElement>(null);
const tagRefs = useRef(tabs.map(() => createRef<HTMLDivElement>()));
useEffect(() => {
const container = scrollRef.current
if (!container) return
const container = scrollRef.current;
if (!container) return;
const isOverflowing = container.scrollWidth > container.clientWidth
if (!isOverflowing) return
const isOverflowing = container.scrollWidth > container.clientWidth;
if (!isOverflowing) return;
const onWheel = (e: WheelEvent) => {
e.preventDefault()
container.scrollLeft += e.deltaY
}
e.preventDefault();
container.scrollLeft += e.deltaY;
};
container.addEventListener("wheel", onWheel, { passive: false })
container.addEventListener("wheel", onWheel, { passive: false });
return () => {
container.removeEventListener("wheel", onWheel)
}
}, [])
container.removeEventListener("wheel", onWheel);
};
}, []);
useEffect(() => {
const savedGroup = sessionStorage.getItem("selectedGroup")
if (tabs.length === 1 && tabs[0] === "All") {
setCurrentTab("All");
return;
}
const savedGroup = sessionStorage.getItem("selectedGroup");
if (savedGroup && tabs.includes(savedGroup)) {
setCurrentTab(savedGroup)
setCurrentTab(savedGroup);
}
}, [tabs, setCurrentTab])
}, [tabs, setCurrentTab]);
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({
behavior: "smooth",
block: "nearest",
inline: "center",
})
});
}
}, [currentTab, tabs.indexOf]);
if (tabs.length === 1 && tabs[0] === "All") {
return null;
}
}, [currentTab])
return (
<div ref={scrollRef} className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]">
<div
className={cn("flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800", {
ref={scrollRef}
className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]"
>
<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,
})}
},
)}
>
{tabs.map((tab: string, index: number) => (
<div
@@ -67,8 +84,10 @@ export default function GroupSwitch({
ref={tagRefs.current[index]}
onClick={() => setCurrentTab(tab)}
className={cn(
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500",
currentTab === tab ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500",
"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",
)}
>
{currentTab === tab && (
@@ -88,5 +107,5 @@ export default function GroupSwitch({
))}
</div>
</div>
)
);
}
+167 -109
View File
@@ -1,87 +1,127 @@
import { ModeToggle } from "@/components/ThemeSwitcher"
import { Separator } from "@/components/ui/separator"
import { Skeleton } from "@/components/ui/skeleton"
import { useBackground } from "@/hooks/use-background"
import { useWebSocketContext } from "@/hooks/use-websocket-context"
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api"
import { cn } from "@/lib/utils"
import NumberFlow, { NumberFlowGroup } from "@number-flow/react"
import { useQuery } from "@tanstack/react-query"
import { AnimatePresence, m } from "framer-motion"
import { ImageMinus } from "lucide-react"
import { DateTime } from "luxon"
import { useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { useQuery } from "@tanstack/react-query";
import { AnimatePresence, m } from "framer-motion";
import { ImageMinus } from "lucide-react";
import { DateTime } from "luxon";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { ModeToggle } from "@/components/ThemeSwitcher";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { useBackground } from "@/hooks/use-background";
import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api";
import { cn } from "@/lib/utils";
import { LanguageSwitcher } from "./LanguageSwitcher"
import { Loader, LoadingSpinner } from "./loading/Loader"
import { Button } from "./ui/button"
import AnimateCountClient from "./AnimatedCount";
import { LanguageSwitcher } from "./LanguageSwitcher";
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() {
const { t } = useTranslation()
const navigate = useNavigate()
const { backgroundImage, updateBackground } = useBackground()
const { t } = useTranslation();
const navigate = useNavigate();
const { backgroundImage, updateBackground } = useBackground();
const { data: settingData, isLoading } = useQuery({
queryKey: ["setting"],
queryFn: () => fetchSetting(),
refetchOnMount: 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(() => {
const link = document.querySelector("link[rel*='icon']") || document.createElement("link")
const link =
document.querySelector("link[rel*='icon']") ||
document.createElement("link");
// @ts-expect-error set link.type
link.type = "image/x-icon"
link.type = "image/x-icon";
// @ts-expect-error set link.rel
link.rel = "shortcut icon"
link.rel = "shortcut icon";
// @ts-expect-error set link.href
link.href = customLogo
document.getElementsByTagName("head")[0].appendChild(link)
}, [customLogo])
link.href = customLogo;
document.getElementsByTagName("head")[0].appendChild(link);
}, [customLogo]);
useEffect(() => {
document.title = siteName || "哪吒监控 Nezha Monitoring"
}, [siteName])
document.title = siteName || "哪吒监控 Nezha Monitoring";
}, [siteName]);
const handleBackgroundToggle = () => {
if (window.CustomBackgroundImage) {
// Store the current background image before removing it
sessionStorage.setItem("savedBackgroundImage", window.CustomBackgroundImage)
updateBackground(undefined)
sessionStorage.setItem(
"savedBackgroundImage",
window.CustomBackgroundImage,
);
updateBackground(undefined);
} else {
// Restore the saved background image
const savedImage = sessionStorage.getItem("savedBackgroundImage")
const savedImage = sessionStorage.getItem("savedBackgroundImage");
if (savedImage) {
updateBackground(savedImage)
}
updateBackground(savedImage);
}
}
};
const customBackgroundImage = backgroundImage
const customBackgroundImage = backgroundImage;
return (
<div className="mx-auto w-full max-w-5xl">
<section className="flex items-center justify-between header-top">
<section
onClick={() => {
sessionStorage.removeItem("selectedGroup")
navigate("/")
sessionStorage.removeItem("selectedGroup");
navigate("/");
}}
className="cursor-pointer flex items-center sm:text-base text-sm font-medium"
>
@@ -94,18 +134,29 @@ function Header() {
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!"
/>
</div>
{isLoading ? <Skeleton className="h-6 w-20 rounded-[5px] bg-muted-foreground/10 animate-none" /> : siteName || "NEZHA"}
<Separator orientation="vertical" className="mx-2 hidden h-4 w-[1px] md:block" />
<p className="hidden text-sm font-medium opacity-40 md:block">{customDesc}</p>
{isLoading ? (
<Skeleton className="h-6 w-20 rounded-[5px] bg-muted-foreground/10 animate-none" />
) : (
siteName || "NEZHA"
)}
<Separator
orientation="vertical"
className="mx-2 hidden h-4 w-px md:block"
/>
<p className="hidden text-sm font-medium opacity-40 md:block">
{customDesc}
</p>
</section>
<section className="flex items-center gap-2 header-handles">
<div className="hidden sm:flex items-center gap-2">
<Links />
<DashboardLink />
</div>
<SearchButton />
<LanguageSwitcher />
<ModeToggle />
{(customBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) && (
{(customBackgroundImage ||
sessionStorage.getItem("savedBackgroundImage")) && (
<Button
variant="outline"
size="sm"
@@ -121,12 +172,17 @@ function Header() {
<Button
variant="outline"
size="sm"
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(
"hover:bg-white dark:hover:bg-black cursor-default rounded-full flex items-center px-[9px] bg-white dark:bg-black",
{
"bg-white/70 dark:bg-black/70": customBackgroundImage,
})}
},
)}
>
{connected ? onlineCount : <Loader visible={true} />}
<p className="text-muted-foreground">{connected ? t("online") : t("offline")}</p>
<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,
@@ -141,21 +197,20 @@ function Header() {
</div>
<Overview />
</div>
)
);
}
type links = {
link: string
name: string
}
link: string;
name: string;
};
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 (
<div className="flex items-center gap-2 w-fit">
@@ -170,27 +225,27 @@ function Links() {
>
{link.name}
</a>
)
);
})}
</div>
)
);
}
export function RefreshToast() {
const { t } = useTranslation()
const navigate = useNavigate()
const { t } = useTranslation();
const navigate = useNavigate();
const { needReconnect } = useWebSocketContext()
const { needReconnect } = useWebSocketContext();
if (!needReconnect) {
return null
return null;
}
if (needReconnect) {
sessionStorage.removeItem("needRefresh")
sessionStorage.removeItem("needRefresh");
setTimeout(() => {
navigate(0)
}, 1000)
navigate(0);
}, 1000);
}
return (
@@ -200,7 +255,7 @@ export function RefreshToast() {
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
exit={{ opacity: 0, filter: "blur(10px)", scale: 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">
<LoadingSpinner />
@@ -208,13 +263,13 @@ export function RefreshToast() {
</section>
</m.div>
</AnimatePresence>
)
);
}
function DashboardLink() {
const { t } = useTranslation()
const { setNeedReconnect } = useWebSocketContext()
const previousLoginState = useRef<boolean | null>(null)
const { t } = useTranslation();
const { setNeedReconnect } = useWebSocketContext();
const previousLoginState = useRef<boolean | null>(null);
const {
data: userData,
isFetched,
@@ -229,27 +284,34 @@ function DashboardLink() {
refetchIntervalInBackground: true,
refetchInterval: 1000 * 30,
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) {
previousLoginState.current = isLogin
previousLoginState.current = isLogin;
}
useEffect(() => {
refetch()
}, [document.cookie])
refetch();
}, [refetch]);
useEffect(() => {
if (isFetched || isError) {
// 只有当登录状态发生变化时才设置needReconnect
if (previousLoginState.current !== null && previousLoginState.current !== isLogin) {
setNeedReconnect(true)
if (
previousLoginState.current !== null &&
previousLoginState.current !== isLogin
) {
setNeedReconnect(true);
}
previousLoginState.current = isLogin
previousLoginState.current = isLogin;
}
}, [isLogin])
}, [isLogin, isError, isFetched, setNeedReconnect]);
return (
<div className="flex items-center gap-2">
@@ -262,42 +324,38 @@ function DashboardLink() {
{isLogin && t("dashboard")}
</a>
</div>
)
);
}
function Overview() {
const { t } = useTranslation()
const [time, setTime] = useState({
hh: DateTime.now().setLocale("en-US").hour,
mm: DateTime.now().setLocale("en-US").minute,
ss: DateTime.now().setLocale("en-US").second,
})
const { t } = useTranslation();
const time = useCurrentTime();
const [mounted, setMounted] = useState(false);
useEffect(() => {
const timer = setInterval(() => {
setTime({
hh: DateTime.now().setLocale("en-US").hour,
mm: DateTime.now().setLocale("en-US").minute,
ss: DateTime.now().setLocale("en-US").second,
})
}, 1000)
setMounted(true);
}, []);
return () => clearInterval(timer)
}, [])
return (
<section className={"mt-10 flex flex-col md:mt-16 header-timer"}>
<p className="text-base font-semibold">👋 {t("overview")}</p>
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1">
<p className="text-sm font-medium opacity-50">{t("whereTheTimeIs")}</p>
<NumberFlowGroup>
<div style={{ fontVariantNumeric: "tabular-nums" }} className="flex text-sm font-medium mt-0.5">
<NumberFlow trend={1} value={time.hh} format={{ minimumIntegerDigits: 2 }} />
<NumberFlow prefix=":" trend={1} value={time.mm} digits={{ 1: { max: 5 } }} format={{ minimumIntegerDigits: 2 }} />
<p className="mt-[0.5px]">:{time.ss.toString().padStart(2, "0")}</p>
{mounted ? (
<div className="flex items-center font-medium text-sm">
<AnimateCountClient count={time.hh} minDigits={2} />
<span className="mb-px font-medium text-sm opacity-50">:</span>
<AnimateCountClient count={time.mm} minDigits={2} />
<span className="mb-px font-medium text-sm opacity-50">:</span>
<span className="font-medium text-sm">
<AnimateCountClient count={time.ss} minDigits={2} />
</span>
</div>
</NumberFlowGroup>
) : (
<Skeleton className="h-[21px] w-16 animate-none rounded-[5px] bg-muted-foreground/10" />
)}
</div>
</section>
)
);
}
export default Header
export default Header;
+2 -2
View File
@@ -3,7 +3,7 @@ export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
<svg viewBox="0 0 496 512" fill="white" {...props}>
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
</svg>
)
);
}
export function BackIcon() {
@@ -28,5 +28,5 @@ export function BackIcon() {
height="20"
/>
</>
)
);
}
+41 -17
View File
@@ -1,22 +1,30 @@
"use client"
"use client";
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils"
import { CheckCircleIcon, LanguageIcon } from "@heroicons/react/20/solid"
import { useTranslation } from "react-i18next"
import { CheckCircleIcon, LanguageIcon } from "@heroicons/react/20/solid";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
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) => {
e.preventDefault() // 阻止默认的关闭行为
i18n.changeLanguage(newLocale)
}
e.preventDefault(); // 阻止默认的关闭行为
i18n.changeLanguage(newLocale);
};
const localeItems = [
{ name: t("language.zh-CN"), code: "zh-CN" },
@@ -26,7 +34,7 @@ export function LanguageSwitcher() {
{ name: t("language.es-ES"), code: "es-ES" },
{ name: t("language.de-DE"), code: "de-DE" },
{ name: t("language.ta-IN"), code: "ta-IN" },
]
];
return (
<DropdownMenu>
@@ -43,12 +51,28 @@ export function LanguageSwitcher() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
{localeItems.map((item) => (
<DropdownMenuItem key={item.code} onSelect={(e) => handleSelect(e, item.code)} className={locale === item.code ? "bg-muted gap-3" : ""}>
{item.name} {locale === item.code && <CheckCircleIcon className="size-4" />}
{localeItems.map((item, index) => (
<DropdownMenuItem
key={item.code}
onSelect={(e) => handleSelect(e, item.code)}
className={cn(
"text-xs",
{
"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>
)
);
}
+33 -17
View File
@@ -1,13 +1,15 @@
import useTooltip from "@/hooks/use-tooltip"
import { AnimatePresence, m } from "framer-motion"
import { memo } from "react"
import { useTranslation } from "react-i18next"
import { AnimatePresence, m } from "framer-motion";
import { memo } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import useTooltip from "@/hooks/use-tooltip";
const MapTooltip = memo(function MapTooltip() {
const { t } = useTranslation()
const { tooltipData } = useTooltip()
const { t } = useTranslation();
const navigate = useNavigate();
const { tooltipData } = useTooltip();
if (!tooltipData) return null
if (!tooltipData) return null;
return (
<AnimatePresence mode="wait">
@@ -23,12 +25,16 @@ const MapTooltip = memo(function MapTooltip() {
transform: "translate(20%, -50%)",
}}
onMouseEnter={(e) => {
e.stopPropagation()
e.stopPropagation();
}}
>
<div>
<p className="font-medium">{tooltipData.country === "China" ? "Mainland China" : tooltipData.country}</p>
<p className="text-neutral-600 dark:text-neutral-400 mb-1">
<p className="font-medium">
{tooltipData.country === "China"
? "Mainland China"
: tooltipData.country}
</p>
<p className="text-neutral-600 dark:text-neutral-400 text-xs font-light mb-1">
{tooltipData.count} {t("map.Servers")}
</p>
</div>
@@ -39,16 +45,26 @@ const MapTooltip = memo(function MapTooltip() {
overflowY: "auto",
}}
>
{tooltipData.servers.map((server, index: number) => (
<div key={index} className="flex items-center gap-1.5 py-0.5">
<span className={`w-1.5 h-1.5 shrink-0 rounded-full ${server.status ? "bg-green-500" : "bg-red-500"}`}></span>
{tooltipData.servers.map((server) => (
<button
key={server.id}
type="button"
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>
</div>
</button>
))}
</div>
</m.div>
</AnimatePresence>
)
})
);
});
export default MapTooltip
export default MapTooltip;
+583 -153
View File
@@ -1,56 +1,200 @@
"use client"
"use client";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
import { fetchMonitor } from "@/lib/nezha-api"
import { cn, formatTime } from "@/lib/utils"
import { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api"
import { useQuery } from "@tanstack/react-query"
import * as React from "react"
import { useCallback, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
import { useQuery } from "@tanstack/react-query";
import { m } from "framer-motion";
import * as React from "react";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
Area,
CartesianGrid,
ComposedChart,
Line,
XAxis,
YAxis,
} from "recharts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
fetchLoginUser,
fetchMonitor,
type MonitorPeriod,
} from "@/lib/nezha-api";
import { cn, formatTime } from "@/lib/utils";
import type { NezhaMonitor, ServerMonitorChart } from "@/types/nezha-api";
import NetworkChartLoading from "./NetworkChartLoading"
import { Label } from "./ui/label"
import { Switch } from "./ui/switch"
import NetworkChartLoading from "./NetworkChartLoading";
import { Label } from "./ui/label";
import { Switch } from "./ui/switch";
interface ResultItem {
created_at: number
[key: string]: number
created_at: number;
[key: string]: number;
}
export function NetworkChart({ server_id, show }: { server_id: number; show: boolean }) {
const { t } = useTranslation()
/**
* Helper method to calculate packet loss from delay data
*/
const calculatePacketLoss = (delays: number[]): number[] => {
if (!delays || delays.length === 0) return [];
const packetLossRates: number[] = [];
const windowSize = Math.min(10, Math.max(3, Math.floor(delays.length / 10)));
const timeoutThreshold = 3000;
const extremeDelayThreshold = 10000;
for (let i = 0; i < delays.length; i++) {
const currentDelay = delays[i];
let lossRate = 0;
if (
currentDelay === 0 ||
currentDelay === null ||
currentDelay === undefined
) {
lossRate = 100;
} else if (currentDelay >= extremeDelayThreshold) {
lossRate = Math.min(
95,
60 + (currentDelay - extremeDelayThreshold) / 1000,
);
} else if (currentDelay >= timeoutThreshold) {
lossRate = Math.min(50, (currentDelay - timeoutThreshold) / 200);
} else {
const start = Math.max(0, i - Math.floor(windowSize / 2));
const end = Math.min(delays.length, i + Math.ceil(windowSize / 2));
const windowDelays = delays.slice(start, end).filter((d) => d > 0);
if (windowDelays.length > 2) {
const mean =
windowDelays.reduce((sum, d) => sum + d, 0) / windowDelays.length;
const variance =
windowDelays.reduce((sum, d) => sum + (d - mean) ** 2, 0) /
windowDelays.length;
const standardDeviation = Math.sqrt(variance);
const coefficientOfVariation = standardDeviation / mean;
if (coefficientOfVariation > 0.8) {
lossRate = Math.min(25, coefficientOfVariation * 15);
} else if (coefficientOfVariation > 0.5) {
lossRate = Math.min(10, coefficientOfVariation * 8);
} else if (coefficientOfVariation > 0.3) {
lossRate = Math.min(5, coefficientOfVariation * 5);
}
if (currentDelay > mean * 2.5) {
lossRate += Math.min(15, (currentDelay / mean - 2.5) * 10);
}
}
}
if (i > 0) {
const alpha = 0.3;
lossRate = alpha * lossRate + (1 - alpha) * packetLossRates[i - 1];
}
packetLossRates.push(Math.max(0, Math.min(100, lossRate)));
}
return packetLossRates.map((rate) => Number(rate.toFixed(2)));
};
export function NetworkChart({
server_id,
show,
}: {
server_id: number;
show: boolean;
}) {
const { t } = useTranslation();
const [period, setPeriod] = React.useState<MonitorPeriod>("1d");
const { data: userData, isError: isLoginError } = useQuery({
queryKey: ["login-user"],
queryFn: () => fetchLoginUser(),
refetchOnMount: false,
refetchOnWindowFocus: true,
refetchIntervalInBackground: true,
refetchInterval: 1000 * 30,
retry: 0,
});
const isLogin = isLoginError
? false
: userData
? !!userData?.data?.id && !!document.cookie
: false;
React.useEffect(() => {
if (!isLogin && period !== "1d") {
setPeriod("1d");
}
}, [isLogin, period]);
const { data: monitorData } = useQuery({
queryKey: ["monitor", server_id],
queryFn: () => fetchMonitor(server_id),
queryKey: ["monitor", server_id, period],
queryFn: () => fetchMonitor(server_id, period),
enabled: show,
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchInterval: 10000,
})
});
if (!monitorData) return <NetworkChartLoading />
if (!monitorData) return <NetworkChartLoading />;
if (monitorData?.success && !monitorData.data) {
return (
<>
<div className="flex flex-col items-center justify-center">
<p className="text-sm font-medium opacity-40"></p>
<p className="text-sm font-medium opacity-40 mb-4">{t("monitor.noData")}</p>
<p className="text-sm font-medium opacity-40 mb-4">
{t("monitor.noData")}
</p>
</div>
<NetworkChartLoading />
</>
)
);
}
const transformedData = transformData(monitorData.data)
const transformedData = transformData(monitorData.data);
const formattedData = formatData(monitorData.data)
const formattedData = formatData(monitorData.data);
const chartDataKey = Object.keys(transformedData)
const monitorInfoByName = new Map(
monitorData.data.map((item) => [
item.monitor_name,
{ id: item.monitor_id, displayIndex: item.display_index },
]),
);
const chartDataKey = Object.keys(transformedData).sort((a, b) => {
const aInfo = monitorInfoByName.get(a);
const bInfo = monitorInfoByName.get(b);
if (!aInfo && !bInfo) return a.localeCompare(b);
if (!aInfo) return 1;
if (!bInfo) return -1;
const indexDiff = (bInfo.displayIndex ?? 0) - (aInfo.displayIndex ?? 0);
if (indexDiff !== 0) return indexDiff;
return aInfo.id - bInfo.id;
});
const initChartConfig = {
avg_delay: {
@@ -59,10 +203,10 @@ export function NetworkChart({ server_id, show }: { server_id: number; show: boo
...chartDataKey.reduce((acc, key) => {
acc[key] = {
label: key,
}
return acc
};
return acc;
}, {} as ChartConfig),
} satisfies ChartConfig
} satisfies ChartConfig;
return (
<NetworkChartClient
@@ -71,8 +215,11 @@ export function NetworkChart({ server_id, show }: { server_id: number; show: boo
chartData={transformedData}
serverName={monitorData.data[0].server_name}
formattedData={formattedData}
period={period}
onPeriodChange={setPeriod}
isLogin={isLogin}
/>
)
);
}
export const NetworkChartClient = React.memo(function NetworkChart({
@@ -81,83 +228,185 @@ export const NetworkChartClient = React.memo(function NetworkChart({
chartData,
serverName,
formattedData,
period,
onPeriodChange,
isLogin,
}: {
chartDataKey: string[]
chartConfig: ChartConfig
chartData: ServerMonitorChart
serverName: string
formattedData: ResultItem[]
chartDataKey: string[];
chartConfig: ChartConfig;
chartData: ServerMonitorChart;
serverName: string;
formattedData: ResultItem[];
period: MonitorPeriod;
onPeriodChange: (period: MonitorPeriod) => void;
isLogin: boolean;
}) {
const { t } = useTranslation()
const { t } = useTranslation();
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
const TIME_RANGE_OPTIONS: { value: MonitorPeriod; label: string }[] = [
{ value: "1d", label: t("monitor.period1d") },
{ value: "7d", label: t("monitor.period7d") },
{ value: "30d", label: t("monitor.period30d") },
];
const forcePeakCutEnabled = (window.ForcePeakCutEnabled as boolean) ?? false
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const forcePeakCutEnabled = (window.ForcePeakCutEnabled as boolean) ?? false;
// Change from string to string array for multi-selection
const [activeCharts, setActiveCharts] = React.useState<string[]>([])
const [isPeakEnabled, setIsPeakEnabled] = React.useState(forcePeakCutEnabled)
const [activeCharts, setActiveCharts] = React.useState<string[]>([]);
const [isPeakEnabled, setIsPeakEnabled] = React.useState(forcePeakCutEnabled);
// Function to clear all selected charts
const clearAllSelections = useCallback(() => {
setActiveCharts([])
}, [])
setActiveCharts([]);
}, []);
// Updated to handle multiple selections
const handleButtonClick = useCallback((chart: string) => {
setActiveCharts((prev) => {
// If chart is already selected, remove it
if (prev.includes(chart)) {
return prev.filter((c) => c !== chart)
return prev.filter((c) => c !== chart);
}
// Otherwise, add it to selected charts
return [...prev, chart]
})
}, [])
return [...prev, chart];
});
}, []);
const getColorByIndex = useCallback(
(chart: string) => {
const index = chartDataKey.indexOf(chart)
return `hsl(var(--chart-${(index % 10) + 1}))`
const index = chartDataKey.indexOf(chart);
return `hsl(var(--chart-${(index % 10) + 1}))`;
},
[chartDataKey],
)
);
const chartStats = useMemo(() => {
const stats: { [key: string]: { minDelay: number; maxDelay: number } } = {};
for (const key of chartDataKey) {
const data = chartData[key] || [];
if (data.length > 0) {
const delays = data.map((item) => item.avg_delay);
const minDelay = Math.min(...delays);
const maxDelay = Math.max(...delays);
stats[key] = { minDelay, maxDelay };
} else {
stats[key] = { minDelay: 0, maxDelay: 0 };
}
}
return stats;
}, [chartDataKey, chartData]);
const chartButtons = useMemo(
() =>
chartDataKey.map((key) => (
chartDataKey.map((key) => {
const monitorData = chartData[key];
const lastDelay = monitorData[monitorData.length - 1].avg_delay;
const stats = chartStats[key];
// Calculate average packet loss if available
const packetLossData = monitorData.reduce<number[]>((acc, item) => {
if (item.packet_loss !== undefined) {
acc.push(item.packet_loss);
}
return acc;
}, []);
const avgPacketLoss =
packetLossData.length > 0
? packetLossData.reduce((sum, loss) => sum + loss, 0) /
packetLossData.length
: null;
return (
<button
key={key}
data-active={activeCharts.includes(key)}
className={`relative z-30 flex cursor-pointer grow basis-0 flex-col justify-center gap-1 border-b border-neutral-200 dark:border-neutral-800 px-6 py-4 text-left data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-6`}
onClick={() => handleButtonClick(key)}
>
<span className="whitespace-nowrap text-xs text-muted-foreground">{key}</span>
<span className="text-md font-bold leading-none sm:text-lg">{chartData[key][chartData[key].length - 1].avg_delay.toFixed(2)}ms</span>
<span className="whitespace-nowrap text-xs text-muted-foreground">
{key}
</span>
<div className="flex flex-col gap-0.5">
<span className="text-md font-semibold leading-none sm:text-xl">
{lastDelay.toFixed(2)}ms
</span>
<div className="flex items-center gap-2 text-[12px]">
<span className="text-green-600 dark:text-green-400">
{stats.minDelay.toFixed(0)}
</span>
<span className="text-red-600 dark:text-red-500">
{stats.maxDelay.toFixed(0)}
</span>
{avgPacketLoss !== null && (
<span className="text-muted-foreground flex items-center gap-1">
{avgPacketLoss.toFixed(2)}%
</span>
)}
</div>
</div>
</button>
)),
[chartDataKey, activeCharts, chartData, handleButtonClick],
)
);
}),
[chartDataKey, activeCharts, chartData, chartStats, handleButtonClick],
);
const chartLines = useMemo(() => {
// If we have active charts selected, render only those
if (activeCharts.length > 0) {
return activeCharts.map((chart) => (
const chartElements = useMemo(() => {
const elements = [];
// If exactly one chart is selected, show delay line and packet loss area
if (activeCharts.length === 1) {
const chart = activeCharts[0];
elements.push(
<Area
key="packet-loss-area"
isAnimationActive={false}
dataKey="packet_loss"
stroke="none"
fill="hsl(45, 100%, 60%)"
fillOpacity={0.3}
yAxisId="packet-loss"
/>,
<Line
key="delay-line"
isAnimationActive={false}
strokeWidth={1}
type="linear"
dot={false}
dataKey="avg_delay"
stroke={getColorByIndex(chart)}
yAxisId="delay"
connectNulls={true}
/>,
);
} else if (activeCharts.length > 1) {
// Multiple charts selected - show only delay lines for selected monitors
elements.push(
...activeCharts.map((chart) => (
<Line
key={chart}
isAnimationActive={false}
strokeWidth={1}
type="linear"
dot={false}
dataKey={chart} // Change from "avg_delay" to the actual chart key name
dataKey={chart}
stroke={getColorByIndex(chart)}
name={chart}
connectNulls={true}
yAxisId="delay"
/>
))
}
// Otherwise show all charts (default view)
return chartDataKey.map((key) => (
)),
);
} else {
// No selection - show all charts (default view)
elements.push(
...chartDataKey.map((key) => (
<Line
key={key}
isAnimationActive={false}
@@ -167,106 +416,206 @@ export const NetworkChartClient = React.memo(function NetworkChart({
dataKey={key}
stroke={getColorByIndex(key)}
connectNulls={true}
yAxisId="delay"
/>
))
}, [activeCharts, chartDataKey, getColorByIndex])
const processedData = useMemo(() => {
if (!isPeakEnabled) {
// Always use formattedData when multiple charts are selected or none selected
return formattedData
)),
);
}
// For peak cutting, always use the formatted data which contains all series
const data = formattedData
return elements;
}, [activeCharts, chartDataKey, getColorByIndex]);
const windowSize = 11 // 增加窗口大小以获取更好的统计效果
const alpha = 0.3 // EWMA平滑因子
const processedData = useMemo(() => {
// Special handling for single chart selection
let baseData = formattedData;
if (activeCharts.length === 1) {
const selectedChart = activeCharts[0];
baseData = chartData[selectedChart].map((item) => ({
created_at: item.created_at,
avg_delay: item.avg_delay,
packet_loss: item.packet_loss ?? 0,
}));
}
if (!isPeakEnabled) {
return baseData;
}
// For peak cutting, use the base data
const data = baseData;
const windowSize = 11; // 增加窗口大小以获取更好的统计效果
const alpha = 0.3; // EWMA平滑因子
// 辅助函数:计算中位数
const getMedian = (arr: number[]) => {
const sorted = [...arr].sort((a, b) => a - b)
const mid = Math.floor(sorted.length / 2)
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2
}
const sorted = [...arr].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2
? sorted[mid]
: (sorted[mid - 1] + sorted[mid]) / 2;
};
// 辅助函数:异常值处理
const processValues = (values: number[]) => {
if (values.length === 0) return null
if (values.length === 0) return null;
const median = getMedian(values)
const deviations = values.map((v) => Math.abs(v - median))
const medianDeviation = getMedian(deviations) * 1.4826 // MAD估计器
const median = getMedian(values);
const deviations = values.map((v) => Math.abs(v - median));
const medianDeviation = getMedian(deviations) * 1.4826; // MAD估计器
// 使用中位数绝对偏差(MAD)进行异常值检测
const validValues = values.filter(
(v) =>
Math.abs(v - median) <= 3 * medianDeviation && // 更严格的异常值判定
v <= median * 3, // 限制最大值不超过中位数的3倍
)
);
if (validValues.length === 0) return median // 如果没有有效值,返回中位数
if (validValues.length === 0) return median; // 如果没有有效值,返回中位数
// 计算EWMA
let ewma = validValues[0]
let ewma = validValues[0];
for (let i = 1; i < validValues.length; i++) {
ewma = alpha * validValues[i] + (1 - alpha) * ewma
ewma = alpha * validValues[i] + (1 - alpha) * ewma;
}
return ewma
}
return ewma;
};
// 初始化EWMA历史值
const ewmaHistory: { [key: string]: number } = {}
const ewmaHistory: { [key: string]: number } = {};
return data.map((point, index) => {
if (index < windowSize - 1) return point
if (index < windowSize - 1) return point;
const window = data.slice(index - windowSize + 1, index + 1)
const smoothed = { ...point } as ResultItem
const window = data.slice(index - windowSize + 1, index + 1);
const smoothed = { ...point } as ResultItem;
// Process all chart keys or just the selected ones
const keysToProcess = activeCharts.length > 0 ? activeCharts : chartDataKey
keysToProcess.forEach((key) => {
const values = window.map((w) => w[key]).filter((v) => v !== undefined && v !== null) as number[]
// Special handling for single chart selection
if (activeCharts.length === 1) {
// Process avg_delay for single chart
const values = window
.map((w) => w.avg_delay as number)
.filter((v) => v !== undefined && v !== null);
if (values.length > 0) {
const processed = processValues(values)
const processed = processValues(values);
if (processed !== null) {
if (ewmaHistory.avg_delay === undefined) {
ewmaHistory.avg_delay = processed;
} else {
ewmaHistory.avg_delay =
alpha * processed + (1 - alpha) * ewmaHistory.avg_delay;
}
smoothed.avg_delay = ewmaHistory.avg_delay;
}
}
} else {
// Process all chart keys or just the selected ones
const keysToProcess =
activeCharts.length > 0 ? activeCharts : chartDataKey;
keysToProcess.forEach((key) => {
const values = window
.map((w) => w[key])
.filter((v) => v !== undefined && v !== null) as number[];
if (values.length > 0) {
const processed = processValues(values);
if (processed !== null) {
// Apply EWMA smoothing
if (ewmaHistory[key] === undefined) {
ewmaHistory[key] = processed
ewmaHistory[key] = processed;
} else {
ewmaHistory[key] = alpha * processed + (1 - alpha) * ewmaHistory[key]
ewmaHistory[key] =
alpha * processed + (1 - alpha) * ewmaHistory[key];
}
smoothed[key] = ewmaHistory[key]
smoothed[key] = ewmaHistory[key];
}
}
})
});
}
return smoothed
})
}, [isPeakEnabled, activeCharts, formattedData, chartDataKey])
return smoothed;
});
}, [isPeakEnabled, activeCharts, formattedData, chartData, chartDataKey]);
return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3 sm:-mt-5 -mt-3 flex-wrap">
<TooltipProvider delayDuration={120}>
<div className="flex items-center gap-1 rounded-full bg-muted dark:bg-muted/40 p-0.5 border border-border/60 dark:border-border">
{TIME_RANGE_OPTIONS.map((option) => {
const isLocked = !isLogin && option.value !== "1d";
const optionItem = (
<div
onClick={() => {
if (!isLocked) {
onPeriodChange(option.value);
}
}}
className={cn(
"relative cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors duration-300",
period === option.value
? "text-foreground"
: "text-muted-foreground hover:text-foreground",
isLocked && "cursor-not-allowed opacity-40 grayscale",
)}
>
{period === option.value && (
<m.div
layoutId="network-period-selector-active"
className="absolute inset-0 z-10 h-full w-full bg-white dark:bg-background rounded-full ring-1 ring-border/60 dark:ring-border/40"
transition={{
type: "spring",
stiffness: 400,
damping: 30,
}}
/>
)}
<span className="relative z-20">{option.label}</span>
</div>
);
if (isLocked) {
return (
<Tooltip key={option.value}>
<TooltipTrigger asChild>{optionItem}</TooltipTrigger>
<TooltipContent>
{t("monitor.loginRequired", "Please login to view")}
</TooltipContent>
</Tooltip>
);
}
return <div key={option.value}>{optionItem}</div>;
})}
</div>
</TooltipProvider>
<div className="flex items-center space-x-2">
<Switch
id="Peak"
checked={isPeakEnabled}
onCheckedChange={setIsPeakEnabled}
/>
<Label className="text-xs" htmlFor="Peak">
{t("monitor.peakCut")}
</Label>
</div>
</div>
<Card
className={cn({
"bg-card/70": customBackgroundImage,
})}
>
<CardHeader className="flex flex-col items-stretch space-y-0 p-0 sm:flex-row">
<CardHeader className="flex flex-col items-stretch space-y-0 overflow-hidden rounded-t-lg p-0 sm:flex-row">
<div className="flex flex-none flex-col justify-center gap-1 border-b px-6 py-4">
<CardTitle className="flex flex-none items-center gap-0.5 text-md">{serverName}</CardTitle>
<CardTitle className="flex flex-none items-center gap-0.5 text-md">
{serverName}
</CardTitle>
<CardDescription className="text-xs">
{chartDataKey.length} {t("monitor.monitorCount")}
</CardDescription>
<div className="flex items-center mt-0.5 space-x-2">
<Switch id="Peak" checked={isPeakEnabled} onCheckedChange={setIsPeakEnabled} />
<Label className="text-xs" htmlFor="Peak">
Peak cut
</Label>
</div>
</div>
<div className="flex flex-wrap w-full">{chartButtons}</div>
</CardHeader>
@@ -274,14 +623,21 @@ export const NetworkChartClient = React.memo(function NetworkChart({
<div className="relative">
{activeCharts.length > 0 && (
<button
className="absolute -top-2 right-1 z-10 text-xs px-2 py-1 bg-stone-100/80 dark:bg-stone-800/80 backdrop-blur-sm rounded-[5px] text-muted-foreground hover:text-foreground transition-colors"
className="absolute -top-2 right-1 z-10 text-xs px-2 py-1 bg-stone-100/80 dark:bg-stone-800/80 backdrop-blur-xs rounded-[5px] text-muted-foreground hover:text-foreground transition-colors"
onClick={clearAllSelections}
>
{t("monitor.clearSelections", "Clear")} ({activeCharts.length})
</button>
)}
<ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full">
<LineChart accessibilityLayer data={processedData} margin={{ left: 12, right: 12 }}>
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<ComposedChart
accessibilityLayer
data={processedData}
margin={{ left: 12, right: 12 }}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="created_at"
@@ -293,30 +649,58 @@ export const NetworkChartClient = React.memo(function NetworkChart({
ticks={processedData
.filter((item, index, array) => {
if (array.length < 6) {
return index === 0 || index === array.length - 1
return index === 0 || index === array.length - 1;
}
// 计算数据的总时间跨度(毫秒)
const timeSpan = array[array.length - 1].created_at - array[0].created_at
const hours = timeSpan / (1000 * 60 * 60)
const timeSpan =
array[array.length - 1].created_at -
array[0].created_at;
const hours = timeSpan / (1000 * 60 * 60);
// 根据时间跨度调整显示间隔
if (hours <= 12) {
// 12小时内,每60分钟显示一个刻度
return index === 0 || index === array.length - 1 || new Date(item.created_at).getMinutes() % 60 === 0
return (
index === 0 ||
index === array.length - 1 ||
new Date(item.created_at).getMinutes() % 60 === 0
);
}
// 超过12小时,每2小时显示一个刻度
const date = new Date(item.created_at)
return date.getMinutes() === 0 && date.getHours() % 2 === 0
const date = new Date(item.created_at);
return (
date.getMinutes() === 0 && date.getHours() % 2 === 0
);
})
.map((item) => item.created_at)}
tickFormatter={(value) => {
const date = new Date(value)
const minutes = date.getMinutes()
return minutes === 0 ? `${date.getHours()}:00` : `${date.getHours()}:${minutes}`
const date = new Date(value);
const minutes = date.getMinutes();
return minutes === 0
? `${date.getHours()}:00`
: `${date.getHours()}:${minutes}`;
}}
/>
<YAxis tickLine={false} axisLine={false} tickMargin={15} minTickGap={20} tickFormatter={(value) => `${value}ms`} />
<YAxis
yAxisId="delay"
tickLine={false}
axisLine={false}
tickMargin={15}
minTickGap={20}
tickFormatter={(value) => `${value}ms`}
/>
{activeCharts.length === 1 && (
<YAxis
yAxisId="packet-loss"
orientation="right"
tickLine={false}
axisLine={false}
tickMargin={15}
minTickGap={20}
tickFormatter={(value) => `${value}%`}
/>
)}
<ChartTooltip
isAnimationActive={false}
content={
@@ -324,65 +708,111 @@ export const NetworkChartClient = React.memo(function NetworkChart({
indicator={"line"}
labelKey="created_at"
labelFormatter={(_, payload) => {
return formatTime(payload[0].payload.created_at)
return formatTime(payload[0].payload.created_at);
}}
formatter={(value, name) => {
let formattedValue: string;
let label: string;
if (name === "packet_loss") {
formattedValue = `${Number(value).toFixed(2)}%`;
label = t("monitor.packetLoss", "Packet Loss");
} else if (name === "avg_delay") {
formattedValue = `${Number(value).toFixed(2)}ms`;
label = t("monitor.avgDelay", "Avg Delay");
} else {
// For monitor names (in multi-chart view) - delay data
formattedValue = `${Number(value).toFixed(2)}ms`;
label = name as string;
}
return (
<div className="flex flex-1 items-center justify-between leading-none">
<span className="text-muted-foreground">
{label}
</span>
<span className="ml-2 font-medium text-foreground tabular-nums">
{formattedValue}
</span>
</div>
);
}}
/>
}
/>
{activeCharts.length !== 1 && (
<ChartLegend content={<ChartLegendContent />} />
{chartLines}
</LineChart>
)}
{chartElements}
</ComposedChart>
</ChartContainer>
</div>
</CardContent>
</Card>
)
})
</div>
);
});
const transformData = (data: NezhaMonitor[]) => {
const monitorData: ServerMonitorChart = {}
const monitorData: ServerMonitorChart = {};
data.forEach((item) => {
const monitorName = item.monitor_name
const monitorName = item.monitor_name;
if (!monitorData[monitorName]) {
monitorData[monitorName] = []
monitorData[monitorName] = [];
}
// Calculate packet loss from delay data if not provided
const packetLoss = item.packet_loss || calculatePacketLoss(item.avg_delay);
for (let i = 0; i < item.created_at.length; i++) {
monitorData[monitorName].push({
created_at: item.created_at[i],
avg_delay: item.avg_delay[i],
})
packet_loss: packetLoss[i],
});
}
})
});
return monitorData
}
return monitorData;
};
const formatData = (rawData: NezhaMonitor[]) => {
const result: { [time: number]: ResultItem } = {}
const result: { [time: number]: ResultItem } = {};
const allTimes = new Set<number>()
const allTimes = new Set<number>();
rawData.forEach((item) => {
item.created_at.forEach((time) => allTimes.add(time))
})
item.created_at.forEach((time) => {
allTimes.add(time);
});
});
const allTimeArray = Array.from(allTimes).sort((a, b) => a - b)
const allTimeArray = Array.from(allTimes).sort((a, b) => a - b);
rawData.forEach((item) => {
const { monitor_name, created_at, avg_delay } = item
const { monitor_name, created_at, avg_delay } = item;
// Calculate packet loss if not provided
const packetLoss = item.packet_loss || calculatePacketLoss(avg_delay);
allTimeArray.forEach((time) => {
if (!result[time]) {
result[time] = { created_at: time }
result[time] = { created_at: time };
}
const timeIndex = created_at.indexOf(time)
const timeIndex = created_at.indexOf(time);
// @ts-expect-error - avg_delay is an array
result[time][monitor_name] = timeIndex !== -1 ? avg_delay[timeIndex] : null
})
})
return Object.values(result).sort((a, b) => a.created_at - b.created_at)
result[time][monitor_name] =
timeIndex !== -1 ? avg_delay[timeIndex] : null;
// Add packet loss data if available
if (packetLoss) {
// @ts-expect-error - packet_loss is calculated
result[time][`${monitor_name}_packet_loss`] =
timeIndex !== -1 ? packetLoss[timeIndex] : null;
}
});
});
return Object.values(result).sort((a, b) => a.created_at - b.created_at);
};
+3 -3
View File
@@ -1,5 +1,5 @@
import { Loader } from "@/components/loading/Loader"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Loader } from "@/components/loading/Loader";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function NetworkChartLoading() {
return (
@@ -19,5 +19,5 @@ export default function NetworkChartLoading() {
<div className="aspect-auto h-[250px] w-full"></div>
</CardContent>
</Card>
)
);
}
+41 -14
View File
@@ -1,8 +1,12 @@
import { PublicNoteData, cn } from "@/lib/utils"
import { cn, type PublicNoteData } from "@/lib/utils";
export default function PlanInfo({ parsedData }: { parsedData: PublicNoteData }) {
export default function PlanInfo({
parsedData,
}: {
parsedData: PublicNoteData;
}) {
if (!parsedData || !parsedData.planDataMod) {
return null
return null;
}
const extraList =
@@ -10,36 +14,57 @@ export default function PlanInfo({ parsedData }: { parsedData: PublicNoteData })
? parsedData.planDataMod.extra.split(",")
: parsedData.planDataMod.extra.split(",")[0] === ""
? []
: [parsedData.planDataMod.extra]
: [parsedData.planDataMod.extra];
const networkRoutes = parsedData.planDataMod.networkRoute
? parsedData.planDataMod.networkRoute.split(",")
: [];
return (
<section className="flex gap-1 items-center flex-wrap mt-0.5">
{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
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]",
)}
>
{parsedData.planDataMod.bandwidth}
</p>
)}
{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]")}>
<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.trafficVol}
</p>
)}
{parsedData.planDataMod.IPv4 === "1" && (
<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]")}
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]",
)}
>
IPv4
</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]")}>
<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]",
)}
>
IPv6
</p>
)}
{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) => {
return route + (index === parsedData.planDataMod!.networkRoute.split(",").length - 1 ? "" : "")
<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]",
)}
>
{networkRoutes.map((route, index) => {
return route + (index === networkRoutes.length - 1 ? "" : "");
})}
</p>
)}
@@ -47,12 +72,14 @@ export default function PlanInfo({ parsedData }: { parsedData: PublicNoteData })
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]")}
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>
)
);
}
+17 -5
View File
@@ -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({
value,
className,
}: {
value: number;
className?: string;
}) {
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"}
indicatorClassName={
value < 30
? "bg-red-500"
: value < 70
? "bg-orange-400"
: "bg-green-500"
}
className={cn("h-[3px] rounded-sm w-[70px]", className)}
/>
)
);
}
+31
View File
@@ -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>
);
}
+130 -46
View File
@@ -1,41 +1,63 @@
import ServerFlag from "@/components/ServerFlag"
import ServerUsageBar from "@/components/ServerUsageBar"
import { formatBytes } from "@/lib/format"
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class"
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils"
import { NezhaServer } from "@/types/nezha-api"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import ServerFlag from "@/components/ServerFlag";
import ServerUsageBar from "@/components/ServerUsageBar";
import { formatBytes } from "@/lib/format";
import {
GetFontLogoClass,
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"
import BillingInfo from "./billingInfo"
import { Badge } from "./ui/badge"
import { Card } from "./ui/card"
export default function ServerCard({ now, serverInfo }: { 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(
export default function ServerCard({
now,
serverInfo,
)
}: {
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);
const cardClick = () => {
sessionStorage.setItem("fromMainPage", "true")
navigate(`/server/${serverInfo.id}`)
}
sessionStorage.setItem("fromMainPage", "true");
navigate(`/server/${serverInfo.id}`);
};
const showFlag = true
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
const showNetTransfer = window.ShowNetTransfer as boolean;
// @ts-expect-error FixedTopServerName is a global variable
const fixedTopServerName = window.FixedTopServerName as boolean
const fixedTopServerName = window.FixedTopServerName as boolean;
const parsedData = parsePublicNote(public_note)
const parsedData = parsePublicNote(public_note);
return online ? (
<Card
@@ -58,17 +80,31 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
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")}>
<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", showFlag ? "text-xs " : "text-sm")}>{name}</p>
<p
className={cn(
"break-normal font-bold tracking-tight",
showFlag ? "text-xs " : "text-sm",
)}
>
{name}
</p>
<div
className={cn("hidden lg:block", {
"lg:hidden": fixedTopServerName,
})}
>
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
{parsedData?.billingDataMod && (
<BillingInfo parsedData={parsedData} />
)}
</div>
</div>
</section>
@@ -86,7 +122,11 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
})}
>
{fixedTopServerName && (
<div className={"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"}>
<div
className={
"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"
}
>
<div className="text-xs font-semibold">
{platform.includes("Windows") ? (
<MageMicrosoftWindows className="size-[10px]" />
@@ -95,36 +135,64 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
)}
</div>
<div className={"flex w-14 flex-col"}>
<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>
<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>
)}
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{"CPU"}</p>
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div>
<div className="flex items-center text-xs font-semibold">
{cpu.toFixed(2)}%
</div>
<ServerUsageBar value={cpu} />
</div>
<div className={"flex w-14 flex-col"}>
<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="text-xs text-muted-foreground">
{t("serverCard.mem")}
</p>
<div className="flex items-center text-xs font-semibold">
{mem.toFixed(2)}%
</div>
<ServerUsageBar value={mem} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p>
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div>
<p className="text-xs text-muted-foreground">
{t("serverCard.stg")}
</p>
<div className="flex items-center text-xs font-semibold">
{stg.toFixed(2)}%
</div>
<ServerUsageBar value={stg} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p>
<p className="text-xs text-muted-foreground">
{t("serverCard.upload")}
</p>
<div className="flex items-center text-xs font-semibold">
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : up >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`}
{up >= 1024
? `${(up / 1024).toFixed(2)}G/s`
: up >= 1
? `${up.toFixed(2)}M/s`
: `${(up * 1024).toFixed(2)}K/s`}
</div>
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p>
<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`}
{down >= 1024
? `${(down / 1024).toFixed(2)}G/s`
: down >= 1
? `${down.toFixed(2)}M/s`
: `${(down * 1024).toFixed(2)}K/s`}
</div>
</div>
</section>
@@ -151,7 +219,9 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
<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]",
showNetTransfer
? "lg:min-h-[91px] min-h-[123px]"
: "lg:min-h-[61px] min-h-[93px]",
{
"flex-col": fixedTopServerName,
"lg:flex-row": !fixedTopServerName,
@@ -169,17 +239,31 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
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")}>
<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>
<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} />}
{parsedData?.billingDataMod && (
<BillingInfo parsedData={parsedData} />
)}
</div>
</div>
</section>
@@ -192,5 +276,5 @@ export default function ServerCard({ now, serverInfo }: { now: number; serverInf
</div>
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</Card>
)
);
}
+147 -50
View File
@@ -1,35 +1,58 @@
import ServerFlag from "@/components/ServerFlag"
import ServerUsageBar from "@/components/ServerUsageBar"
import { formatBytes } from "@/lib/format"
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class"
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils"
import { NezhaServer } from "@/types/nezha-api"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import ServerFlag from "@/components/ServerFlag";
import ServerUsageBar from "@/components/ServerUsageBar";
import { formatBytes } from "@/lib/format";
import {
GetFontLogoClass,
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"
import BillingInfo from "./billingInfo"
import { Card } from "./ui/card"
import { Separator } from "./ui/separator"
export default function ServerCardInline({ now, serverInfo }: { 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(
export default function ServerCardInline({
now,
serverInfo,
)
}: {
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);
const cardClick = () => {
sessionStorage.setItem("fromMainPage", "true")
navigate(`/server/${serverInfo.id}`)
}
sessionStorage.setItem("fromMainPage", "true");
navigate(`/server/${serverInfo.id}`);
};
const showFlag = true
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>
@@ -42,20 +65,39 @@ export default function ServerCardInline({ now, serverInfo }: { now: number; ser
)}
onClick={cardClick}
>
<section className={cn("grid items-center gap-2 lg:w-36")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
<section
className={cn("grid items-center gap-2 lg:w-36")}
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")}>
<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 w-28 flex flex-col">
<p className={cn("break-normal font-bold tracking-tight", showFlag ? "text-xs " : "text-sm")}>{name}</p>
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
<p
className={cn(
"break-normal font-bold tracking-tight",
showFlag ? "text-xs " : "text-sm",
)}
>
{name}
</p>
{parsedData?.billingDataMod && (
<BillingInfo parsedData={parsedData} />
)}
</div>
</section>
<Separator orientation="vertical" className="h-8 mx-0 ml-2" />
<div className="flex flex-col gap-1">
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}>
<div className={"items-center flex flex-row gap-2 whitespace-nowrap"}>
<div
className={"items-center flex flex-row gap-2 whitespace-nowrap"}
>
<div className="text-xs font-semibold">
{platform.includes("Windows") ? (
<MageMicrosoftWindows className="size-[10px]" />
@@ -64,12 +106,20 @@ export default function ServerCardInline({ now, serverInfo }: { now: number; ser
)}
</div>
<div className={"flex w-14 flex-col"}>
<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>
<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>
<div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.uptime")}</p>
<p className="text-xs text-muted-foreground">
{t("serverCard.uptime")}
</p>
<div className="flex items-center text-xs font-semibold">
{uptime / 86400 >= 1
? `${(uptime / 86400).toFixed(0)} ${t("serverCard.days")}`
@@ -78,38 +128,68 @@ export default function ServerCardInline({ now, serverInfo }: { now: number; ser
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{"CPU"}</p>
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div>
<div className="flex items-center text-xs font-semibold">
{cpu.toFixed(2)}%
</div>
<ServerUsageBar value={cpu} />
</div>
<div className={"flex w-14 flex-col"}>
<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="text-xs text-muted-foreground">
{t("serverCard.mem")}
</p>
<div className="flex items-center text-xs font-semibold">
{mem.toFixed(2)}%
</div>
<ServerUsageBar value={mem} />
</div>
<div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p>
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div>
<p className="text-xs text-muted-foreground">
{t("serverCard.stg")}
</p>
<div className="flex items-center text-xs font-semibold">
{stg.toFixed(2)}%
</div>
<ServerUsageBar value={stg} />
</div>
<div className={"flex w-16 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p>
<p className="text-xs text-muted-foreground">
{t("serverCard.upload")}
</p>
<div className="flex items-center text-xs font-semibold">
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : up >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`}
{up >= 1024
? `${(up / 1024).toFixed(2)}G/s`
: up >= 1
? `${up.toFixed(2)}M/s`
: `${(up * 1024).toFixed(2)}K/s`}
</div>
</div>
<div className={"flex w-16 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p>
<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`}
{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>
<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>
<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} />}
@@ -126,18 +206,35 @@ export default function ServerCardInline({ now, serverInfo }: { now: number; ser
)}
onClick={cardClick}
>
<section className={cn("grid items-center gap-2 w-40")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
<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")}>
<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} />}
<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>
)
);
}
File diff suppressed because it is too large Load Diff
+127 -60
View File
@@ -1,58 +1,74 @@
import { BackIcon } from "@/components/Icon"
import ServerFlag from "@/components/ServerFlag"
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"
import { useWebSocketContext } from "@/hooks/use-websocket-context"
import { formatBytes } from "@/lib/format"
import { cn, formatNezhaInfo } from "@/lib/utils"
import { NezhaWebsocketResponse } from "@/types/nezha-api"
import countries from "i18n-iso-countries"
import enLocale from "i18n-iso-countries/langs/en.json"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import countries from "i18n-iso-countries";
import enLocale from "i18n-iso-countries/langs/en.json";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { BackIcon } from "@/components/Icon";
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading";
import ServerFlag from "@/components/ServerFlag";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { formatBytes } from "@/lib/format";
import { cn, formatNezhaInfo } from "@/lib/utils";
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "./ui/accordion";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./ui/tooltip";
export default function ServerDetailOverview({ server_id }: { server_id: string }) {
const { t } = useTranslation()
const navigate = useNavigate()
export default function ServerDetailOverview({
server_id,
}: {
server_id: string;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const [hasHistory, setHasHistory] = useState(false)
const [hasHistory, setHasHistory] = useState(false);
useEffect(() => {
const previousPath = sessionStorage.getItem("fromMainPage")
const previousPath = sessionStorage.getItem("fromMainPage");
if (previousPath) {
setHasHistory(true)
setHasHistory(true);
}
}, [])
}, []);
const { lastMessage, connected } = useWebSocketContext()
const { lastMessage, connected } = useWebSocketContext();
if (!connected && !lastMessage) {
return <ServerDetailLoading />
return <ServerDetailLoading />;
}
const linkClick = () => {
if (hasHistory) {
navigate(-1)
navigate(-1);
} 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) {
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) {
return <ServerDetailLoading />
return <ServerDetailLoading />;
}
const {
@@ -75,11 +91,14 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
net_in_transfer,
last_active_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 (
<div
@@ -98,12 +117,17 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<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.status")}</p>
<p className="text-xs text-muted-foreground">
{t("serverDetail.status")}
</p>
<Badge
className={cn("text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white", {
className={cn(
"text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white",
{
" bg-green-800": online,
" bg-red-600": !online,
})}
},
)}
>
{online ? t("serverDetail.online") : t("serverDetail.offline")}
</Badge>
@@ -114,7 +138,9 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<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.uptime")}</p>
<p className="text-xs text-muted-foreground">
{t("serverDetail.uptime")}
</p>
<div className="text-xs">
{" "}
{uptime / 86400 >= 1
@@ -129,7 +155,9 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<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.version")}</p>
<p className="text-xs text-muted-foreground">
{t("serverDetail.version")}
</p>
<div className="text-xs">{version} </div>
</section>
</CardContent>
@@ -139,7 +167,9 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<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>
<p className="text-xs text-muted-foreground">
{t("serverDetail.arch")}
</p>
<div className="text-xs">{arch} </div>
</section>
</CardContent>
@@ -150,7 +180,9 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<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.mem")}</p>
<p className="text-xs text-muted-foreground">
{t("serverDetail.mem")}
</p>
<div className="text-xs">{formatBytes(mem_total)}</div>
</section>
</CardContent>
@@ -161,7 +193,9 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<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.disk")}</p>
<p className="text-xs text-muted-foreground">
{t("serverDetail.disk")}
</p>
<div className="text-xs">{formatBytes(disk_total)}</div>
</section>
</CardContent>
@@ -175,10 +209,19 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<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.region")}</p>
<p className="text-xs text-muted-foreground">
{t("serverDetail.region")}
</p>
<section className="flex items-start gap-1">
<div className="text-xs text-start">{country_code?.toUpperCase()}</div>
{country_code && <ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} />}
<div className="text-xs text-start">
{country_code?.toUpperCase()}
</div>
{country_code && (
<ServerFlag
className="text-[11px] -mt-px"
country_code={country_code}
/>
)}
</section>
</section>
</CardContent>
@@ -196,10 +239,12 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<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.system")}</p>
<p className="text-xs text-muted-foreground">
{t("serverDetail.system")}
</p>
<div className="text-xs">
{" "}
{platform} {platform_version ? " - " + platform_version : ""}
{platform} {platform_version ? ` - ${platform_version}` : ""}
</div>
</section>
</CardContent>
@@ -241,9 +286,14 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<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.upload")}</p>
<p className="text-xs text-muted-foreground">
{t("serverDetail.upload")}
</p>
{net_out_transfer ? (
<div className="text-xs"> {formatBytes(net_out_transfer)} </div>
<div className="text-xs">
{" "}
{formatBytes(net_out_transfer)}{" "}
</div>
) : (
<div className="text-xs"> {t("serverDetail.unknown")}</div>
)}
@@ -255,9 +305,14 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<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.download")}</p>
<p className="text-xs text-muted-foreground">
{t("serverDetail.download")}
</p>
{net_in_transfer ? (
<div className="text-xs"> {formatBytes(net_in_transfer)} </div>
<div className="text-xs">
{" "}
{formatBytes(net_in_transfer)}{" "}
</div>
) : (
<div className="text-xs"> {t("serverDetail.unknown")}</div>
)}
@@ -267,16 +322,20 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
) : null}
</section>
<section className="flex flex-wrap gap-2 mt-1">
{server?.state.temperatures && server?.state.temperatures.length > 0 && (
{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>
<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
<p className="font-semibold">{item.Name}</p>:{" "}
{item.Temperature.toFixed(2)} °C
</div>
))}
</section>
@@ -291,20 +350,28 @@ export default function ServerDetailOverview({ server_id }: { server_id: string
<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.bootTime")}</p>
<div className="text-xs">{boot_time_string ? boot_time_string : "N/A"}</div>
<p className="text-xs text-muted-foreground">
{t("serverDetail.bootTime")}
</p>
<div className="text-xs">
{boot_time_string ? boot_time_string : "N/A"}
</div>
</section>
</CardContent>
</Card>
<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.lastActive")}</p>
<div className="text-xs">{last_active_time_string ? last_active_time_string : "N/A"}</div>
<p className="text-xs text-muted-foreground">
{t("serverDetail.lastActive")}
</p>
<div className="text-xs">
{last_active_time_string ? last_active_time_string : "N/A"}
</div>
</section>
</CardContent>
</Card>
</section>
</div>
)
);
}
+111
View File
@@ -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"}
/>
);
}
+34 -24
View File
@@ -1,42 +1,52 @@
import { cn } from "@/lib/utils"
import getUnicodeFlagIcon from "country-flag-icons/unicode"
import { useEffect, useState } from "react"
import getUnicodeFlagIcon from "country-flag-icons/unicode";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
export default function ServerFlag({ country_code, className }: { country_code: string; className?: string }) {
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false)
export default function ServerFlag({
country_code,
className,
}: {
country_code: string;
className?: string;
}) {
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false);
// @ts-expect-error ForceUseSvgFlag is a global variable
const forceUseSvgFlag = window.ForceUseSvgFlag as boolean
const forceUseSvgFlag = window.ForceUseSvgFlag as boolean;
useEffect(() => {
if (forceUseSvgFlag) {
// 如果环境变量要求直接使用 SVG,则无需检查 Emoji 支持
setSupportsEmojiFlags(false)
return
setSupportsEmojiFlags(false);
return;
}
const checkEmojiSupport = () => {
const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d")
const emojiFlag = "🇺🇸" // 使用美国国旗作为测试
if (!ctx) return
ctx.fillStyle = "#000"
ctx.textBaseline = "top"
ctx.font = "32px Arial"
ctx.fillText(emojiFlag, 0, 0)
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const emojiFlag = "🇺🇸"; // 使用美国国旗作为测试
if (!ctx) return;
ctx.fillStyle = "#000";
ctx.textBaseline = "top";
ctx.font = "32px Arial";
ctx.fillText(emojiFlag, 0, 0);
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0
setSupportsEmojiFlags(support)
}
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0;
setSupportsEmojiFlags(support);
};
checkEmojiSupport()
}, [])
checkEmojiSupport();
}, [forceUseSvgFlag]);
if (!country_code) return null
if (!country_code) return null;
return (
<span className={cn("text-[12px] text-muted-foreground", className)}>
{forceUseSvgFlag || !supportsEmojiFlags ? <span className={`fi fi-${country_code}`} /> : getUnicodeFlagIcon(country_code)}
{forceUseSvgFlag || !supportsEmojiFlags ? (
<span className={`fi fi-${country_code}`} />
) : (
getUnicodeFlagIcon(country_code)
)}
</span>
)
);
}
+107 -57
View File
@@ -1,26 +1,26 @@
// 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 = {
online: number
offline: number
total: number
up: number
down: number
upSpeed: number
downSpeed: number
totalDomains: number // 新增:接收域名总数
onViewChange: (view: 'servers' | 'domains') => void // 新增:点击事件回调
activeView: 'servers' | 'domains' // 新增:当前激活的视图
}
online: number;
offline: number;
total: number;
up: number;
down: number;
upSpeed: number;
downSpeed: number;
totalDomains: number; // 新增:接收域名总数
onViewChange: (view: 'servers' | 'domains') => void; // 新增:点击事件回调
activeView: 'servers' | 'domains'; // 新增:当前激活的视图
};
export default function ServerOverview({
online,
@@ -34,33 +34,41 @@ export default function ServerOverview({
onViewChange,
activeView,
}: ServerOverviewProps) {
const { t } = useTranslation()
const { status, setStatus } = useStatus()
const { t } = useTranslation();
const { status, setStatus } = useStatus();
// --- 所有原始变量和逻辑保持不变 ---
const disableAnimatedMan = (window as any).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 DisableAnimatedMan is a global variable
const disableAnimatedMan = window.DisableAnimatedMan as boolean;
// @ts-expect-error CustomIllustration is a global variable
const customIllustration = window.CustomIllustration || "/animated-man.webp";
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
// 新增:一个组合了两个动作的点击处理函数
const handleServerCardClick = (serverStatus: 'all' | 'online' | 'offline') => {
onViewChange('servers'); // 动作1: 确保视图切换回服务器
setStatus(serverStatus); // 动作2: 执行原有的状态筛选
}
};
return (
<>
<section className="grid grid-cols-2 gap-4 lg:grid-cols-5 server-overview">
<Card
onClick={() => handleServerCardClick("all")}
className={cn(
"hover:border-blue-500 cursor-pointer transition-all",
{ "bg-card/70": customBackgroundImage },
)}
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>
<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>
@@ -71,37 +79,54 @@ export default function ServerOverview({
</CardContent>
</Card>
<Card
onClick={() => handleServerCardClick("online")}
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" }
{
"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>
<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")}
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" }
{
"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>
<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>
@@ -112,13 +137,16 @@ export default function ServerOverview({
</section>
</CardContent>
</Card>
<Card
onClick={() => onViewChange('domains')}
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' }
{
"bg-card/70": customBackgroundImage,
},
{
"ring-indigo-500 ring-2 border-transparent": activeView === "domains",
},
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
@@ -131,28 +159,50 @@ export default function ServerOverview({
</section>
</CardContent>
</Card>
<Card
className={cn("hover:ring-purple-500 ring-1 ring-transparent transition-all", { "bg-card/70": customBackgroundImage })}
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>
<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>
<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-[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>
<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-[-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" />
<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>
</>
)
);
}
+11 -5
View File
@@ -1,8 +1,8 @@
import { Progress } from "@/components/ui/progress"
import { Progress } from "@/components/ui/progress";
type ServerUsageBarProps = {
value: number
}
value: number;
};
export default function ServerUsageBar({ value }: ServerUsageBarProps) {
return (
@@ -10,8 +10,14 @@ export default function ServerUsageBar({ value }: ServerUsageBarProps) {
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"}
indicatorClassName={
value > 90
? "bg-red-500"
: value > 70
? "bg-orange-400"
: "bg-green-500"
}
className={"h-[3px] rounded-sm"}
/>
)
);
}
+50 -29
View File
@@ -1,43 +1,49 @@
import { fetchService } from "@/lib/nezha-api"
import { NezhaServer, ServiceData } from "@/types/nezha-api"
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid"
import { useQuery } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { fetchService } from "@/lib/nezha-api";
import type { NezhaServer, ServiceData } from "@/types/nezha-api";
import { CycleTransferStatsCard } from "./CycleTransferStats"
import ServiceTrackerClient from "./ServiceTrackerClient"
import { Loader } from "./loading/Loader"
import { CycleTransferStatsCard } from "./CycleTransferStats";
import { Loader } from "./loading/Loader";
import ServiceTrackerClient from "./ServiceTrackerClient";
export function ServiceTracker({ serverList }: { serverList: NezhaServer[] }) {
const { t } = useTranslation()
const { t } = useTranslation();
const { data: serviceData, isLoading } = useQuery({
queryKey: ["service"],
queryFn: () => fetchService(),
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchInterval: 10000,
})
});
const processServiceData = (serviceData: ServiceData) => {
const days = serviceData.up.map((up, index) => {
const totalChecks = up + serviceData.down[index]
const dailyUptime = totalChecks > 0 ? (up / totalChecks) * 100 : 0
const totalChecks = up + serviceData.down[index];
const dailyUptime = totalChecks > 0 ? (up / totalChecks) * 100 : 0;
return {
completed: up > serviceData.down[index],
date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000),
uptime: dailyUptime,
delay: serviceData.delay[index] || 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 uptime = (totalUp / totalChecks) * 100
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 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) {
return (
@@ -45,35 +51,50 @@ export function ServiceTracker({ serverList }: { serverList: NezhaServer[] }) {
<Loader visible={true} />
{t("serviceTracker.loading")}
</div>
)
);
}
if (!serviceData?.data?.services && !serviceData?.data?.cycle_transfer_stats) {
if (
!serviceData?.data?.services &&
!serviceData?.data?.cycle_transfer_stats
) {
return (
<div className="mt-4 text-sm font-medium flex items-center gap-1">
<ExclamationTriangleIcon className="w-4 h-4" />
{t("serviceTracker.noService")}
</div>
)
);
}
return (
<div className="mt-4 w-full mx-auto ">
{serviceData.data.cycle_transfer_stats && (
<div>
<CycleTransferStatsCard serverList={serverList} cycleStats={serviceData.data.cycle_transfer_stats} />
<CycleTransferStatsCard
serverList={serverList}
cycleStats={serviceData.data.cycle_transfer_stats}
/>
</div>
)}
{serviceData.data.services && Object.keys(serviceData.data.services).length > 0 && (
{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">
{Object.entries(serviceData.data.services).map(([name, data]) => {
const { days, uptime, avgDelay } = processServiceData(data)
return <ServiceTrackerClient key={name} days={days} title={data.service_name} uptime={uptime} avgDelay={avgDelay} />
const { days, uptime, avgDelay } = processServiceData(data);
return (
<ServiceTrackerClient
key={name}
days={days}
title={data.service_name}
uptime={uptime}
avgDelay={avgDelay}
/>
);
})}
</section>
)}
</div>
)
);
}
export default ServiceTracker
export default ServiceTracker;
+93 -45
View File
@@ -1,44 +1,58 @@
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import React from "react"
import { useTranslation } from "react-i18next"
import type React from "react";
import { useTranslation } from "react-i18next";
import {
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 {
days: Array<{
completed: boolean
date?: Date
uptime: number
delay: number
}>
className?: string
title?: string
uptime?: number
avgDelay?: number
completed: boolean;
date?: Date;
uptime: number;
delay: number;
}>;
className?: string;
title?: string;
uptime?: number;
avgDelay?: number;
}
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, className, title, uptime = 100, avgDelay = 0 }) => {
const { t } = useTranslation()
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
days,
className,
title,
uptime = 100,
avgDelay = 0,
}) => {
const { t } = useTranslation();
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const getUptimeColor = (uptime: number) => {
if (uptime >= 99) return "text-emerald-500"
if (uptime >= 95) return "text-amber-500"
return "text-rose-500"
}
if (uptime >= 99) return "text-emerald-500";
if (uptime >= 95) return "text-amber-500";
return "text-rose-500";
};
const getDelayColor = (delay: number) => {
if (delay < 100) return "text-emerald-500"
if (delay < 300) return "text-amber-500"
return "text-rose-500"
}
if (delay < 100) return "text-emerald-500";
if (delay < 300) return "text-amber-500";
return "text-rose-500";
};
const getStatusColor = (uptime: number) => {
if (uptime >= 99) return "bg-emerald-500"
if (uptime >= 95) return "bg-amber-500"
return "bg-rose-500"
}
if (uptime >= 99) return "bg-emerald-500";
if (uptime >= 95) return "bg-amber-500";
return "bg-rose-500";
};
return (
<div
@@ -52,13 +66,30 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, clas
>
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<div className={cn("w-2.5 h-2.5 rounded-full transition-colors", getStatusColor(uptime))} />
<div
className={cn(
"w-2.5 h-2.5 rounded-full transition-colors",
getStatusColor(uptime),
)}
/>
<span className="font-medium text-sm">{title}</span>
</div>
<div className="flex items-center gap-3">
<span className={cn("font-medium text-sm transition-colors", getDelayColor(avgDelay))}>{avgDelay.toFixed(0)}ms</span>
<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={cn(
"font-medium text-sm transition-colors",
getUptimeColor(uptime),
)}
>
{uptime.toFixed(1)}% {t("serviceTracker.uptime")}
</span>
</div>
@@ -73,27 +104,44 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, clas
className={cn(
"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",
"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
? "bg-gradient-to-b from-green-500/90 to-green-600 shadow-[0_1px_2px_theme(colors.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-green-500/90 to-green-600 shadow-[0_1px_2px_--theme(--color-green-600/30%)]"
: "bg-linear-to-b from-red-500/80 to-red-600/90 shadow-[0_1px_2px_--theme(--color-red-600/30%)]",
)}
/>
</TooltipTrigger>
<TooltipContent className="p-0 overflow-hidden">
<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">
{day.date?.toLocaleDateString()}
</p>
<div className="space-y-1.5">
<div className="flex items-center justify-between gap-3">
<span className="text-xs text-muted-foreground">{t("serviceTracker.uptime")}:</span>
<span className={cn("text-xs font-medium", day.uptime > 95 ? "text-green-500" : "text-red-500")}>{day.uptime.toFixed(1)}%</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-xs text-muted-foreground">{t("serviceTracker.delay")}:</span>
<span className="text-xs text-muted-foreground">
{t("serviceTracker.uptime")}:
</span>
<span
className={cn(
"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.uptime.toFixed(1)}%
</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-xs text-muted-foreground">
{t("serviceTracker.delay")}:
</span>
<span
className={cn(
"text-xs font-medium",
day.delay < 100
? "text-green-500"
: day.delay < 300
? "text-yellow-500"
: "text-red-500",
)}
>
{day.delay.toFixed(0)}ms
@@ -112,7 +160,7 @@ export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, clas
<span>{t("serviceTracker.today")}</span>
</div>
</div>
)
}
);
};
export default ServiceTrackerClient
export default ServiceTrackerClient;
+28 -12
View File
@@ -1,24 +1,40 @@
import { cn } from "@/lib/utils"
import { m } from "framer-motion"
import { useTranslation } from "react-i18next"
import { m } from "framer-motion";
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 }) {
const { t } = useTranslation()
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
export default function TabSwitch({
tabs,
currentTab,
setCurrentTab,
}: {
tabs: string[];
currentTab: string;
setCurrentTab: (tab: string) => void;
}) {
const { t } = useTranslation();
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
return (
<div className="z-50 flex flex-col items-start rounded-[50px] server-info-tab">
<div
className={cn("flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800", {
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,
})}
},
)}
>
{tabs.map((tab: string) => (
<div
key={tab}
onClick={() => setCurrentTab(tab)}
className={cn(
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500",
currentTab === tab ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500",
"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",
)}
>
{currentTab === tab && (
@@ -32,11 +48,11 @@ export default function TabSwitch({ tabs, currentTab, setCurrentTab }: { tabs: s
/>
)}
<div className="relative z-20 flex items-center gap-1">
<p className="whitespace-nowrap">{t("tabSwitch." + tab)}</p>
<p className="whitespace-nowrap">{t(`tabSwitch.${tab}`)}</p>
</div>
</div>
))}
</div>
</div>
)
);
}
+20 -18
View File
@@ -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() {
const { theme } = useTheme()
const { theme } = useTheme();
useEffect(() => {
const updateThemeColor = () => {
const currentTheme = theme
const meta = document.querySelector('meta[name="theme-color"]')
const currentTheme = theme;
const meta = document.querySelector('meta[name="theme-color"]');
if (!meta) {
const newMeta = document.createElement("meta")
newMeta.name = "theme-color"
document.head.appendChild(newMeta)
const newMeta = document.createElement("meta");
newMeta.name = "theme-color";
document.head.appendChild(newMeta);
}
const themeColor =
currentTheme === "dark"
? "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
updateThemeColor()
updateThemeColor();
// Listen for system theme changes
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
mediaQuery.addEventListener("change", updateThemeColor)
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener("change", updateThemeColor);
return () => mediaQuery.removeEventListener("change", updateThemeColor)
}, [theme])
return () => mediaQuery.removeEventListener("change", updateThemeColor);
}, [theme]);
return null
return null;
}
+55 -30
View File
@@ -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 = {
children: ReactNode
defaultTheme?: Theme
storageKey?: string
}
children: ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
}
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
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")
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
const root = window.document.documentElement;
root.classList.remove("light", "dark")
root.classList.add("disable-transitions");
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
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(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)
}, [theme])
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)
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
return <ThemeProviderContext.Provider value={value}>{children}</ThemeProviderContext.Provider>
}
export { ThemeProviderContext }
export { ThemeProviderContext };
+41 -18
View File
@@ -1,23 +1,31 @@
import { Theme } from "@/components/ThemeProvider"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils"
import { CheckCircleIcon } from "@heroicons/react/20/solid"
import { Moon, Sun } from "lucide-react"
import { useTranslation } from "react-i18next"
import { CheckCircleIcon } from "@heroicons/react/20/solid";
import { Moon, Sun } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { Theme } from "@/components/ThemeProvider";
import { Button } from "@/components/ui/button";
import {
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() {
const { t } = useTranslation()
const { setTheme, theme } = useTheme()
const { t } = useTranslation();
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) => {
e.preventDefault()
setTheme(newTheme)
}
e.preventDefault();
setTheme(newTheme);
};
return (
<DropdownMenu>
@@ -35,19 +43,34 @@ export function ModeToggle() {
</Button>
</DropdownMenuTrigger>
<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
className={cn("rounded-b-[5px] text-xs", {
"gap-3 bg-muted font-semibold": theme === "light",
})}
onSelect={(e) => handleSelect(e, "light")}
>
{t("theme.light")}
{theme === "light" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem>
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "dark" })} onSelect={(e) => handleSelect(e, "dark")}>
<DropdownMenuItem
className={cn("rounded-[5px] text-xs", {
"gap-3 bg-muted font-semibold": theme === "dark",
})}
onSelect={(e) => handleSelect(e, "dark")}
>
{t("theme.dark")}
{theme === "dark" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem>
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "system" })} onSelect={(e) => handleSelect(e, "system")}>
<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>
)
);
}
+62 -24
View File
@@ -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 }) {
const { t } = useTranslation()
export default function BillingInfo({
parsedData,
}: {
parsedData: PublicNoteData;
}) {
const { t } = useTranslation();
if (!parsedData || !parsedData.billingDataMod) {
return null
return null;
}
let isNeverExpire = false
let isNeverExpire = false;
let daysLeftObject = {
days: 0,
cycleLabel: "",
remainingPercentage: 0,
}
};
const hasBillingDates =
Boolean(parsedData.billingDataMod.startDate) ||
Boolean(parsedData.billingDataMod.endDate);
if (parsedData?.billingDataMod?.endDate) {
if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) {
isNeverExpire = true
isNeverExpire = true;
} else {
try {
daysLeftObject = getDaysBetweenDatesWithAutoRenewal(parsedData.billingDataMod)
daysLeftObject = getDaysBetweenDatesWithAutoRenewal(
parsedData.billingDataMod,
);
} catch (error) {
console.error(error)
console.error(error);
return (
<div className={cn("text-[10px] text-muted-foreground text-red-600")}>
{t("billingInfo.remaining")}: {t("billingInfo.error")}
</div>
)
);
}
}
}
return daysLeftObject.days >= 0 ? (
<>
{parsedData.billingDataMod.amount && parsedData.billingDataMod.amount !== "0" && parsedData.billingDataMod.amount !== "-1" ? (
{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}
{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>
<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>
<p className={cn("text-[10px] text-pink-600 ")}>
{t("billingInfo.usage-baseed")}
</p>
) : null}
{hasBillingDates && (
<div className={cn("text-[10px] text-muted-foreground")}>
{t("billingInfo.remaining")}: {isNeverExpire ? t("billingInfo.indefinite") : daysLeftObject.days + " " + t("billingInfo.days")}
{t("billingInfo.remaining")}:{" "}
{isNeverExpire
? t("billingInfo.indefinite")
: `${daysLeftObject.days} ${t("billingInfo.days")}`}
</div>
{!isNeverExpire && <RemainPercentBar className="mt-0.5" value={daysLeftObject.remainingPercentage * 100} />}
)}
{hasBillingDates && !isNeverExpire && (
<RemainPercentBar
className="mt-0.5"
value={daysLeftObject.remainingPercentage * 100}
/>
)}
</>
) : (
<>
{parsedData.billingDataMod.amount && parsedData.billingDataMod.amount !== "0" && parsedData.billingDataMod.amount !== "-1" ? (
{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}
{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>
<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>
<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")}
{t("billingInfo.expired")}: {daysLeftObject.days * -1}{" "}
{t("billingInfo.days")}
</p>
</>
)
);
}
+24
View File
@@ -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>
);
}
+5 -5
View File
@@ -1,4 +1,4 @@
const bars = Array(8).fill(0)
const bars = Array(8).fill(0);
export const Loader = ({ visible }: { visible: boolean }) => {
return (
@@ -9,8 +9,8 @@ export const Loader = ({ visible }: { visible: boolean }) => {
))}
</div>
</div>
)
}
);
};
export const LoadingSpinner = () => {
return (
@@ -28,5 +28,5 @@ export const LoadingSpinner = () => {
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
)
}
);
};
@@ -1,7 +1,7 @@
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() {
return (
@@ -15,17 +15,17 @@ export function ServerDetailChartLoading() {
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</section>
</div>
)
);
}
export function ServerDetailLoading() {
const navigate = useNavigate()
const navigate = useNavigate();
return (
<div className="mx-auto w-full max-w-5xl px-0">
<div
onClick={() => {
navigate("/")
navigate("/");
}}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
>
@@ -34,5 +34,5 @@ export function ServerDetailLoading() {
</div>
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</div>
)
);
}
+1 -1
View File
@@ -1 +1 @@
export { domMax as default } from "framer-motion"
export { domMax as default } from "framer-motion";
+5 -4
View File
@@ -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 }) => {
return (
<LazyMotion features={loadFeatures} strict key="framer">
{children}
</LazyMotion>
)
}
);
};
+18 -12
View File
@@ -1,15 +1,21 @@
import { cn } from "@/lib/utils"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />)
AccordionItem.displayName = "AccordionItem"
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
@@ -28,8 +34,8 @@ const AccordionTrigger = React.forwardRef<
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
@@ -42,8 +48,8 @@ const AccordionContent = React.forwardRef<
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -1,17 +1,23 @@
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
interface Props {
max: number
value: number
min: number
className?: string
primaryColor?: string
max: number;
value: number;
min: number;
className?: string;
primaryColor?: string;
}
export default function AnimatedCircularProgressBar({ max = 100, min = 0, value = 0, primaryColor, className }: Props) {
const circumference = 2 * Math.PI * 45
const percentPx = circumference / 100
const currentPercent = ((value - min) / (max - min)) * 100
export default function AnimatedCircularProgressBar({
max = 100,
min = 0,
value = 0,
primaryColor,
className,
}: Props) {
const circumference = 2 * Math.PI * 45;
const percentPx = circumference / 100;
const currentPercent = ((value - min) / (max - min)) * 100;
return (
<div
@@ -31,7 +37,12 @@ export default function AnimatedCircularProgressBar({ max = 100, min = 0, value
} as React.CSSProperties
}
>
<svg fill="none" className="size-full" strokeWidth="2" viewBox="0 0 100 100">
<svg
fill="none"
className="size-full"
strokeWidth="2"
viewBox="0 0 100 100"
>
{currentPercent <= 90 && currentPercent >= 0 && (
<circle
cx="50"
@@ -46,10 +57,13 @@ export default function AnimatedCircularProgressBar({ max = 100, min = 0, value
{
"--stroke-percent": 90 - currentPercent,
"--offset-factor-secondary": "calc(1 - var(--offset-factor))",
strokeDasharray: "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
transform: "rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
strokeDasharray:
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
transform:
"rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
transition: "all var(--transition-length) ease var(--delay)",
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
transformOrigin:
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
} as React.CSSProperties
}
/>
@@ -63,17 +77,21 @@ export default function AnimatedCircularProgressBar({ max = 100, min = 0, value
strokeLinecap="round"
strokeLinejoin="round"
className={cn("opacity-100 stroke-current", {
"stroke-[var(--stroke-primary-color)]": primaryColor,
"stroke-(--stroke-primary-color)": primaryColor,
})}
style={
{
"--stroke-primary-color": primaryColor,
"--stroke-percent": currentPercent,
strokeDasharray: "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
transition: "var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)",
strokeDasharray:
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
transition:
"var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)",
transitionProperty: "stroke-dasharray,transform",
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)",
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)",
} as React.CSSProperties
}
/>
@@ -85,5 +103,5 @@ export default function AnimatedCircularProgressBar({ max = 100, min = 0, value
{currentPercent}
</span>
</div>
)
);
}
+18 -11
View File
@@ -1,15 +1,18 @@
import { cn } from "@/lib/utils"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@/lib/utils";
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: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
@@ -17,12 +20,16 @@ const badgeVariants = cva(
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) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants }
export { Badge, badgeVariants };
+30 -17
View File
@@ -1,17 +1,20 @@
import { cn } from "@/lib/utils"
import { Slot } from "@radix-ui/react-slot"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
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: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
@@ -27,16 +30,26 @@ const buttonVariants = cva(
size: "default",
},
},
)
);
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
})
Button.displayName = "Button"
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants }
export { Button, buttonVariants };
+73 -26
View File
@@ -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<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none", className)}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
className,
)}
{...props}
/>
))
Card.displayName = "Card"
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
))
CardHeader.displayName = "CardHeader"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ 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) => (
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
))
CardTitle.displayName = "CardTitle"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ 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) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
))
CardDescription.displayName = "CardDescription"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ 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<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
))
CardFooter.displayName = "CardFooter"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ 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,
};
+211 -91
View File
@@ -1,42 +1,49 @@
import { cn } from "@/lib/utils"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
"use client";
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> })
}
[k: string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
type ChartContextProps = {
config: ChartConfig
}
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null)
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext)
const context = React.useContext(ChartContext);
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<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"]
config: ChartConfig;
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"];
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
@@ -44,24 +51,28 @@ const ChartContainer = React.forwardRef<
data-chart={chartId}
ref={ref}
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,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
);
});
ChartContainer.displayName = "Chart";
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) {
return null
return null;
}
return (
@@ -73,8 +84,10 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
return color ? ` --color-${key}: ${color};` : null
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
@@ -83,20 +96,32 @@ ${colorConfig
.join("\n"),
}}
/>
)
}
);
};
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
active?: boolean;
payload?: any[];
label?: any;
hideLabel?: boolean;
hideIndicator?: boolean;
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;
}
>(
(
@@ -117,49 +142,77 @@ const ChartTooltipContent = React.forwardRef<
},
ref,
) => {
const { config } = useChart()
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
return null;
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value = !labelKey && typeof label === "string" ? config[label as keyof typeof config]?.label || label : itemConfig?.label
const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
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) {
return null
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey])
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
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);
});
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<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",
"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",
className,
)}
>
{!nestLabel && (
<div className="mx-auto -mb-1 px-2.5 pt-1">
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
</div>
)}
<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
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
@@ -178,12 +231,16 @@ const ChartTooltipContent = React.forwardRef<
) : (
!hideIndicator && (
<div
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
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",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
})}
},
)}
style={
{
"--color-bg": indicatorColor,
@@ -193,49 +250,84 @@ const ChartTooltipContent = React.forwardRef<
/>
)
)}
<div className={cn("flex flex-1 justify-between leading-none", nestLabel ? "items-end" : "items-center")}>
<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>
<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>}
{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"
);
ChartTooltipContent.displayName = "ChartTooltip";
const ChartLegend = RechartsPrimitive.Legend
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
React.ComponentProps<"div"> & {
payload?: any[];
verticalAlign?: "top" | "bottom" | "middle";
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) {
return null
return null;
}
return (
<div ref={ref} className={cn("flex flex-wrap items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}>
<div
ref={ref}
className={cn(
"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)
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div key={item.value} className={cn("flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground")}>
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
@@ -246,32 +338,60 @@ const ChartLegendContent = React.forwardRef<
}}
/>
)}
{itemConfig?.label}
{key}
</div>
)
);
})}
</div>
)
})
ChartLegendContent.displayName = "ChartLegend"
);
},
);
ChartLegendContent.displayName = "ChartLegend";
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string,
) {
if (typeof payload !== "object" || payload === null) {
return undefined
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") {
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
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "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,
};
+15 -12
View File
@@ -1,24 +1,27 @@
import { cn } from "@/lib/utils"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-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>>(
({ className, ...props }, ref) => (
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"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",
"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",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
),
)
Checkbox.displayName = CheckboxPrimitive.Root.displayName
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox }
export { Checkbox };
+109 -57
View File
@@ -1,107 +1,159 @@
"use client"
"use client";
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { cn } from "@/lib/utils"
import { type DialogProps, DialogTitle } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import * as React from "react"
import { type DialogProps, DialogTitle } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import * as React from "react";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
const Command = React.forwardRef<React.ElementRef<typeof CommandPrimitive>, React.ComponentPropsWithoutRef<typeof CommandPrimitive>>(
({ className, ...props }, ref) => (
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn("flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", className)}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props}
/>
),
)
Command.displayName = CommandPrimitive.displayName
));
Command.displayName = CommandPrimitive.displayName;
const CommandDialog = ({ children, ...props }: DialogProps) => {
return (
<Dialog {...props}>
<DialogTitle />
<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}
</Command>
</DialogContent>
</Dialog>
)
}
);
};
const CommandInput = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Input>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>>(
({ className, ...props }, ref) => (
<div className="flex items-center bg-stone-100 dark:bg-stone-900 px-3" cmdk-input-wrapper="">
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div
className="flex items-center bg-stone-100 dark:bg-stone-900 px-3"
cmdk-input-wrapper=""
>
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"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",
"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>>(
({ 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
const CommandEmpty = React.forwardRef<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
const CommandGroup = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Group>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>>(
({ className, ...props }, ref) => (
<CommandPrimitive.Group
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
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",
"max-h-[300px] mb-1 overflow-y-auto overflow-x-hidden",
className,
)}
{...props}
/>
),
)
));
CommandGroup.displayName = CommandPrimitive.Group.displayName
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
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;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
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",
className,
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<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} />)
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
>(({ className, ...props }, ref) => (
<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>>(
({ className, ...props }, ref) => (
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"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",
"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",
className,
)}
{...props}
/>
),
)
));
CommandItem.displayName = CommandPrimitive.Item.displayName
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
}
CommandShortcut.displayName = "CommandShortcut"
const CommandShortcut = ({
className,
...props
}: 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,
};
+74 -30
View File
@@ -1,15 +1,16 @@
import { cn } from "@/lib/utils"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-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<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -23,8 +24,8 @@ const DialogOverlay = React.forwardRef<
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@@ -35,7 +36,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
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,
)}
{...props}
@@ -47,30 +48,73 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
)
DialogHeader.displayName = "DialogHeader"
const DialogHeader = ({
className,
...props
}: 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>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
)
DialogFooter.displayName = "DialogFooter"
const DialogFooter = ({
className,
...props
}: 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>>(
({ className, ...props }, ref) => (
<DialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
),
)
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => <DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />)
DialogDescription.displayName = DialogPrimitive.Description.displayName
>(({ className, ...props }, ref) => (
<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,
};
+66 -41
View File
@@ -1,30 +1,30 @@
import { cn } from "@/lib/utils"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-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<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
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",
className,
)}
@@ -33,8 +33,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -43,13 +44,14 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
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,
)}
{...props}
/>
))
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -60,32 +62,32 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
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,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
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",
className,
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@@ -94,7 +96,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
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,
)}
checked={checked}
@@ -107,8 +109,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -117,7 +120,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
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,
)}
{...props}
@@ -129,29 +132,51 @@ const DropdownMenuRadioItem = React.forwardRef<
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)} {...props} />
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
<DropdownMenuPrimitive.Label
ref={ref}
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<
React.ElementRef<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} />)
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
>(({ className, ...props }, ref) => (
<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>) => {
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
@@ -169,4 +194,4 @@ export {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
};
+12 -9
View File
@@ -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>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"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",
"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}
/>
)
})
Input.displayName = "Input"
);
},
);
Input.displayName = "Input";
export { Input }
export { Input };
+18 -9
View File
@@ -1,14 +1,23 @@
import { cn } from "@/lib/utils"
import * as LabelPrimitive from "@radix-ui/react-label"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
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<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />)
Label.displayName = LabelPrimitive.Root.displayName
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label }
export { Label };
+9 -9
View File
@@ -1,10 +1,10 @@
import { cn } from "@/lib/utils"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover";
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<
React.ElementRef<typeof PopoverPrimitive.Content>,
@@ -16,13 +16,13 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
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,
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent }
export { Popover, PopoverTrigger, PopoverContent };
+19 -9
View File
@@ -1,20 +1,30 @@
import { cn } from "@/lib/utils"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress";
import * as React from "react";
import { cn } from "@/lib/utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
indicatorClassName?: string
indicatorClassName?: string;
}
>(({ 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
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
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
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress }
export { Progress };
+66 -35
View File
@@ -1,13 +1,13 @@
import { cn } from "@/lib/utils"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-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<
React.ElementRef<typeof SelectPrimitive.Trigger>,
@@ -16,7 +16,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
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,
)}
{...props}
@@ -26,28 +26,43 @@ const SelectTrigger = React.forwardRef<
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}>
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}>
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
@@ -57,7 +72,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
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" &&
"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,
@@ -67,29 +82,40 @@ const SelectContent = React.forwardRef<
>
<SelectScrollUpButton />
<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",
position === "popper" &&
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Label>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>>(
({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
),
)
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
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>>(
({ className, children, ...props }, ref) => (
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"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",
"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",
className,
)}
{...props}
@@ -102,15 +128,20 @@ const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
),
)
SelectItem.displayName = SelectPrimitive.Item.displayName
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<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} />)
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
@@ -123,4 +154,4 @@ export {
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
};
+19 -9
View File
@@ -1,18 +1,28 @@
import { cn } from "@/lib/utils"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import * as React from "react";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<React.ElementRef<typeof SeparatorPrimitive.Root>, React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>>(
({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
className,
)}
{...props}
/>
),
)
Separator.displayName = SeparatorPrimitive.Root.displayName
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator }
export { Separator };
+12 -4
View File
@@ -1,7 +1,15 @@
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
}
export { Skeleton }
export { Skeleton };
+11 -10
View File
@@ -1,12 +1,14 @@
import { cn } from "@/lib/utils"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch";
import * as React from "react";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>, React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>>(
({ className, ...props }, ref) => (
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
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-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",
"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",
className,
)}
{...props}
@@ -18,8 +20,7 @@ const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>,
)}
/>
</SwitchPrimitives.Root>
),
)
Switch.displayName = SwitchPrimitives.Root.displayName
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch }
export { Switch };
+108 -42
View File
@@ -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<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<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) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ 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) => (
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} />
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(({ 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) => (
<th
<table
ref={ref}
className={cn("h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", className)}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
))
TableHead.displayName = "TableHead"
</div>
));
Table.displayName = "Table";
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => (
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
))
TableCell.displayName = "TableCell"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(({ className, ...props }, ref) => (
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
))
TableCaption.displayName = "TableCaption"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ 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) => (
<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) => (
<th
ref={ref}
className={cn(
"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) => (
<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) => (
<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,
};
+9 -9
View File
@@ -1,12 +1,12 @@
import { cn } from "@/lib/utils"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
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<
React.ElementRef<typeof TooltipPrimitive.Content>,
@@ -21,7 +21,7 @@ const TooltipContent = React.forwardRef<
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+12
View File
@@ -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,
);
+24
View File
@@ -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
View File
@@ -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 {
sortType: SortType
sortOrder: SortOrder
setSortType: (sortType: SortType) => void
setSortOrder: (sortOrder: SortOrder) => void
sortType: SortType;
sortOrder: SortOrder;
setSortType: (sortType: SortType) => void;
setSortOrder: (sortOrder: SortOrder) => void;
}
export const SortContext = createContext<SortContextType | undefined>(undefined)
export const SortContext = createContext<SortContextType | undefined>(
undefined,
);
+11 -5
View File
@@ -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 }) {
const [sortType, setSortType] = useState<SortType>("default")
const [sortOrder, setSortOrder] = useState<SortOrder>("desc")
const [sortType, setSortType] = useState<SortType>("default");
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>
);
}
+7 -5
View File
@@ -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 {
status: Status
setStatus: (status: Status) => void
status: Status;
setStatus: (status: Status) => void;
}
export const StatusContext = createContext<StatusContextType | undefined>(undefined)
export const StatusContext = createContext<StatusContextType | undefined>(
undefined,
);
+8 -4
View File
@@ -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 }) {
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>
);
}
+13 -10
View File
@@ -1,18 +1,21 @@
import { createContext } from "react"
import { createContext } from "react";
export interface TooltipData {
centroid: [number, number]
country: string
count: number
centroid: [number, number];
country: string;
count: number;
servers: Array<{
name: string
status: boolean
}>
id: number;
name: string;
status: boolean;
}>;
}
interface TooltipContextType {
tooltipData: TooltipData | null
setTooltipData: (data: TooltipData | null) => void
tooltipData: TooltipData | null;
setTooltipData: (data: TooltipData | null) => void;
}
export const TooltipContext = createContext<TooltipContextType | undefined>(undefined)
export const TooltipContext = createContext<TooltipContextType | undefined>(
undefined,
);
+8 -4
View File
@@ -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 }) {
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>
);
}
+8 -8
View File
@@ -1,12 +1,12 @@
import { createContext } from "react"
import { createContext } from "react";
export interface WebSocketContextType {
lastMessage: { data: string } | null
connected: boolean
messageHistory: { data: string }[]
reconnect: () => void
needReconnect: boolean
setNeedReconnect: (needReconnect: boolean) => void
lastMessage: { data: string } | null;
connected: boolean;
messageHistory: { data: string }[];
reconnect: () => void;
needReconnect: boolean;
setNeedReconnect: (needReconnect: boolean) => void;
}
export const WebSocketContext = createContext<WebSocketContextType>({
@@ -16,4 +16,4 @@ export const WebSocketContext = createContext<WebSocketContextType>({
reconnect: () => {},
needReconnect: false,
setNeedReconnect: () => {},
})
});
+89 -75
View File
@@ -1,123 +1,133 @@
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 {
url: string
children: React.ReactNode
url: string;
children: React.ReactNode;
}
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, children }) => {
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null)
const [messageHistory, setMessageHistory] = useState<{ data: string }[]>([]) // 新增历史消息状态
const [connected, setConnected] = useState(false)
const [needReconnect, setNeedReconnect] = useState(false)
const ws = useRef<WebSocket | null>(null)
const reconnectTimeout = useRef<NodeJS.Timeout>(null)
const maxReconnectAttempts = 30
const reconnectAttempts = useRef(0)
const isConnecting = useRef(false)
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
url,
children,
}) => {
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null);
const [messageHistory, setMessageHistory] = useState<{ data: string }[]>([]); // 新增历史消息状态
const [connected, setConnected] = useState(false);
const [needReconnect, setNeedReconnect] = useState(false);
const ws = useRef<WebSocket | null>(null);
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) {
// 移除所有事件监听器
ws.current.onopen = null
ws.current.onclose = null
ws.current.onmessage = null
ws.current.onerror = null
ws.current.onopen = null;
ws.current.onclose = null;
ws.current.onmessage = null;
ws.current.onerror = null;
if (ws.current.readyState === WebSocket.OPEN || ws.current.readyState === WebSocket.CONNECTING) {
ws.current.close()
if (
ws.current.readyState === WebSocket.OPEN ||
ws.current.readyState === WebSocket.CONNECTING
) {
ws.current.close();
}
ws.current = null
ws.current = null;
}
if (reconnectTimeout.current) {
clearTimeout(reconnectTimeout.current)
reconnectTimeout.current = null
}
setConnected(false)
clearTimeout(reconnectTimeout.current);
reconnectTimeout.current = null;
}
setConnected(false);
}, []);
const connect = () => {
const connect = useCallback(() => {
if (isConnecting.current) {
console.log("Connection already in progress")
return
console.log("Connection already in progress");
return;
}
cleanup()
isConnecting.current = true
cleanup();
isConnecting.current = true;
try {
const wsUrl = new URL(url, window.location.origin)
wsUrl.protocol = wsUrl.protocol.replace("http", "ws")
const wsUrl = new URL(url, window.location.origin);
wsUrl.protocol = wsUrl.protocol.replace("http", "ws");
ws.current = new WebSocket(wsUrl.toString())
ws.current = new WebSocket(wsUrl.toString());
ws.current.onopen = () => {
console.log("WebSocket connected")
setConnected(true)
reconnectAttempts.current = 0
isConnecting.current = false
}
console.log("WebSocket connected");
setConnected(true);
reconnectAttempts.current = 0;
isConnecting.current = false;
};
ws.current.onclose = () => {
console.log("WebSocket disconnected")
setConnected(false)
ws.current = null
isConnecting.current = false
console.log("WebSocket disconnected");
setConnected(false);
ws.current = null;
isConnecting.current = false;
if (reconnectAttempts.current < maxReconnectAttempts) {
reconnectTimeout.current = setTimeout(() => {
reconnectAttempts.current++
connect()
}, 3000)
}
reconnectAttempts.current++;
connect();
}, 3000);
}
};
ws.current.onmessage = (event) => {
const newMessage = { data: event.data }
setLastMessage(newMessage)
const newMessage = { data: event.data };
setLastMessage(newMessage);
// 更新历史消息,保持最新的30条记录
setMessageHistory((prev) => {
const updated = [newMessage, ...prev]
return updated.slice(0, 30)
})
}
const updated = [newMessage, ...prev];
return updated.slice(0, 30);
});
};
ws.current.onerror = (error) => {
console.error("WebSocket error:", error)
isConnecting.current = false
}
console.error("WebSocket error:", error);
isConnecting.current = false;
};
} catch (error) {
console.error("WebSocket connection error:", error)
isConnecting.current = false
}
console.error("WebSocket connection error:", error);
isConnecting.current = false;
}
}, [cleanup, url]);
const reconnect = () => {
reconnectAttempts.current = 0
reconnectAttempts.current = 0;
// 等待一个小延时确保清理完成
cleanup()
cleanup();
setTimeout(() => {
connect()
}, 1000)
}
connect();
}, 1000);
};
useEffect(() => {
connect()
connect();
// 添加页面卸载事件监听
const handleBeforeUnload = () => {
cleanup()
}
cleanup();
};
window.addEventListener("beforeunload", handleBeforeUnload)
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
cleanup()
window.removeEventListener("beforeunload", handleBeforeUnload)
}
}, [url])
cleanup();
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, [cleanup, connect]);
const contextValue: WebSocketContextType = {
lastMessage,
@@ -126,7 +136,11 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, child
reconnect,
needReconnect,
setNeedReconnect,
}
};
return <WebSocketContext.Provider value={contextValue}>{children}</WebSocketContext.Provider>
}
return (
<WebSocketContext.Provider value={contextValue}>
{children}
</WebSocketContext.Provider>
);
};
+37 -29
View File
@@ -1,60 +1,68 @@
import { useEffect, useState } from "react"
import { useEffect, useState } from "react";
declare global {
interface Window {
CustomBackgroundImage: string
CustomMobileBackgroundImage: string
ForceShowServices: boolean
ForceCardInline: boolean
ForceShowMap: boolean
ForcePeakCutEnabled: boolean
CustomBackgroundImage: string;
CustomMobileBackgroundImage: string;
ForceShowServices: boolean;
ForceCardInline: boolean;
ForceShowMap: boolean;
ForcePeakCutEnabled: boolean;
}
}
const BACKGROUND_CHANGE_EVENT = "backgroundChange"
const BACKGROUND_CHANGE_EVENT = "backgroundChange";
export function useBackground() {
const [backgroundImage, setBackgroundImage] = useState<string | undefined>(undefined)
const [backgroundImage, setBackgroundImage] = useState<string | undefined>(
undefined,
);
useEffect(() => {
// 监听背景变化
const handleBackgroundChange = () => {
setBackgroundImage(window.CustomBackgroundImage || undefined)
}
setBackgroundImage(window.CustomBackgroundImage || undefined);
};
// 初始化检查
const checkInitialBackground = () => {
if (window.CustomBackgroundImage) {
setBackgroundImage(window.CustomBackgroundImage)
setBackgroundImage(window.CustomBackgroundImage);
} else {
const savedImage = sessionStorage.getItem("savedBackgroundImage")
const savedImage = sessionStorage.getItem("savedBackgroundImage");
if (savedImage) {
window.CustomBackgroundImage = savedImage
setBackgroundImage(savedImage)
}
window.CustomBackgroundImage = savedImage;
setBackgroundImage(savedImage);
}
}
};
// 设置一个轮询来检查初始背景
const intervalId = setInterval(() => {
if (window.CustomBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) {
checkInitialBackground()
clearInterval(intervalId)
if (
window.CustomBackgroundImage ||
sessionStorage.getItem("savedBackgroundImage")
) {
checkInitialBackground();
clearInterval(intervalId);
}
}, 100)
}, 100);
window.addEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange)
window.addEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange);
return () => {
window.removeEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange)
clearInterval(intervalId)
}
}, [])
window.removeEventListener(
BACKGROUND_CHANGE_EVENT,
handleBackgroundChange,
);
clearInterval(intervalId);
};
}, []);
const updateBackground = (newBackground: string | undefined) => {
window.CustomBackgroundImage = newBackground || ""
window.dispatchEvent(new Event(BACKGROUND_CHANGE_EVENT))
}
window.CustomBackgroundImage = newBackground || "";
window.dispatchEvent(new Event(BACKGROUND_CHANGE_EVENT));
};
return { backgroundImage, updateBackground }
return { backgroundImage, updateBackground };
}
+9 -9
View File
@@ -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>(
messageHistory: { data: string }[],
serverId: number,
formatFn: (wsData: NezhaWebsocketResponse, serverId: number) => T | null,
) {
const [data, setData] = useState<T[]>([])
const [data, setData] = useState<T[]>([]);
useEffect(() => {
if (messageHistory.length > 0 && data.length === 0) {
const historyData = messageHistory
.map((msg) => {
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse
return formatFn(wsData, serverId)
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse;
return formatFn(wsData, serverId);
})
.filter((item): item is T => item !== null)
.reverse()
.reverse();
setData(historyData)
setData(historyData);
}
}, [messageHistory])
}, [messageHistory, data.length, formatFn, serverId]);
return data
return data;
}
+10
View File
@@ -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;
}
+5 -5
View File
@@ -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() {
const context = useContext(SortContext)
const context = useContext(SortContext);
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;
}
+5 -5
View File
@@ -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() {
const context = useContext(StatusContext)
const context = useContext(StatusContext);
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;
}
+6 -6
View File
@@ -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 = () => {
const context = useContext(ThemeProviderContext)
const context = useContext(ThemeProviderContext);
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;
};
+7 -7
View File
@@ -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 = () => {
const context = useContext(TooltipContext)
const context = useContext(TooltipContext);
if (context === undefined) {
throw new Error("useTooltip must be used within a TooltipProvider")
}
return context
throw new Error("useTooltip must be used within a TooltipProvider");
}
return context;
};
export default useTooltip
export default useTooltip;
+8 -6
View File
@@ -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 = () => {
const context = useContext(WebSocketContext)
const context = useContext(WebSocketContext);
if (context === undefined) {
throw new Error("useWebSocketContext must be used within a WebSocketProvider")
}
return context
throw new Error(
"useWebSocketContext must be used within a WebSocketProvider",
);
}
return context;
};
+16 -16
View File
@@ -1,13 +1,13 @@
import i18n from "i18next"
import { initReactI18next } from "react-i18next"
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import deTranslation from "./locales/de/translation.json"
import enTranslation from "./locales/en/translation.json"
import esTranslation from "./locales/es/translation.json"
import ruTranslation from "./locales/ru/translation.json"
import taTranslation from "./locales/ta/translation.json"
import zhCNTranslation from "./locales/zh-CN/translation.json"
import zhTWTranslation from "./locales/zh-TW/translation.json"
import deTranslation from "./locales/de/translation.json";
import enTranslation from "./locales/en/translation.json";
import esTranslation from "./locales/es/translation.json";
import ruTranslation from "./locales/ru/translation.json";
import taTranslation from "./locales/ta/translation.json";
import zhCNTranslation from "./locales/zh-CN/translation.json";
import zhTWTranslation from "./locales/zh-TW/translation.json";
const resources = {
"en-US": {
@@ -31,11 +31,11 @@ const resources = {
"ta-IN": {
translation: taTranslation,
},
}
};
const getStoredLanguage = () => {
return localStorage.getItem("language") || "en-US"
}
return localStorage.getItem("language") || "en-US";
};
i18n.use(initReactI18next).init({
resources,
@@ -44,11 +44,11 @@ i18n.use(initReactI18next).init({
interpolation: {
escapeValue: false, // react已经安全地转义
},
})
});
// 添加语言改变时的处理函数
i18n.on("languageChanged", (lng) => {
localStorage.setItem("language", lng)
})
localStorage.setItem("language", lng);
});
export default i18n
export default i18n;
+185 -12
View File
@@ -1,7 +1,101 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@import url("https://cdn.jsdelivr.net/npm/lxgw-wenkai-screen-webfont@1.7.0/style.css");
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@theme {
--font-sans: var(--font-sans);
--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;
}
}
}
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
@utility step {
counter-increment: step;
&:before {
@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 mt-[-4px] ml-[-50px];
content: counter(step);
}
}
@layer utilities {
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
@@ -16,6 +110,14 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer base {
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}
@layer base {
:root {
@@ -49,6 +151,7 @@
--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 {
@@ -96,18 +199,19 @@
/* 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 utilities {
.step {
counter-increment: step;
}
.step:before {
@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;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
@layer base {
/* Avoid color fade when toggling themes. */
html.disable-transitions *,
html.disable-transitions *::before,
html.disable-transitions *::after {
transition: none;
}
}
@@ -232,3 +336,72 @@
.scrollbar-hidden::-webkit-scrollbar {
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);
}
+38
View File
@@ -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
View File
@@ -1,11 +1,21 @@
export function formatBytes(bytes: number, decimals: number = 2) {
if (!+bytes) return "0 Bytes"
if (!+bytes) return "0 Bytes";
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
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
+6 -2
View File
@@ -1,4 +1,7 @@
export const countryCoordinates: Record<string, { lat: number; lng: number; name: string }> = {
export const countryCoordinates: Record<
string,
{ lat: number; lng: number; name: string }
> = {
// 亚洲
AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗
AM: { lat: 40.0, lng: 45.0, name: "Armenia" }, // 亚美尼亚
@@ -10,6 +13,7 @@ export const countryCoordinates: Record<string, { lat: number; lng: number; name
KH: { lat: 13.0, lng: 105.0, name: "Cambodia" }, // 柬埔寨
CN: { lat: 35.0, lng: 105.0, name: "China" }, // 中国
HK: { lat: 22.0, lng: 114.0, name: "Hong Kong" }, // 香港
MO: { lat: 22.1667, lng: 113.55, name: "Macau" }, // 澳门
CY: { lat: 35.0, lng: 33.0, name: "Cyprus" }, // 塞浦路斯
GE: { lat: 42.0, lng: 43.5, name: "Georgia" }, // 格鲁吉亚
IN: { lat: 20.0, lng: 77.0, name: "India" }, // 印度
@@ -205,4 +209,4 @@ export const countryCoordinates: Record<string, { lat: number; lng: number; name
EH: { lat: 24.5, lng: -13.0, name: "Western Sahara" }, // 西撒哈拉
ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚
ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦
}
};
+65 -57
View File
@@ -1,99 +1,107 @@
export const InjectContext = (content: string) => {
const tempDiv = document.createElement("div")
tempDiv.innerHTML = content
const tempDiv = document.createElement("div");
tempDiv.innerHTML = content;
const INJECTION_MARK = "data-injected" // 自定义属性标识
const INJECTION_MARK = "data-injected"; // 自定义属性标识
// 清理已有的注入资源
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 = (
scriptElement: HTMLScriptElement,
): Promise<void> => {
return new Promise((resolve, reject) => {
const script = document.createElement("script")
script.src = scriptElement.src
script.async = false // 保持顺序执行
script.setAttribute(INJECTION_MARK, "true") // 添加标识
script.onload = () => resolve()
script.onerror = () => reject(new Error(`Failed to load script: ${scriptElement.src}`))
document.head.appendChild(script)
})
}
const script = document.createElement("script");
script.src = scriptElement.src;
script.async = false; // 保持顺序执行
script.setAttribute(INJECTION_MARK, "true"); // 添加标识
script.onload = () => resolve();
script.onerror = () =>
reject(new Error(`Failed to load script: ${scriptElement.src}`));
document.head.appendChild(script);
});
};
const executeInlineScript = (scriptContent: string): Promise<void> => {
return new Promise((resolve) => {
const script = document.createElement("script")
script.textContent = scriptContent
script.setAttribute(INJECTION_MARK, "true") // 添加标识
document.body.appendChild(script)
resolve()
})
}
const script = document.createElement("script");
script.textContent = scriptContent;
script.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.body.appendChild(script);
resolve();
});
};
const loadStyle = (styleElement: HTMLStyleElement): Promise<void> => {
return new Promise((resolve, reject) => {
if ((styleElement as any).href) {
// 处理 <link>
const link = document.createElement("link")
link.rel = "stylesheet"
link.href = (styleElement as any).href
link.setAttribute(INJECTION_MARK, "true") // 添加标识
link.onload = () => resolve()
link.onerror = () => reject(new Error(`Failed to load stylesheet: ${link.href}`))
document.head.appendChild(link)
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = (styleElement as any).href;
link.setAttribute(INJECTION_MARK, "true"); // 添加标识
link.onload = () => resolve();
link.onerror = () =>
reject(new Error(`Failed to load stylesheet: ${link.href}`));
document.head.appendChild(link);
} else {
const style = document.createElement("style")
style.textContent = styleElement.textContent
style.setAttribute(INJECTION_MARK, "true") // 添加标识
document.head.appendChild(style)
resolve()
}
})
const style = document.createElement("style");
style.textContent = styleElement.textContent;
style.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.head.appendChild(style);
resolve();
}
});
};
const handlers: { [key: string]: (element: HTMLElement) => Promise<void> } = {
SCRIPT: (element) => {
const scriptElement = element as HTMLScriptElement
const scriptElement = element as HTMLScriptElement;
if (scriptElement.src) {
// 加载外部脚本
return loadExternalScript(scriptElement)
return loadExternalScript(scriptElement);
} else {
// 执行内联脚本
return executeInlineScript(scriptElement.textContent || "")
return executeInlineScript(scriptElement.textContent || "");
}
},
STYLE: (element) => loadStyle(element as HTMLStyleElement),
META: (element) => {
const meta = element.cloneNode(true) as HTMLElement
meta.setAttribute(INJECTION_MARK, "true") // 添加标识
document.head.appendChild(meta) // 将 meta 标签插入到 <head>
return Promise.resolve()
const meta = element.cloneNode(true) as HTMLElement;
meta.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.head.appendChild(meta); // 将 meta 标签插入到 <head>
return Promise.resolve();
},
DEFAULT: (element) => {
element.setAttribute(INJECTION_MARK, "true") // 添加标识
document.body.appendChild(element)
return Promise.resolve()
element.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.body.appendChild(element);
return Promise.resolve();
},
}
};
// 开始注入前清理已有资源
cleanInjectedResources()
cleanInjectedResources();
const executeSequentially = async () => {
for (const node of Array.from(tempDiv.childNodes)) {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as HTMLElement
const handler = handlers[element.tagName] || handlers.DEFAULT
await handler(element) // 按顺序等待当前脚本或资源完成处理
const element = node as HTMLElement;
const handler = handlers[element.tagName] || handlers.DEFAULT;
await handler(element); // 按顺序等待当前脚本或资源完成处理
} 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) => {
console.error("Error during resource injection:", error)
})
}
console.error("Error during resource injection:", error);
});
};
+29 -23
View File
@@ -1,4 +1,4 @@
import type { SVGProps } from "react"
import type { SVGProps } from "react";
export function GetFontLogoClass(platform: string): string {
if (
@@ -47,24 +47,24 @@ export function GetFontLogoClass(platform: string): string {
"zorin",
].indexOf(platform) > -1
) {
return platform
return platform;
}
if (platform == "darwin") {
return "apple"
if (platform === "darwin") {
return "apple";
}
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
return "tux"
return "tux";
}
if (platform == "amazon") {
return "redhat"
if (platform === "amazon") {
return "redhat";
}
if (platform == "arch") {
return "archlinux"
if (platform === "arch") {
return "archlinux";
}
if (platform.toLowerCase().includes("opensuse")) {
return "opensuse"
return "opensuse";
}
return "tux"
return "tux";
}
export function GetOsName(platform: string): string {
@@ -110,33 +110,39 @@ export function GetOsName(platform: string): string {
"zorin",
].indexOf(platform) > -1
) {
return platform.charAt(0).toUpperCase() + platform.slice(1)
return platform.charAt(0).toUpperCase() + platform.slice(1);
}
if (platform == "darwin") {
return "macOS"
if (platform === "darwin") {
return "macOS";
}
if (["openwrt", "linux", "immortalwrt"].indexOf(platform) > -1) {
return "Linux"
return "Linux";
}
if (platform == "amazon") {
return "Redhat"
if (platform === "amazon") {
return "Redhat";
}
if (platform == "arch") {
return "Archlinux"
if (platform === "arch") {
return "Archlinux";
}
if (platform.toLowerCase().includes("opensuse")) {
return "Opensuse"
return "Opensuse";
}
return "Linux"
return "Linux";
}
export function MageMicrosoftWindows(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M2.75 7.189V2.865c0-.102 0-.115.115-.115h8.622c.128 0 .14 0 .14.128V11.5c0 .128 0 .128-.14.128H2.865c-.102 0-.115 0-.115-.116zM7.189 21.25H2.865c-.102 0-.115 0-.115-.116V12.59c0-.128 0-.128.128-.128h8.635c.102 0 .115 0 .115.115v8.57c0 .09 0 .103-.116.103zM21.25 7.189v4.31c0 .116 0 .116-.116.116h-8.557c-.102 0-.128 0-.128-.115V2.865c0-.09 0-.102.115-.102h8.48c.206 0 .206 0 .206.205zm-8.763 9.661v-4.273c0-.09 0-.115.103-.09h8.621c.026 0 0 .09 0 .142v8.518a.06.06 0 0 1-.017.06a.06.06 0 0 1-.06.017H12.54s-.09 0-.077-.09V16.85z"
></path>
</svg>
)
);
}
+65 -30
View File
@@ -1,55 +1,90 @@
import { LoginUserResponse, MonitorResponse, ServerGroupResponse, ServiceResponse, SettingResponse } from "@/types/nezha-api"
import type {
LoginUserResponse,
MetricPeriod,
MetricType,
MonitorResponse,
ServerGroupResponse,
ServerMetricsResponse,
ServiceResponse,
SettingResponse,
} from "@/types/nezha-api";
let lastestRefreshTokenAt = 0
let lastestRefreshTokenAt = 0;
export const fetchServerGroup = async (): Promise<ServerGroupResponse> => {
const response = await fetch("/api/v1/server-group")
const data = await response.json()
const response = await fetch("/api/v1/server-group");
const data = await response.json();
if (data.error) {
throw new Error(data.error)
}
return data
throw new Error(data.error);
}
return data;
};
export const fetchLoginUser = async (): Promise<LoginUserResponse> => {
const response = await fetch("/api/v1/profile")
const data = await response.json()
const response = await fetch("/api/v1/profile");
const data = await response.json();
if (data.error) {
throw new Error(data.error)
throw new Error(data.error);
}
// auto refresh token
if (document.cookie && (!lastestRefreshTokenAt || Date.now() - lastestRefreshTokenAt > 1000 * 60 * 60)) {
lastestRefreshTokenAt = Date.now()
fetch("/api/v1/refresh-token")
if (
document.cookie &&
(!lastestRefreshTokenAt ||
Date.now() - lastestRefreshTokenAt > 1000 * 60 * 60)
) {
lastestRefreshTokenAt = Date.now();
fetch("/api/v1/refresh-token");
}
return data
}
return data;
};
export const fetchMonitor = async (server_id: number): Promise<MonitorResponse> => {
const response = await fetch(`/api/v1/service/${server_id}`)
const data = await response.json()
export type MonitorPeriod = "1d" | "7d" | "30d";
export const fetchMonitor = async (
server_id: number,
period?: MonitorPeriod,
): Promise<MonitorResponse> => {
const query = period ? `?period=${period}` : "";
const response = await fetch(`/api/v1/server/${server_id}/service${query}`);
const data = await response.json();
if (data.error) {
throw new Error(data.error)
}
return data
throw new Error(data.error);
}
return data;
};
export const fetchService = async (): Promise<ServiceResponse> => {
const response = await fetch("/api/v1/service")
const data = await response.json()
const response = await fetch("/api/v1/service");
const data = await response.json();
if (data.error) {
throw new Error(data.error)
}
return data
throw new Error(data.error);
}
return data;
};
export const fetchSetting = async (): Promise<SettingResponse> => {
const response = await fetch("/api/v1/setting")
const data = await response.json()
const response = await fetch("/api/v1/setting");
const data = await response.json();
if (data.error) {
throw new Error(data.error)
throw new Error(data.error);
}
return data
return data;
};
export const fetchServerMetrics = async (
server_id: number,
metric: MetricType,
period?: MetricPeriod,
): Promise<ServerMetricsResponse> => {
const query = period
? `?metric=${metric}&period=${period}`
: `?metric=${metric}`;
const response = await fetch(`/api/v1/server/${server_id}/metrics${query}`);
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
return data;
};
+136 -114
View File
@@ -1,14 +1,16 @@
import { NezhaServer } from "@/types/nezha-api"
import { type ClassValue, clsx } from "clsx"
import dayjs from "dayjs"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx";
import dayjs from "dayjs";
import { twMerge } from "tailwind-merge";
import type { NezhaServer } from "@/types/nezha-api";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}
export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
const lastActiveTime = serverInfo.last_active.startsWith("000") ? 0 : parseISOTimestamp(serverInfo.last_active)
const lastActiveTime = serverInfo.last_active.startsWith("000")
? 0
: parseISOTimestamp(serverInfo.last_active);
return {
...serverInfo,
cpu: serverInfo.state.cpu || 0,
@@ -16,7 +18,9 @@ export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
process: serverInfo.state.process_count || 0,
up: serverInfo.state.net_out_speed / 1024 / 1024 || 0,
down: serverInfo.state.net_in_speed / 1024 / 1024 || 0,
last_active_time_string: lastActiveTime ? dayjs(lastActiveTime).format("YYYY-MM-DD HH:mm:ss") : "",
last_active_time_string: lastActiveTime
? dayjs(lastActiveTime).format("YYYY-MM-DD HH:mm:ss")
: "",
online: now - lastActiveTime <= 30000,
uptime: serverInfo.state.uptime || 0,
version: serverInfo.host.version || null,
@@ -35,7 +39,9 @@ export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
swap_total: serverInfo.host.swap_total || 0,
disk_total: serverInfo.host.disk_total || 0,
boot_time: serverInfo.host.boot_time || 0,
boot_time_string: serverInfo.host.boot_time ? dayjs(serverInfo.host.boot_time * 1000).format("YYYY-MM-DD HH:mm:ss") : "",
boot_time_string: serverInfo.host.boot_time
? dayjs(serverInfo.host.boot_time * 1000).format("YYYY-MM-DD HH:mm:ss")
: "",
platform_version: serverInfo.host.platform_version || "",
cpu_info: serverInfo.host.cpu || [],
gpu_info: serverInfo.host.gpu || [],
@@ -43,17 +49,22 @@ export function formatNezhaInfo(now: number, serverInfo: NezhaServer) {
load_5: serverInfo.state.load_5?.toFixed(2) || 0.0,
load_15: serverInfo.state.load_15?.toFixed(2) || 0.0,
public_note: handlePublicNote(serverInfo.id, serverInfo.public_note || ""),
}
};
}
export function getDaysBetweenDatesWithAutoRenewal({ autoRenewal, cycle, startDate, endDate }: BillingData): {
days: number
cycleLabel: string
remainingPercentage: number
export function getDaysBetweenDatesWithAutoRenewal({
autoRenewal,
cycle,
startDate,
endDate,
}: BillingData): {
days: number;
cycleLabel: string;
remainingPercentage: number;
} {
let months = 1
let months = 1;
// 套餐资费
let cycleLabel = cycle
let cycleLabel = cycle;
switch (cycle.toLowerCase()) {
case "月":
@@ -61,49 +72,52 @@ export function getDaysBetweenDatesWithAutoRenewal({ autoRenewal, cycle, startDa
case "mo":
case "month":
case "monthly":
cycleLabel = "月"
months = 1
break
cycleLabel = "月";
months = 1;
break;
case "年":
case "y":
case "yr":
case "year":
case "annual":
cycleLabel = "年"
months = 12
break
cycleLabel = "年";
months = 12;
break;
case "季":
case "q":
case "qr":
case "quarterly":
cycleLabel = "季"
months = 3
break
cycleLabel = "季";
months = 3;
break;
case "半":
case "半年":
case "h":
case "half":
case "semi-annually":
cycleLabel = "半年"
months = 6
break
cycleLabel = "半年";
months = 6;
break;
default:
cycleLabel = cycle
break
cycleLabel = cycle;
break;
}
const nowTime = new Date().getTime()
const endTime = dayjs(endDate).valueOf()
const nowTime = Date.now();
const endTime = dayjs(endDate).valueOf();
if (autoRenewal !== "1") {
return {
days: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()),
cycleLabel: cycleLabel,
remainingPercentage:
getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / dayjs(endDate).diff(startDate, "day") > 1
getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
dayjs(endDate).diff(startDate, "day") >
1
? 1
: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / dayjs(endDate).diff(startDate, "day"),
}
: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
dayjs(endDate).diff(startDate, "day"),
};
}
if (nowTime < endTime) {
@@ -111,149 +125,157 @@ export function getDaysBetweenDatesWithAutoRenewal({ autoRenewal, cycle, startDa
days: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()),
cycleLabel: cycleLabel,
remainingPercentage:
getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / (30 * months) > 1
getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
(30 * months) >
1
? 1
: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) / (30 * months),
}
: getDaysBetweenDates(endDate, new Date(nowTime).toISOString()) /
(30 * months),
};
}
const nextTime = getNextCycleTime(endTime, months, nowTime)
const diff = dayjs(nextTime).diff(dayjs(), "day") + 1
const remainingPercentage = diff / (30 * months) > 1 ? 1 : diff / (30 * months)
const nextTime = getNextCycleTime(endTime, months, nowTime);
const diff = dayjs(nextTime).diff(dayjs(), "day") + 1;
const remainingPercentage =
diff / (30 * months) > 1 ? 1 : diff / (30 * months);
return {
days: diff,
cycleLabel: cycleLabel,
remainingPercentage: remainingPercentage,
}
};
}
// Thanks to hi2shark for the code
// https://github.com/hi2shark/nazhua/blob/main/src/utils/date.js#L86
export function getNextCycleTime(startDate: number, months: number, specifiedDate: number): number {
const start = dayjs(startDate)
const checkDate = dayjs(specifiedDate)
export function getNextCycleTime(
startDate: number,
months: number,
specifiedDate: number,
): number {
const start = dayjs(startDate);
const checkDate = dayjs(specifiedDate);
if (!start.isValid() || months <= 0) {
throw new Error("参数无效:请检查起始日期、周期月份数和指定日期。")
throw new Error("参数无效:请检查起始日期、周期月份数和指定日期。");
}
let nextDate = start
let nextDate = start;
// 循环增加周期直到大于当前日期
let whileStatus = true
let whileStatus = true;
while (whileStatus) {
nextDate = nextDate.add(months, "month")
whileStatus = nextDate.valueOf() <= checkDate.valueOf()
nextDate = nextDate.add(months, "month");
whileStatus = nextDate.valueOf() <= checkDate.valueOf();
}
return nextDate.valueOf() // 返回时间毫秒数
return nextDate.valueOf(); // 返回时间毫秒数
}
export function getDaysBetweenDates(date1: string, date2: string): number {
const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
const firstDate = new Date(date1)
const secondDate = new Date(date2)
const oneDay = 24 * 60 * 60 * 1000; // 一天的毫秒数
const firstDate = new Date(date1);
const secondDate = new Date(date2);
// 计算两个日期之间的天数差异
return Math.round((firstDate.getTime() - secondDate.getTime()) / oneDay)
return Math.round((firstDate.getTime() - secondDate.getTime()) / oneDay);
}
export const fetcher = (url: string) =>
fetch(url)
.then((res) => {
if (!res.ok) {
throw new Error(res.statusText)
throw new Error(res.statusText);
}
return res.json()
return res.json();
})
.then((data) => data.data)
.catch((err) => {
console.error(err)
throw err
})
console.error(err);
throw err;
});
export const nezhaFetcher = async (url: string) => {
const res = await fetch(url)
const res = await fetch(url);
if (!res.ok) {
const error = new Error("An error occurred while fetching the data.")
const error = new Error("An error occurred while fetching the data.");
// @ts-expect-error - res.json() returns a Promise<any>
error.info = await res.json()
error.info = await res.json();
// @ts-expect-error - res.status is a number
error.status = res.status
throw error
error.status = res.status;
throw error;
}
return res.json()
}
return res.json();
};
export function parseISOTimestamp(isoString: string): number {
return new Date(isoString).getTime()
return new Date(isoString).getTime();
}
export function formatRelativeTime(timestamp: number): string {
const now = Date.now()
const diff = now - timestamp
const hours = Math.floor(diff / (1000 * 60 * 60))
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((diff % (1000 * 60)) / 1000)
const now = Date.now();
const diff = now - timestamp;
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
if (hours > 24) {
const days = Math.floor(hours / 24)
return `${days}d`
const days = Math.floor(hours / 24);
return `${days}d`;
} else if (hours > 0) {
return `${hours}h`
return `${hours}h`;
} else if (minutes > 0) {
return `${minutes}m`
return `${minutes}m`;
} else if (seconds >= 0) {
return `${seconds}s`
return `${seconds}s`;
}
return "0s"
return "0s";
}
export function formatTime(timestamp: number): string {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hours = date.getHours().toString().padStart(2, "0")
const minutes = date.getMinutes().toString().padStart(2, "0")
const seconds = date.getSeconds().toString().padStart(2, "0")
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
const date = new Date(timestamp);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const seconds = date.getSeconds().toString().padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
interface BillingData {
startDate: string
endDate: string
autoRenewal: string
cycle: string
amount: string
startDate: string;
endDate: string;
autoRenewal: string;
cycle: string;
amount: string;
}
interface PlanData {
bandwidth: string
trafficVol: string
trafficType: string
IPv4: string
IPv6: string
networkRoute: string
extra: string
bandwidth: string;
trafficVol: string;
trafficType: string;
IPv4: string;
IPv6: string;
networkRoute: string;
extra: string;
}
export interface PublicNoteData {
billingDataMod?: BillingData
planDataMod?: PlanData
billingDataMod?: BillingData;
planDataMod?: PlanData;
}
export function parsePublicNote(publicNote: string): PublicNoteData | null {
try {
if (!publicNote) {
return null
return null;
}
const data = JSON.parse(publicNote)
const data = JSON.parse(publicNote);
if (!data.billingDataMod && !data.planDataMod) {
return null
return null;
}
if (data.billingDataMod && !data.planDataMod) {
return {
@@ -264,7 +286,7 @@ export function parsePublicNote(publicNote: string): PublicNoteData | null {
cycle: data.billingDataMod.cycle || "",
amount: data.billingDataMod.amount || "",
},
}
};
}
if (!data.billingDataMod && data.planDataMod) {
return {
@@ -277,7 +299,7 @@ export function parsePublicNote(publicNote: string): PublicNoteData | null {
networkRoute: data.planDataMod.networkRoute || "",
extra: data.planDataMod.extra || "",
},
}
};
}
return {
@@ -297,26 +319,26 @@ export function parsePublicNote(publicNote: string): PublicNoteData | null {
networkRoute: data.planDataMod.networkRoute || "",
extra: data.planDataMod.extra || "",
},
}
};
} catch (error) {
console.error("Error parsing public note:", error)
return null
console.error("Error parsing public note:", error);
return null;
}
}
// Function to handle public_note with sessionStorage
export function handlePublicNote(serverId: number, publicNote: string): string {
const storageKey = `server_${serverId}_public_note`
const storedNote = sessionStorage.getItem(storageKey)
const storageKey = `server_${serverId}_public_note`;
const storedNote = sessionStorage.getItem(storageKey);
if (!publicNote && storedNote) {
return storedNote
return storedNote;
}
if (publicNote) {
sessionStorage.setItem(storageKey, publicNote)
return publicNote
sessionStorage.setItem(storageKey, publicNote);
return publicNote;
}
return ""
return "";
}

Some files were not shown because too many files have changed in this diff Show More