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 - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
draft: ${{ contains(github.ref_name, '-alpha') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-rc') }}
files: dist.zip files: dist.zip
- name: Changelog - name: Changelog
+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] > [!NOTE]
> 此项目为 nezha-dash 的官方实现,作为哪吒监控 V1 版本的默认前端,功能上可能与 nezha-dash 有所不同。 > 此项目为 nezha-dash 的官方实现,作为哪吒监控的默认前端,功能上可能与 nezha-dash 有所不同。
> https://github.com/hamster1963/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.
+13 -13
View File
@@ -1,15 +1,15 @@
{ {
"types": { "types": {
"feat": { "title": "🚀 Features" }, "feat": { "title": "🚀 Features" },
"fix": { "title": "🔧 Bug Fixes" }, "fix": { "title": "🔧 Bug Fixes" },
"docs": { "title": "📚 Documentation" }, "docs": { "title": "📚 Documentation" },
"style": { "title": "💄 Styles" }, "style": { "title": "💄 Styles" },
"refactor": { "title": "🔨 Refactor" }, "refactor": { "title": "🔨 Refactor" },
"perf": { "title": "🏎 Performance" }, "perf": { "title": "🏎 Performance" },
"test": { "title": "🚨 Tests" }, "test": { "title": "🚨 Tests" },
"build": { "title": "🛠 Build" }, "build": { "title": "🛠 Build" },
"ci": { "title": "👷 CI" }, "ci": { "title": "👷 CI" },
"chore": { "title": "🛗 Chore" }, "chore": { "title": "🛗 Chore" },
"revert": { "title": "⏪ Revert" } "revert": { "title": "⏪ Revert" }
} }
} }
+18 -18
View File
@@ -1,20 +1,20 @@
{ {
"$schema": "https://ui.shadcn.com/schema.json", "$schema": "https://ui.shadcn.com/schema.json",
"style": "default", "style": "default",
"rsc": false, "rsc": false,
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {
"config": "tailwind.config.js", "config": "tailwind.config.js",
"css": "src/index.css", "css": "src/index.css",
"baseColor": "stone", "baseColor": "stone",
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""
}, },
"aliases": { "aliases": {
"components": "@/components", "components": "@/components",
"utils": "@/lib/utils", "utils": "@/lib/utils",
"ui": "@/components/ui", "ui": "@/components/ui",
"lib": "@/lib", "lib": "@/lib",
"hooks": "@/hooks" "hooks": "@/hooks"
} }
} }
-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" theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
} }
document.documentElement.classList.add(theme) document.documentElement.classList.add(theme)
} catch (e) { } catch (_e) {
document.documentElement.classList.add("light") document.documentElement.classList.add("light")
} }
</script> </script>
@@ -36,15 +36,15 @@
} }
html { html {
background-color: var(--bg) !important; background-color: var(--bg);
} }
body { body {
background-color: var(--bg) !important; background-color: var(--bg);
} }
#root { #root {
background-color: var(--bg) !important; background-color: var(--bg);
visibility: hidden; visibility: hidden;
} }
@@ -63,7 +63,7 @@
} }
</style> </style>
<script> <script>
;(function () { ;(() => {
const storageKey = "vite-ui-theme" const storageKey = "vite-ui-theme"
const theme = localStorage.getItem(storageKey) || "system" const theme = localStorage.getItem(storageKey) || "system"
const root = document.documentElement const root = document.documentElement
+68 -72
View File
@@ -1,74 +1,70 @@
{ {
"name": "nazha-dash-v1", "name": "nazha-dash-v2",
"private": true, "private": true,
"version": "1.0.0", "version": "2.0.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "biome lint",
"lint:fix": "eslint --fix .", "lint:fix": "biome lint --fix",
"format": "prettier --write .", "format": "biome format --write .",
"preview": "vite preview" "check": "biome check",
}, "check:fix": "biome check --fix --unsafe",
"dependencies": { "preview": "vite preview"
"@fontsource/inter": "5.1.1", },
"@heroicons/react": "2.2.0", "dependencies": {
"@number-flow/react": "0.5.5", "@fontsource/inter": "5.1.1",
"@radix-ui/react-accordion": "1.2.3", "@heroicons/react": "2.2.0",
"@radix-ui/react-checkbox": "1.1.4", "@number-flow/react": "0.5.5",
"@radix-ui/react-dialog": "1.1.6", "@radix-ui/react-accordion": "1.2.3",
"@radix-ui/react-dropdown-menu": "2.1.6", "@radix-ui/react-checkbox": "1.1.4",
"@radix-ui/react-label": "2.1.2", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-popover": "1.1.6", "@radix-ui/react-dropdown-menu": "2.1.6",
"@radix-ui/react-progress": "1.1.2", "@radix-ui/react-label": "2.1.2",
"@radix-ui/react-select": "2.1.6", "@radix-ui/react-popover": "1.1.6",
"@radix-ui/react-separator": "1.1.2", "@radix-ui/react-progress": "1.1.2",
"@radix-ui/react-slot": "1.1.2", "@radix-ui/react-select": "2.1.6",
"@radix-ui/react-switch": "1.1.3", "@radix-ui/react-separator": "1.1.2",
"@radix-ui/react-tooltip": "1.1.8", "@radix-ui/react-slot": "1.1.2",
"@tanstack/react-query": "5.66.7", "@radix-ui/react-switch": "1.1.3",
"@tanstack/react-query-devtools": "5.66.7", "@radix-ui/react-tooltip": "1.1.8",
"@tanstack/react-table": "8.21.2", "@tanstack/react-query": "5.66.7",
"@trivago/prettier-plugin-sort-imports": "5.2.2", "@tanstack/react-query-devtools": "5.66.7",
"@types/d3-geo": "3.1.0", "@tanstack/react-table": "8.21.2",
"@types/luxon": "3.4.2", "@types/d3-geo": "3.1.0",
"class-variance-authority": "0.7.1", "@types/luxon": "3.4.2",
"clsx": "2.1.1", "class-variance-authority": "0.7.1",
"cmdk": "1.1.1", "clsx": "2.1.1",
"country-flag-icons": "1.5.18", "cmdk": "1.1.1",
"d3-geo": "3.1.1", "country-flag-icons": "1.5.18",
"dayjs": "1.11.13", "d3-geo": "3.1.1",
"framer-motion": "11.18.2", "dayjs": "1.11.13",
"i18n-iso-countries": "7.14.0", "framer-motion": "12.23.26",
"i18next": "24.2.2", "i18n-iso-countries": "7.14.0",
"lucide-react": "0.460.0", "i18next": "24.2.2",
"luxon": "3.5.0", "lucide-react": "0.460.0",
"prettier-plugin-tailwindcss": "0.6.11", "luxon": "3.5.0",
"react": "19.0.0", "react": "19.0.0",
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-i18next": "15.4.1", "react-i18next": "15.4.1",
"react-router-dom": "7.2.0", "react-router-dom": "^7.13.2",
"recharts": "2.15.1", "recharts": "2.15.1",
"sonner": "1.7.4", "sonner": "1.7.4",
"tailwind-merge": "2.6.0", "tailwind-merge": "2.6.0",
"tailwindcss-animate": "1.0.7" "tailwindcss-animate": "^1.0.7"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.20.0", "@biomejs/biome": "2.3.10",
"@types/node": "22.13.4", "@tailwindcss/postcss": "^4.2.2",
"@types/react": "19.0.10", "@types/node": "22.13.4",
"@types/react-dom": "19.0.4", "@types/react": "19.0.10",
"@vitejs/plugin-react-swc": "3.8.0", "@types/react-dom": "19.0.4",
"autoprefixer": "10.4.20", "@vitejs/plugin-react": "^6.0.1",
"eslint": "9.20.1", "globals": "15.15.0",
"eslint-plugin-react-hooks": "5.1.0", "postcss": "8.5.3",
"eslint-plugin-react-refresh": "0.4.19", "tailwindcss": "^4.2.2",
"globals": "15.15.0", "typescript": "~5.6.3",
"postcss": "8.5.3", "vite": "8.0.3"
"tailwindcss": "3.4.17", }
"typescript": "~5.6.3",
"typescript-eslint": "8.24.1",
"vite": "6.1.1"
}
} }
+911 -2355
View File
File diff suppressed because it is too large Load Diff
+4 -5
View File
@@ -1,6 +1,5 @@
export default { export default {
plugins: { plugins: {
tailwindcss: {}, "@tailwindcss/postcss": {},
autoprefixer: {}, },
}, };
}
+15 -15
View File
@@ -1,17 +1,17 @@
{ {
"name": "Nezha Monitoring", "name": "Nezha Monitoring",
"short_name": "Nezha Monitoring", "short_name": "Nezha Monitoring",
"icons": [ "icons": [
{ {
"src": "/android-chrome-192x192.png", "src": "/android-chrome-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png", "type": "image/png",
"purpose": "any maskable" "purpose": "any maskable"
} }
], ],
"theme_color": "hsl(0 0% 98%)", "theme_color": "hsl(0 0% 98%)",
"background_color": "hsl(0 0% 98%)", "background_color": "hsl(0 0% 98%)",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"orientation": "portrait" "orientation": "portrait"
} }
+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 // Get the short version of the git hash
const gitHash = execSync("git rev-parse --short HEAD").toString().trim() const gitHash = execSync("git rev-parse --short HEAD").toString().trim();
// Write it to stdout // Write it to stdout
console.log(gitHash) console.log(gitHash);
+153 -105
View File
@@ -1,109 +1,157 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query";
import React, { useEffect, useState } from "react" import { DateTime } from "luxon";
import { useTranslation } from "react-i18next" import type React from "react";
import { Route, BrowserRouter as Router, Routes } from "react-router-dom" import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
import { DashCommand } from "./components/DashCommand" import { DashCommand } from "./components/DashCommand";
import ErrorBoundary from "./components/ErrorBoundary" import ErrorBoundary from "./components/ErrorBoundary";
import Footer from "./components/Footer" import Footer from "./components/Footer";
import Header, { RefreshToast } from "./components/Header" import Header, { RefreshToast } from "./components/Header";
import { useBackground } from "./hooks/use-background" import { useBackground } from "./hooks/use-background";
import { useTheme } from "./hooks/use-theme" import { useTheme } from "./hooks/use-theme";
import { InjectContext } from "./lib/inject" import { InjectContext } from "./lib/inject";
import { fetchSetting } from "./lib/nezha-api" import { fetchSetting } from "./lib/nezha-api";
import { cn } from "./lib/utils" import { cn } from "./lib/utils";
import ErrorPage from "./pages/ErrorPage" import ErrorPage from "./pages/ErrorPage";
import NotFound from "./pages/NotFound" import NotFound from "./pages/NotFound";
import Server from "./pages/Server" import Server from "./pages/Server";
import ServerDetail from "./pages/ServerDetail" import ServerDetail from "./pages/ServerDetail";
// Route checker component
const RouteChecker: React.FC = () => {
return <MainApp />;
};
const MainApp: React.FC = () => {
const { data: settingData, error } = useQuery({
queryKey: ["setting"],
queryFn: () => fetchSetting(),
refetchOnMount: true,
refetchOnWindowFocus: true,
});
const { i18n } = useTranslation();
const { setTheme } = useTheme();
const [isCustomCodeInjected, setIsCustomCodeInjected] = useState(false);
const { backgroundImage: customBackgroundImage } = useBackground();
useEffect(() => {
if (settingData?.data?.config?.custom_code) {
InjectContext(settingData?.data?.config?.custom_code);
setIsCustomCodeInjected(true);
}
// 同步自定义配置到全局变量
const config = settingData?.data?.config;
if (config) {
if (config.custom_logo) window.CustomLogo = config.custom_logo;
if (config.custom_description)
window.CustomDesc = config.custom_description;
if (config.custom_links) window.CustomLinks = config.custom_links;
const hour = DateTime.now().hour;
const isNight = hour >= 18 || hour < 6;
if (isNight && config.background_image_night) {
window.CustomBackgroundImage = config.background_image_night;
} else if (!isNight && config.background_image_day) {
window.CustomBackgroundImage = config.background_image_day;
}
window.CustomMobileBackgroundImage = window.CustomBackgroundImage;
}
}, [settingData]);
// 检测是否强制指定了主题颜色
const forceTheme =
(window.ForceTheme as string) !== "" ? window.ForceTheme : undefined;
useEffect(() => {
const savedTheme = localStorage.getItem("vite-ui-theme");
// Only auto-apply ForceTheme if the user hasn't manually picked one (or picked 'system')
if (
(!savedTheme || savedTheme === "system") &&
(forceTheme === "dark" || forceTheme === "light")
) {
setTheme(forceTheme);
}
}, [forceTheme, setTheme]);
if (error) {
return <ErrorPage code={500} message={error.message} />;
}
if (!settingData) {
return null;
}
if (settingData?.data?.config?.custom_code && !isCustomCodeInjected) {
return null;
}
if (
settingData?.data?.config?.language &&
!localStorage.getItem("language")
) {
i18n.changeLanguage(settingData?.data?.config?.language);
}
const customMobileBackgroundImage =
window.CustomMobileBackgroundImage !== ""
? window.CustomMobileBackgroundImage
: undefined;
return (
<ErrorBoundary>
{/* 固定定位的背景层 */}
{customBackgroundImage && (
<div
className={cn(
"fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center dark:brightness-75",
{
"hidden sm:block": customMobileBackgroundImage,
},
)}
style={{ backgroundImage: `url(${customBackgroundImage})` }}
/>
)}
{customMobileBackgroundImage && (
<div
className={cn(
"fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center sm:hidden dark:brightness-75",
)}
style={{ backgroundImage: `url(${customMobileBackgroundImage})` }}
/>
)}
<div
className={cn("flex min-h-screen w-full flex-col", {
"bg-background": !customBackgroundImage,
})}
>
<main className="flex z-20 min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 p-4 md:p-10 md:pt-8">
<RefreshToast />
<Header />
<DashCommand />
<Routes>
<Route path="/" element={<Server />} />
<Route path="/server/:id" element={<ServerDetail />} />
<Route path="/error" element={<ErrorPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
<Footer />
</main>
</div>
</ErrorBoundary>
);
};
// Main App wrapper with router
const App: React.FC = () => { const App: React.FC = () => {
const { data: settingData, error } = useQuery({ return (
queryKey: ["setting"], <Router basename={import.meta.env.BASE_URL}>
queryFn: () => fetchSetting(), <RouteChecker />
refetchOnMount: true, </Router>
refetchOnWindowFocus: true, );
}) };
const { i18n } = useTranslation()
const { setTheme } = useTheme()
const [isCustomCodeInjected, setIsCustomCodeInjected] = useState(false)
const { backgroundImage: customBackgroundImage } = useBackground()
useEffect(() => { export default App;
if (settingData?.data?.config?.custom_code) {
InjectContext(settingData?.data?.config?.custom_code)
setIsCustomCodeInjected(true)
}
}, [settingData?.data?.config?.custom_code])
// 检测是否强制指定了主题颜色
const forceTheme =
// @ts-expect-error ForceTheme is a global variable
(window.ForceTheme as string) !== "" ? window.ForceTheme : undefined
useEffect(() => {
if (forceTheme === "dark" || forceTheme === "light") {
setTheme(forceTheme)
}
}, [forceTheme])
if (error) {
return <ErrorPage code={500} message={error.message} />
}
if (!settingData) {
return null
}
if (settingData?.data?.config?.custom_code && !isCustomCodeInjected) {
return null
}
if (settingData?.data?.config?.language && !localStorage.getItem("language")) {
i18n.changeLanguage(settingData?.data?.config?.language)
}
const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined
return (
<Router basename={import.meta.env.BASE_URL}>
<ErrorBoundary>
{/* 固定定位的背景层 */}
{customBackgroundImage && (
<div
className={cn("fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center dark:brightness-75", {
"hidden sm:block": customMobileBackgroundImage,
})}
style={{ backgroundImage: `url(${customBackgroundImage})` }}
/>
)}
{customMobileBackgroundImage && (
<div
className={cn("fixed inset-0 z-0 bg-cover min-h-lvh bg-no-repeat bg-center sm:hidden dark:brightness-75")}
style={{ backgroundImage: `url(${customMobileBackgroundImage})` }}
/>
)}
<div
className={cn("flex min-h-screen w-full flex-col", {
"bg-background": !customBackgroundImage,
})}
>
<main className="flex z-20 min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 p-4 md:p-10 md:pt-8">
<RefreshToast />
<Header />
<DashCommand />
<Routes>
<Route path="/" element={<Server />} />
<Route path="/server/:id" element={<ServerDetail />} />
<Route path="/error" element={<ErrorPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
<Footer />
</main>
</div>
</ErrorBoundary>
</Router>
)
}
export default App
+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>
);
}
+56 -50
View File
@@ -1,62 +1,68 @@
import { CycleTransferStats, NezhaServer } from "@/types/nezha-api" import type React from "react";
import React from "react" import type { CycleTransferStats, NezhaServer } from "@/types/nezha-api";
import { CycleTransferStatsClient } from "./CycleTransferStatsClient" import { CycleTransferStatsClient } from "./CycleTransferStatsClient";
interface CycleTransferStatsProps { interface CycleTransferStatsProps {
serverList: NezhaServer[] serverList: NezhaServer[];
cycleStats: CycleTransferStats cycleStats: CycleTransferStats;
className?: string className?: string;
} }
export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({ serverList, cycleStats, className }) => { export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({
if (serverList.length === 0) { serverList,
return null cycleStats,
} className,
}) => {
if (serverList.length === 0) {
return null;
}
const serverIdList = serverList.map((server) => server.id.toString()) const serverIdList = serverList.map((server) => server.id.toString());
return ( return (
<section className="grid grid-cols-1 md:grid-cols-3 gap-3"> <section className="grid grid-cols-1 md:grid-cols-3 gap-3">
{Object.entries(cycleStats).map(([cycleId, cycleData]) => { {Object.entries(cycleStats).map(([cycleId, cycleData]) => {
if (!cycleData.server_name) { if (!cycleData.server_name) {
return null return null;
} }
return Object.entries(cycleData.server_name).map(([serverId, serverName]) => { return Object.entries(cycleData.server_name).map(
const transfer = cycleData.transfer?.[serverId] || 0 ([serverId, serverName]) => {
const nextUpdate = cycleData.next_update?.[serverId] const transfer = cycleData.transfer?.[serverId] || 0;
const nextUpdate = cycleData.next_update?.[serverId];
if (!serverIdList.includes(serverId)) { if (!serverIdList.includes(serverId)) {
return null return null;
} }
if (!transfer && !nextUpdate) { if (!transfer && !nextUpdate) {
return null return null;
} }
return ( return (
<CycleTransferStatsClient <CycleTransferStatsClient
key={`${cycleId}-${serverId}`} key={`${cycleId}-${serverId}`}
name={cycleData.name} name={cycleData.name}
from={cycleData.from} from={cycleData.from}
to={cycleData.to} to={cycleData.to}
max={cycleData.max} max={cycleData.max}
serverStats={[ serverStats={[
{ {
serverId, serverId,
serverName, serverName,
transfer, transfer,
nextUpdate: nextUpdate || "", nextUpdate: nextUpdate || "",
}, },
]} ]}
className={className} className={className}
/> />
) );
}) },
})} );
</section> })}
) </section>
} );
};
export default CycleTransferStatsCard export default CycleTransferStatsCard;
+87 -70
View File
@@ -1,79 +1,96 @@
import { formatBytes } from "@/lib/format" import type React from "react";
import { cn } from "@/lib/utils" import { useTranslation } from "react-i18next";
import React from "react" import { formatBytes } from "@/lib/format";
import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils";
interface CycleTransferStatsClientProps { interface CycleTransferStatsClientProps {
name: string name: string;
from: string from: string;
to: string to: string;
max: number max: number;
serverStats: Array<{ serverStats: Array<{
serverId: string serverId: string;
serverName: string serverName: string;
transfer: number transfer: number;
nextUpdate: string nextUpdate: string;
}> }>;
className?: string className?: string;
} }
export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> = ({ name, from, to, max, serverStats, className }) => { export const CycleTransferStatsClient: React.FC<
const { t } = useTranslation() CycleTransferStatsClientProps
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined > = ({ name, from, to, max, serverStats, className }) => {
return ( const { t } = useTranslation();
<div const customBackgroundImage =
className={cn( (window.CustomBackgroundImage as string) !== ""
"w-full bg-white px-4 py-3.5 rounded-lg border bg-card text-card-foreground hover:shadow-sm transition-all duration-200 dark:shadow-none", ? window.CustomBackgroundImage
className, : undefined;
{ return (
"bg-card/70": customBackgroundImage, <div
}, className={cn(
)} "w-full bg-white px-4 py-3.5 rounded-lg border bg-card text-card-foreground hover:shadow-xs transition-all duration-200 dark:shadow-none",
> className,
{serverStats.map(({ serverId, serverName, transfer, nextUpdate }) => { {
const progress = (transfer / max) * 100 "bg-card/70": customBackgroundImage,
},
)}
>
{serverStats.map(({ serverId, serverName, transfer, nextUpdate }) => {
const progress = (transfer / max) * 100;
return ( return (
<div key={serverId} className="space-y-3"> <div key={serverId} className="space-y-3">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{serverName}</span> <span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
<div className="bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-2 py-0.5 rounded text-xs font-medium">{name}</div> {serverName}
</div> </span>
<div className="bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-2 py-0.5 rounded text-xs font-medium">
{name}
</div>
</div>
{/* Progress Section */} {/* Progress Section */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-baseline gap-1"> <div className="flex items-baseline gap-1">
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{formatBytes(transfer)}</span> <span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
<span className="text-xs text-neutral-500 dark:text-neutral-400">/ {formatBytes(max)}</span> {formatBytes(transfer)}
</div> </span>
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">{progress.toFixed(1)}%</span> <span className="text-xs text-neutral-500 dark:text-neutral-400">
</div> / {formatBytes(max)}
</span>
</div>
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">
{progress.toFixed(1)}%
</span>
</div>
<div className="relative h-1.5"> <div className="relative h-1.5">
<div className="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full" /> <div className="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full" />
<div <div
className="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300" className="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300"
style={{ width: `${Math.min(progress, 100)}%` }} style={{ width: `${Math.min(progress, 100)}%` }}
/> />
</div> </div>
</div> </div>
{/* Footer */} {/* Footer */}
<div className="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400"> <div className="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400">
<span> <span>
{new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()} {new Date(from).toLocaleDateString()} -{" "}
</span> {new Date(to).toLocaleDateString()}
<span> </span>
{t("cycleTransfer.nextUpdate")}: {new Date(nextUpdate).toLocaleString()} <span>
</span> {t("cycleTransfer.nextUpdate")}:{" "}
</div> {new Date(nextUpdate).toLocaleString()}
</div> </span>
) </div>
})} </div>
</div> );
) })}
} </div>
);
};
export default CycleTransferStatsClient export default CycleTransferStatsClient;
+117 -106
View File
@@ -1,118 +1,129 @@
"use client" "use client";
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command" import { Home, Moon, Sun, SunMoon } from "lucide-react";
import { useTheme } from "@/hooks/use-theme" import { useEffect, useState } from "react";
import { useWebSocketContext } from "@/hooks/use-websocket-context" import { useTranslation } from "react-i18next";
import { formatNezhaInfo } from "@/lib/utils" import { useNavigate } from "react-router-dom";
import { NezhaWebsocketResponse } from "@/types/nezha-api" import {
import { Home, Moon, Sun, SunMoon } from "lucide-react" CommandDialog,
import { useEffect, useState } from "react" CommandEmpty,
import { useTranslation } from "react-i18next" CommandGroup,
import { useNavigate } from "react-router-dom" CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
import { useCommand } from "@/hooks/use-command";
import { useTheme } from "@/hooks/use-theme";
import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { formatNezhaInfo } from "@/lib/utils";
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
export function DashCommand() { export function DashCommand() {
const [open, setOpen] = useState(false) const { isOpen, closeCommand, toggleCommand } = useCommand();
const [search, setSearch] = useState("") const [search, setSearch] = useState("");
const navigate = useNavigate() const navigate = useNavigate();
const { t } = useTranslation() const { t } = useTranslation();
const { setTheme } = useTheme() const { setTheme } = useTheme();
const { lastMessage, connected } = useWebSocketContext() const { lastMessage, connected } = useWebSocketContext();
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null const nezhaWsData = lastMessage
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
: null;
useEffect(() => { useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) { if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault() e.preventDefault();
setOpen((open) => !open) toggleCommand();
} }
} };
document.addEventListener("keydown", down) document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down) return () => document.removeEventListener("keydown", down);
}, []) }, [toggleCommand]);
if (!connected || !nezhaWsData) return null if (!connected || !nezhaWsData) return null;
const shortcuts = [ const shortcuts = [
{ {
keywords: ["home", "homepage"], keywords: ["home", "homepage"],
icon: <Home />, icon: <Home />,
label: t("Home"), label: t("Home"),
action: () => navigate("/"), action: () => navigate("/"),
}, },
{ {
keywords: ["light", "theme", "lightmode"], keywords: ["light", "theme", "lightmode"],
icon: <Sun />, icon: <Sun />,
label: t("ToggleLightMode"), label: t("ToggleLightMode"),
action: () => setTheme("light"), action: () => setTheme("light"),
}, },
{ {
keywords: ["dark", "theme", "darkmode"], keywords: ["dark", "theme", "darkmode"],
icon: <Moon />, icon: <Moon />,
label: t("ToggleDarkMode"), label: t("ToggleDarkMode"),
action: () => setTheme("dark"), action: () => setTheme("dark"),
}, },
{ {
keywords: ["system", "theme", "systemmode"], keywords: ["system", "theme", "systemmode"],
icon: <SunMoon />, icon: <SunMoon />,
label: t("ToggleSystemMode"), label: t("ToggleSystemMode"),
action: () => setTheme("system"), action: () => setTheme("system"),
}, },
].map((item) => ({ ].map((item) => ({
...item, ...item,
value: `${item.keywords.join(" ")} ${item.label}`, value: `${item.keywords.join(" ")} ${item.label}`,
})) }));
return ( return (
<> <CommandDialog open={isOpen} onOpenChange={closeCommand}>
<CommandDialog open={open} onOpenChange={setOpen}> <CommandInput
<CommandInput placeholder={t("TypeCommand")} value={search} onValueChange={setSearch} /> placeholder={t("TypeCommand")}
<CommandList className="border-t"> value={search}
<CommandEmpty>{t("NoResults")}</CommandEmpty> onValueChange={setSearch}
{nezhaWsData.servers && nezhaWsData.servers.length > 0 && ( />
<> <CommandList className="border-t">
<CommandGroup heading={t("Servers")}> <CommandEmpty>{t("NoResults")}</CommandEmpty>
{nezhaWsData.servers.map((server) => ( {nezhaWsData.servers && nezhaWsData.servers.length > 0 && (
<CommandItem <CommandGroup heading={t("Servers")}>
key={server.id} {nezhaWsData.servers.map((server) => (
value={server.name} <CommandItem
onSelect={() => { key={server.id}
navigate(`/server/${server.id}`) value={server.name}
setOpen(false) onSelect={() => {
}} navigate(`/server/${server.id}`);
> closeCommand();
{formatNezhaInfo(nezhaWsData.now, server).online ? ( }}
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center" /> >
) : ( {formatNezhaInfo(nezhaWsData.now, server).online ? (
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" /> <span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center" />
)} ) : (
<span>{server.name}</span> <span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" />
</CommandItem> )}
))} <span>{server.name}</span>
</CommandGroup> </CommandItem>
</> ))}
)} </CommandGroup>
<CommandSeparator /> )}
<CommandSeparator />
<CommandGroup heading={t("Shortcuts")}> <CommandGroup heading={t("Shortcuts")}>
{shortcuts.map((item) => ( {shortcuts.map((item) => (
<CommandItem <CommandItem
key={item.label} key={item.label}
value={item.value} value={item.value}
onSelect={() => { onSelect={() => {
item.action() item.action();
setOpen(false) closeCommand();
}} }}
> >
{item.icon} {item.icon}
<span>{item.label}</span> <span>{item.label}</span>
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</CommandDialog> </CommandDialog>
</> );
)
} }
+2 -17
View File
@@ -5,6 +5,7 @@ import { getDomains, Domain } from '@/api/domain';
import { CalendarDays, DollarSign } from 'lucide-react'; import { CalendarDays, DollarSign } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import RemainPercentBar from "./RemainPercentBar";
// ======================================================= // =======================================================
// 彩色备注标签组件 // 彩色备注标签组件
@@ -98,21 +99,7 @@ const DomainCard = ({ domain }: { domain: Domain }) => {
const billingData = domain.BillingData || {}; const billingData = domain.BillingData || {};
const customBackgroundImage = (window as any).CustomBackgroundImage !== "" ? (window as any).CustomBackgroundImage : undefined; const customBackgroundImage = (window as any).CustomBackgroundImage !== "" ? (window as any).CustomBackgroundImage : undefined;
let progressBarColor = 'bg-gray-300';
let progressBarWidth = '100%';
if (expiresIn !== undefined) {
if (expiresIn <= 10) {
progressBarColor = 'bg-red-500';
progressBarWidth = `${Math.max(5, (expiresIn / 10) * 100)}%`;
} else if (expiresIn <= 100) {
const lightness = 50 + (expiresIn - 10) / 90 * 20;
progressBarColor = `bg-[hsl(45,90%,${lightness}%)]`;
progressBarWidth = `${Math.max(5, (expiresIn / 100) * 100)}%`;
} else {
progressBarColor = 'bg-green-500';
}
}
return ( return (
<a href={`https://${domain.Domain}`} target="_blank" rel="noopener noreferrer" className="block h-full"> <a href={`https://${domain.Domain}`} target="_blank" rel="noopener noreferrer" className="block h-full">
@@ -136,9 +123,7 @@ const DomainCard = ({ domain }: { domain: Domain }) => {
<span className="truncate">{billingData.renewalPrice || 'N/A'}</span> <span className="truncate">{billingData.renewalPrice || 'N/A'}</span>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="w-full bg-muted rounded-full h-1.5 overflow-hidden"> <RemainPercentBar value={expiresIn ? Math.max(0, Math.min(100, (expiresIn / 365) * 100)) : 100} className="w-full h-1.5" />
<div className={cn("h-1.5 rounded-full transition-all duration-500", progressBarColor)} style={{ width: progressBarWidth }} />
</div>
</div> </div>
<span className="font-medium text-muted-foreground w-12 text-right">{expiresIn !== undefined ? `${expiresIn}` : 'N/A'}</span> <span className="font-medium text-muted-foreground w-12 text-right">{expiresIn !== undefined ? `${expiresIn}` : 'N/A'}</span>
</div> </div>
+27 -22
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 { interface Props {
children: React.ReactNode children: React.ReactNode;
} }
interface State { interface State {
hasError: boolean hasError: boolean;
error?: Error error?: Error;
} }
class ErrorBoundary extends React.Component<Props, State> { class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props) super(props);
this.state = { hasError: false } this.state = { hasError: false };
} }
static getDerivedStateFromError(error: Error): State { static getDerivedStateFromError(error: Error): State {
return { return {
hasError: true, hasError: true,
error, error,
} };
} }
render() { render() {
if (this.state.hasError) { if (this.state.hasError) {
return <ErrorPage code={500} message={this.state.error?.message || "应用程序发生错误"} /> return (
} <ErrorPage
code={500}
message={this.state.error?.message || "应用程序发生错误"}
/>
);
}
return this.props.children return this.props.children;
} }
} }
export default ErrorBoundary export default ErrorBoundary;
+69 -55
View File
@@ -1,60 +1,74 @@
// src/components/Footer.tsx (已添加您的署名) import { useQuery } from "@tanstack/react-query";
import type React from "react";
import { fetchSetting } from "@/lib/nezha-api" import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query" import { fetchSetting } from "@/lib/nezha-api";
import React from "react"
import { useTranslation } from "react-i18next"
const Footer: React.FC = () => { const Footer: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation();
const isMac = /macintosh|mac os x/i.test(navigator.userAgent) const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
const { data: settingData } = useQuery({ const { data: settingData } = useQuery({
queryKey: ["setting"], queryKey: ["setting"],
queryFn: () => fetchSetting(), queryFn: () => fetchSetting(),
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
}) });
return ( return (
<footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4 server-footer"> <footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4 server-footer">
<section className="flex flex-col"> <section className="flex flex-col">
<section className="mt-1 flex items-center sm:flex-row flex-col justify-between gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50 server-footer-name"> <section className="mt-1 flex items-center sm:flex-row flex-col justify-between gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50 server-footer-name">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
&copy;2020-{new Date().getFullYear()}{" "} &copy;2020-{new Date().getFullYear()}{" "}
<a href={"https://github.com/naiba/nezha"} target="_blank" className="hover:underline"> <a
Nezha href={"https://github.com/naiba/nezha"}
</a> target="_blank"
<p>{settingData?.data?.version || ""}</p> rel="noopener"
</div> >
<div className="server-footer-theme flex flex-col items-center sm:items-end"> Nezha
<p className="mt-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50"> </a>
<kbd className="pointer-events-none mx-1 inline-flex h-4 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100"> <p>{settingData?.data?.version || ""}</p>
{isMac ? <span className="text-xs"></span> : "Ctrl "}K </div>
</kbd> <div className="server-footer-theme flex flex-col items-center sm:items-end">
</p> <p className="mt-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
<section> <kbd className="pointer-events-none mx-1 inline-flex h-4 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
{t("footer.themeBy")}{" "} {isMac ? <span className="text-xs"></span> : "Ctrl "}K
<a href={"https://github.com/hamster1963/nezha-dash"} target="_blank" className="hover:underline"> </kbd>
nezha-dash </p>
</a> <section>
{import.meta.env.VITE_GIT_HASH && ( {t("footer.themeBy")}
<a href={"https://github.com/hamster1963/nezha-dash-v1/commit/" + import.meta.env.VITE_GIT_HASH} className="ml-1 hover:underline"> <a
({import.meta.env.VITE_GIT_HASH}) href={"https://github.com/hamster1963/nezha-dash"}
</a> target="_blank"
)} rel="noopener"
</section> >
<section className="mt-1"> nezha-dash
{"Modified by "} </a>
<a href={"https://github.com/buriburizaem0n"} target="_blank" className="hover:underline font-medium"> {import.meta.env.VITE_GIT_HASH && (
buriburizaem0n <a
</a> href={`https://github.com/hamster1963/nezha-dash-v1/commit/${import.meta.env.VITE_GIT_HASH}`}
</section> className="ml-1"
</div> >
</section> ({import.meta.env.VITE_GIT_HASH})
</section> </a>
</footer> )}
) </section>
} <section className="mt-1">
{"Modified by "}
<a
href={"https://github.com/buriburizaem0n"}
target="_blank"
rel="noopener"
className="hover:underline font-medium"
>
buriburizaem0n
</a>
</section>
</div>
</section>
</section>
</footer>
);
};
export default Footer export default Footer;
+210 -161
View File
@@ -1,184 +1,233 @@
import useTooltip from "@/hooks/use-tooltip" import { geoEquirectangular, geoPath } from "d3-geo";
import { geoJsonString } from "@/lib/geo-json-string" import { useTranslation } from "react-i18next";
import { countryCoordinates } from "@/lib/geo-limit" import useTooltip from "@/hooks/use-tooltip";
import { cn, formatNezhaInfo } from "@/lib/utils" import { geoJsonString } from "@/lib/geo-json-string";
import { NezhaServer } from "@/types/nezha-api" import { countryCoordinates } from "@/lib/geo-limit";
import { geoEquirectangular, geoPath } from "d3-geo" import { cn, formatNezhaInfo } from "@/lib/utils";
import { useTranslation } from "react-i18next" import type { NezhaServer } from "@/types/nezha-api";
import MapTooltip from "./MapTooltip" import MapTooltip from "./MapTooltip";
export default function GlobalMap({ serverList, now }: { serverList: NezhaServer[]; now: number }) { export default function GlobalMap({
const { t } = useTranslation() serverList,
const countryList: string[] = [] now,
const serverCounts: { [key: string]: number } = {} }: {
serverList: NezhaServer[];
now: number;
}) {
const { t } = useTranslation();
const countryList: string[] = [];
const serverCounts: { [key: string]: number } = {};
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
serverList.forEach((server) => { serverList.forEach((server) => {
if (server.country_code) { if (server.country_code) {
const countryCode = server.country_code.toUpperCase() const countryCode = server.country_code.toUpperCase();
if (!countryList.includes(countryCode)) { if (!countryList.includes(countryCode)) {
countryList.push(countryCode) countryList.push(countryCode);
} }
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1 serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1;
} }
}) });
const width = 900 const width = 900;
const height = 500 const height = 500;
const geoJson = JSON.parse(geoJsonString) const geoJson = JSON.parse(geoJsonString);
const filteredFeatures = geoJson.features.filter((feature: { properties: { iso_a3_eh: string } }) => feature.properties.iso_a3_eh !== "") const filteredFeatures = geoJson.features.filter(
(feature: { properties: { iso_a3_eh: string } }) =>
feature.properties.iso_a3_eh !== "",
);
return ( return (
<section <section
className={cn("flex flex-col gap-4 mt-8", { className={cn("flex flex-col gap-4 mt-8", {
"bg-card/70 rounded-lg p-4": customBackgroundImage, "bg-card/70 rounded-lg p-4": customBackgroundImage,
})} })}
> >
<p className="text-sm font-medium opacity-40"> <p className="text-sm font-medium opacity-40">
{t("map.Distributions")} {countryList.length} {t("map.Regions")} {t("map.Distributions")} {countryList.length} {t("map.Regions")}
</p> </p>
<div className="w-full overflow-x-auto"> <div className="w-full overflow-x-auto">
<InteractiveMap <InteractiveMap
countries={countryList} countries={countryList}
serverCounts={serverCounts} serverCounts={serverCounts}
width={width} width={width}
height={height} height={height}
filteredFeatures={filteredFeatures} filteredFeatures={filteredFeatures}
nezhaServerList={serverList} nezhaServerList={serverList}
now={now} now={now}
/> />
</div> </div>
</section> </section>
) );
} }
interface InteractiveMapProps { interface InteractiveMapProps {
countries: string[] countries: string[];
serverCounts: { [key: string]: number } serverCounts: { [key: string]: number };
width: number width: number;
height: number height: number;
filteredFeatures: { filteredFeatures: {
type: "Feature" type: "Feature";
properties: { properties: {
iso_a2_eh: string iso_a2_eh: string;
[key: string]: string [key: string]: string;
} };
geometry: never geometry: never;
}[] }[];
nezhaServerList: NezhaServer[] nezhaServerList: NezhaServer[];
now: number now: number;
} }
export function InteractiveMap({ countries, serverCounts, width, height, filteredFeatures, nezhaServerList, now }: InteractiveMapProps) { export function InteractiveMap({
const { setTooltipData } = useTooltip() countries,
serverCounts,
width,
height,
filteredFeatures,
nezhaServerList,
now,
}: InteractiveMapProps) {
const { setTooltipData } = useTooltip();
const projection = geoEquirectangular() const projection = geoEquirectangular()
.scale(140) .scale(140)
.translate([width / 2, height / 2]) .translate([width / 2, height / 2])
.rotate([-12, 0, 0]) .rotate([-12, 0, 0]);
const path = geoPath().projection(projection) const path = geoPath().projection(projection);
return ( return (
<div className="relative w-full aspect-[2/1]" onMouseLeave={() => setTooltipData(null)}> <div
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} xmlns="http://www.w3.org/2000/svg" className="w-full h-auto"> className="relative w-full aspect-2/1"
<defs> onMouseLeave={() => setTooltipData(null)}
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse"> >
<circle cx="1" cy="1" r="0.5" fill="currentColor" /> <svg
</pattern> width={width}
</defs> height={height}
<g> viewBox={`0 0 ${width} ${height}`}
{/* Background rect to handle mouse events in empty areas */} xmlns="http://www.w3.org/2000/svg"
<rect x="0" y="0" width={width} height={height} fill="transparent" onMouseEnter={() => setTooltipData(null)} /> className="w-full h-auto"
{filteredFeatures.map((feature, index) => { >
const isHighlighted = countries.includes(feature.properties.iso_a2_eh) <defs>
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.5" fill="currentColor" />
</pattern>
</defs>
<g>
{/* Background rect to handle mouse events in empty areas */}
<rect
x="0"
y="0"
width={width}
height={height}
fill="transparent"
onMouseEnter={() => setTooltipData(null)}
/>
{filteredFeatures.map((feature, index) => {
const isHighlighted = countries.includes(
feature.properties.iso_a2_eh,
);
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0 const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0;
return ( return (
<path <path
key={index} key={index}
d={path(feature) || ""} d={path(feature) || ""}
className={ className={
isHighlighted isHighlighted
? "fill-green-700 hover:fill-green-600 dark:fill-green-900 dark:hover:fill-green-700 transition-all cursor-pointer" ? "fill-green-700 hover:fill-green-600 dark:fill-green-900 dark:hover:fill-green-700 transition-all cursor-pointer"
: "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]" : "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]"
} }
onMouseEnter={() => { onMouseEnter={() => {
if (!isHighlighted) { if (!isHighlighted) {
setTooltipData(null) setTooltipData(null);
return return;
} }
if (path.centroid(feature)) { if (path.centroid(feature)) {
const countryCode = feature.properties.iso_a2_eh const countryCode = feature.properties.iso_a2_eh;
const countryServers = nezhaServerList const countryServers = nezhaServerList
.filter((server: NezhaServer) => server.country_code?.toUpperCase() === countryCode) .filter(
.map((server: NezhaServer) => ({ (server: NezhaServer) =>
name: server.name, server.country_code?.toUpperCase() === countryCode,
status: formatNezhaInfo(now, server).online, )
})) .map((server: NezhaServer) => ({
setTooltipData({ id: server.id,
centroid: path.centroid(feature), name: server.name,
country: feature.properties.name, status: formatNezhaInfo(now, server).online,
count: serverCount, }));
servers: countryServers, setTooltipData({
}) centroid: path.centroid(feature),
} country: feature.properties.name,
}} count: serverCount,
/> servers: countryServers,
) });
})} }
}}
/>
);
})}
{/* 渲染不在 filteredFeatures 中的国家标记点 */} {/* 渲染不在 filteredFeatures 中的国家标记点 */}
{countries.map((countryCode) => { {countries.map((countryCode) => {
// 检查该国家是否已经在 filteredFeatures 中 // 检查该国家是否已经在 filteredFeatures 中
const isInFilteredFeatures = filteredFeatures.some((feature) => feature.properties.iso_a2_eh === countryCode) const isInFilteredFeatures = filteredFeatures.some(
(feature) => feature.properties.iso_a2_eh === countryCode,
);
// 如果已经在 filteredFeatures 中,跳过 // 如果已经在 filteredFeatures 中,跳过
if (isInFilteredFeatures) return null if (isInFilteredFeatures) return null;
// 获取国家的经纬度 // 获取国家的经纬度
const coords = countryCoordinates[countryCode] const coords = countryCoordinates[countryCode];
if (!coords) return null if (!coords) return null;
// 使用投影函数将经纬度转换为 SVG 坐标 // 使用投影函数将经纬度转换为 SVG 坐标
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0] const [x, y] = projection([coords.lng, coords.lat]) || [0, 0];
const serverCount = serverCounts[countryCode] || 0 const serverCount = serverCounts[countryCode] || 0;
return ( return (
<g <g
key={countryCode} key={countryCode}
onMouseEnter={() => { onMouseEnter={() => {
const countryServers = nezhaServerList const countryServers = nezhaServerList
.filter((server: NezhaServer) => server.country_code?.toUpperCase() === countryCode.toUpperCase()) .filter(
.map((server: NezhaServer) => ({ (server: NezhaServer) =>
name: server.name, server.country_code?.toUpperCase() ===
status: formatNezhaInfo(now, server).online, countryCode.toUpperCase(),
})) )
setTooltipData({ .map((server: NezhaServer) => ({
centroid: [x, y], id: server.id,
country: coords.name, name: server.name,
count: serverCount, status: formatNezhaInfo(now, server).online,
servers: countryServers, }));
}) setTooltipData({
}} centroid: [x, y],
className="cursor-pointer" country: coords.name,
> count: serverCount,
<circle servers: countryServers,
cx={x} });
cy={y} }}
r={4} className="cursor-pointer"
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all" >
/> <circle
</g> cx={x}
) cy={y}
})} r={4}
</g> className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
</svg> />
<MapTooltip /> </g>
</div> );
) })}
</g>
</svg>
<MapTooltip />
</div>
);
} }
+96 -77
View File
@@ -1,92 +1,111 @@
import { cn } from "@/lib/utils" import { m } from "framer-motion";
import { m } from "framer-motion" import { createRef, useEffect, useRef } from "react";
import { createRef, useEffect, useRef } from "react" import { cn } from "@/lib/utils";
export default function GroupSwitch({ export default function GroupSwitch({
tabs, tabs,
currentTab, currentTab,
setCurrentTab, setCurrentTab,
}: { }: {
tabs: string[] tabs: string[];
currentTab: string currentTab: string;
setCurrentTab: (tab: string) => void setCurrentTab: (tab: string) => void;
}) { }) {
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const scrollRef = useRef<HTMLDivElement>(null) const scrollRef = useRef<HTMLDivElement>(null);
const tagRefs = useRef(tabs.map(() => createRef<HTMLDivElement>())) const tagRefs = useRef(tabs.map(() => createRef<HTMLDivElement>()));
useEffect(() => { useEffect(() => {
const container = scrollRef.current const container = scrollRef.current;
if (!container) return if (!container) return;
const isOverflowing = container.scrollWidth > container.clientWidth const isOverflowing = container.scrollWidth > container.clientWidth;
if (!isOverflowing) return if (!isOverflowing) return;
const onWheel = (e: WheelEvent) => { const onWheel = (e: WheelEvent) => {
e.preventDefault() e.preventDefault();
container.scrollLeft += e.deltaY container.scrollLeft += e.deltaY;
} };
container.addEventListener("wheel", onWheel, { passive: false }) container.addEventListener("wheel", onWheel, { passive: false });
return () => { return () => {
container.removeEventListener("wheel", onWheel) container.removeEventListener("wheel", onWheel);
} };
}, []) }, []);
useEffect(() => { useEffect(() => {
const savedGroup = sessionStorage.getItem("selectedGroup") if (tabs.length === 1 && tabs[0] === "All") {
if (savedGroup && tabs.includes(savedGroup)) { setCurrentTab("All");
setCurrentTab(savedGroup) return;
} }
}, [tabs, setCurrentTab]) const savedGroup = sessionStorage.getItem("selectedGroup");
if (savedGroup && tabs.includes(savedGroup)) {
setCurrentTab(savedGroup);
}
}, [tabs, setCurrentTab]);
useEffect(() => { useEffect(() => {
const currentTagRef = tagRefs.current[tabs.indexOf(currentTab)] const currentTagRef = tagRefs.current[tabs.indexOf(currentTab)];
if (currentTagRef && currentTagRef.current) { if (currentTagRef?.current) {
currentTagRef.current.scrollIntoView({ currentTagRef.current.scrollIntoView({
behavior: "smooth", behavior: "smooth",
block: "nearest", block: "nearest",
inline: "center", inline: "center",
}) });
} }
}, [currentTab]) }, [currentTab, tabs.indexOf]);
return ( if (tabs.length === 1 && tabs[0] === "All") {
<div ref={scrollRef} className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]"> return null;
<div }
className={cn("flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800", {
"bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage, return (
})} <div
> ref={scrollRef}
{tabs.map((tab: string, index: number) => ( className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]"
<div >
key={tab} <div
ref={tagRefs.current[index]} className={cn(
onClick={() => setCurrentTab(tab)} "flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800",
className={cn( {
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500", "bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage,
currentTab === tab ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500", },
)} )}
> >
{currentTab === tab && ( {tabs.map((tab: string, index: number) => (
<m.div <div
layoutId="tab-switch" key={tab}
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5" ref={tagRefs.current[index]}
style={{ onClick={() => setCurrentTab(tab)}
originY: "0px", className={cn(
borderRadius: 46, "relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-semibold transition-all duration-500",
}} currentTab === tab
/> ? "text-black dark:text-white"
)} : "text-stone-400 dark:text-stone-500",
<div className="relative z-20 flex items-center gap-1"> )}
<p className="whitespace-nowrap">{tab}</p> >
</div> {currentTab === tab && (
</div> <m.div
))} layoutId="tab-switch"
</div> className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5"
</div> style={{
) originY: "0px",
borderRadius: 46,
}}
/>
)}
<div className="relative z-20 flex items-center gap-1">
<p className="whitespace-nowrap">{tab}</p>
</div>
</div>
))}
</div>
</div>
);
} }
+317 -259
View File
@@ -1,303 +1,361 @@
import { ModeToggle } from "@/components/ThemeSwitcher" import { useQuery } from "@tanstack/react-query";
import { Separator } from "@/components/ui/separator" import { AnimatePresence, m } from "framer-motion";
import { Skeleton } from "@/components/ui/skeleton" import { ImageMinus } from "lucide-react";
import { useBackground } from "@/hooks/use-background" import { DateTime } from "luxon";
import { useWebSocketContext } from "@/hooks/use-websocket-context" import { useEffect, useRef, useState } from "react";
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api" import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils" import { useNavigate } from "react-router-dom";
import NumberFlow, { NumberFlowGroup } from "@number-flow/react" import { ModeToggle } from "@/components/ThemeSwitcher";
import { useQuery } from "@tanstack/react-query" import { Separator } from "@/components/ui/separator";
import { AnimatePresence, m } from "framer-motion" import { Skeleton } from "@/components/ui/skeleton";
import { ImageMinus } from "lucide-react" import { useBackground } from "@/hooks/use-background";
import { DateTime } from "luxon" import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { useEffect, useRef, useState } from "react" import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api";
import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils";
import { useNavigate } from "react-router-dom"
import { LanguageSwitcher } from "./LanguageSwitcher" import AnimateCountClient from "./AnimatedCount";
import { Loader, LoadingSpinner } from "./loading/Loader" import { LanguageSwitcher } from "./LanguageSwitcher";
import { Button } from "./ui/button" import { Loader, LoadingSpinner } from "./loading/Loader";
import { SearchButton } from "./SearchButton";
import { Button } from "./ui/button";
interface TimeState {
hh: number;
mm: number;
ss: number;
}
const useCurrentTime = () => {
const [time, setTime] = useState<TimeState>({
hh: DateTime.now().setLocale("en-US").hour,
mm: DateTime.now().setLocale("en-US").minute,
ss: DateTime.now().setLocale("en-US").second,
});
useEffect(() => {
const intervalId = setInterval(() => {
const now = DateTime.now().setLocale("en-US");
setTime({
hh: now.hour,
mm: now.minute,
ss: now.second,
});
}, 1000);
return () => clearInterval(intervalId);
}, []);
return time;
};
function Header() { function Header() {
const { t } = useTranslation() const { t } = useTranslation();
const navigate = useNavigate() const navigate = useNavigate();
const { backgroundImage, updateBackground } = useBackground() const { backgroundImage, updateBackground } = useBackground();
const { data: settingData, isLoading } = useQuery({ const { data: settingData, isLoading } = useQuery({
queryKey: ["setting"], queryKey: ["setting"],
queryFn: () => fetchSetting(), queryFn: () => fetchSetting(),
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
}) });
const { lastMessage, connected } = useWebSocketContext() const { lastMessage, connected } = useWebSocketContext();
const onlineCount = connected ? (lastMessage ? JSON.parse(lastMessage.data).online || 0 : 0) : "..." const onlineCount = connected
? lastMessage
? JSON.parse(lastMessage.data).online || 0
: 0
: "...";
const siteName = settingData?.data?.config?.site_name const siteName = settingData?.data?.config?.site_name;
// @ts-expect-error CustomLogo is a global variable const customLogo = window.CustomLogo || "/apple-touch-icon.png";
const customLogo = window.CustomLogo || "/apple-touch-icon.png"
// @ts-expect-error CustomDesc is a global variable const customDesc = window.CustomDesc || t("nezha");
const customDesc = window.CustomDesc || t("nezha")
const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined const customMobileBackgroundImage =
window.CustomMobileBackgroundImage !== ""
? window.CustomMobileBackgroundImage
: undefined;
useEffect(() => { useEffect(() => {
const link = document.querySelector("link[rel*='icon']") || document.createElement("link") const link =
// @ts-expect-error set link.type document.querySelector("link[rel*='icon']") ||
link.type = "image/x-icon" document.createElement("link");
// @ts-expect-error set link.rel // @ts-expect-error set link.type
link.rel = "shortcut icon" link.type = "image/x-icon";
// @ts-expect-error set link.href // @ts-expect-error set link.rel
link.href = customLogo link.rel = "shortcut icon";
document.getElementsByTagName("head")[0].appendChild(link) // @ts-expect-error set link.href
}, [customLogo]) link.href = customLogo;
document.getElementsByTagName("head")[0].appendChild(link);
}, [customLogo]);
useEffect(() => { useEffect(() => {
document.title = siteName || "哪吒监控 Nezha Monitoring" document.title = siteName || "哪吒监控 Nezha Monitoring";
}, [siteName]) }, [siteName]);
const handleBackgroundToggle = () => { const handleBackgroundToggle = () => {
if (window.CustomBackgroundImage) { if (window.CustomBackgroundImage) {
// Store the current background image before removing it // Store the current background image before removing it
sessionStorage.setItem("savedBackgroundImage", window.CustomBackgroundImage) sessionStorage.setItem(
updateBackground(undefined) "savedBackgroundImage",
} else { window.CustomBackgroundImage,
// Restore the saved background image );
const savedImage = sessionStorage.getItem("savedBackgroundImage") updateBackground(undefined);
if (savedImage) { } else {
updateBackground(savedImage) // Restore the saved background image
} const savedImage = sessionStorage.getItem("savedBackgroundImage");
} if (savedImage) {
} updateBackground(savedImage);
}
}
};
const customBackgroundImage = backgroundImage const customBackgroundImage = backgroundImage;
return ( return (
<div className="mx-auto w-full max-w-5xl"> <div className="mx-auto w-full max-w-5xl">
<section className="flex items-center justify-between header-top"> <section className="flex items-center justify-between header-top">
<section <section
onClick={() => { onClick={() => {
sessionStorage.removeItem("selectedGroup") sessionStorage.removeItem("selectedGroup");
navigate("/") navigate("/");
}} }}
className="cursor-pointer flex items-center sm:text-base text-sm font-medium" className="cursor-pointer flex items-center sm:text-base text-sm font-medium"
> >
<div className="mr-1 flex flex-row items-center justify-start header-logo"> <div className="mr-1 flex flex-row items-center justify-start header-logo">
<img <img
width={40} width={40}
height={40} height={40}
alt="apple-touch-icon" alt="apple-touch-icon"
src={customLogo} src={customLogo}
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!" className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!"
/> />
</div> </div>
{isLoading ? <Skeleton className="h-6 w-20 rounded-[5px] bg-muted-foreground/10 animate-none" /> : siteName || "NEZHA"} {isLoading ? (
<Separator orientation="vertical" className="mx-2 hidden h-4 w-[1px] md:block" /> <Skeleton className="h-6 w-20 rounded-[5px] bg-muted-foreground/10 animate-none" />
<p className="hidden text-sm font-medium opacity-40 md:block">{customDesc}</p> ) : (
</section> siteName || "NEZHA"
<section className="flex items-center gap-2 header-handles"> )}
<div className="hidden sm:flex items-center gap-2"> <Separator
<Links /> orientation="vertical"
<DashboardLink /> className="mx-2 hidden h-4 w-px md:block"
</div> />
<LanguageSwitcher /> <p className="hidden text-sm font-medium opacity-40 md:block">
<ModeToggle /> {customDesc}
{(customBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) && ( </p>
<Button </section>
variant="outline" <section className="flex items-center gap-2 header-handles">
size="sm" <div className="hidden sm:flex items-center gap-2">
onClick={handleBackgroundToggle} <Links />
className={cn("rounded-full px-[9px] bg-white dark:bg-black", { <DashboardLink />
"bg-white/70 dark:bg-black/70": customBackgroundImage, </div>
"hidden sm:block": customMobileBackgroundImage, <SearchButton />
})} <LanguageSwitcher />
> <ModeToggle />
<ImageMinus className="w-4 h-4" /> {(customBackgroundImage ||
</Button> sessionStorage.getItem("savedBackgroundImage")) && (
)} <Button
<Button variant="outline"
variant="outline" size="sm"
size="sm" onClick={handleBackgroundToggle}
className={cn("hover:bg-white dark:hover:bg-black cursor-default rounded-full flex items-center px-[9px] bg-white dark:bg-black", { className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
"bg-white/70 dark:bg-black/70": customBackgroundImage, "bg-white/70 dark:bg-black/70": customBackgroundImage,
})} "hidden sm:block": customMobileBackgroundImage,
> })}
{connected ? onlineCount : <Loader visible={true} />} >
<p className="text-muted-foreground">{connected ? t("online") : t("offline")}</p> <ImageMinus className="w-4 h-4" />
<span </Button>
className={cn("h-2 w-2 rounded-full bg-green-500", { )}
"bg-red-500": !connected, <Button
})} variant="outline"
></span> size="sm"
</Button> className={cn(
</section> "hover:bg-white dark:hover:bg-black cursor-default rounded-full flex items-center px-[9px] bg-white dark:bg-black",
</section> {
<div className="w-full flex justify-between sm:hidden mt-1"> "bg-white/70 dark:bg-black/70": customBackgroundImage,
<DashboardLink /> },
<Links /> )}
</div> >
<Overview /> {connected ? onlineCount : <Loader visible={true} />}
</div> <p className="text-muted-foreground">
) {connected ? t("online") : t("offline")}
</p>
<span
className={cn("h-2 w-2 rounded-full bg-green-500", {
"bg-red-500": !connected,
})}
></span>
</Button>
</section>
</section>
<div className="w-full flex justify-between sm:hidden mt-1">
<DashboardLink />
<Links />
</div>
<Overview />
</div>
);
} }
type links = { type links = {
link: string link: string;
name: string name: string;
} };
function Links() { function Links() {
// @ts-expect-error CustomLinks is a global variable const customLinks = window.CustomLinks as string;
const customLinks = window.CustomLinks as string
const links: links[] | null = customLinks ? JSON.parse(customLinks) : null const links: links[] | null = customLinks ? JSON.parse(customLinks) : null;
if (!links) return null if (!links) return null;
return ( return (
<div className="flex items-center gap-2 w-fit"> <div className="flex items-center gap-2 w-fit">
{links.map((link, index) => { {links.map((link, index) => {
return ( return (
<a <a
key={index} key={index}
href={link.link} href={link.link}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100" className="flex items-center gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
> >
{link.name} {link.name}
</a> </a>
) );
})} })}
</div> </div>
) );
} }
export function RefreshToast() { export function RefreshToast() {
const { t } = useTranslation() const { t } = useTranslation();
const navigate = useNavigate() const navigate = useNavigate();
const { needReconnect } = useWebSocketContext() const { needReconnect } = useWebSocketContext();
if (!needReconnect) { if (!needReconnect) {
return null return null;
} }
if (needReconnect) { if (needReconnect) {
sessionStorage.removeItem("needRefresh") sessionStorage.removeItem("needRefresh");
setTimeout(() => { setTimeout(() => {
navigate(0) navigate(0);
}, 1000) }, 1000);
} }
return ( return (
<AnimatePresence> <AnimatePresence>
<m.div <m.div
initial={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }} initial={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }} animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
exit={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }} exit={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
transition={{ type: "spring", duration: 0.8 }} transition={{ type: "spring", duration: 0.8 }}
className="fixed left-1/2 -translate-x-1/2 top-8 z-[999] flex items-center justify-between gap-4 rounded-[50px] border-[1px] border-solid bg-white px-2 py-1.5 shadow-xl shadow-black/5 dark:border-stone-700 dark:bg-stone-800 dark:shadow-none" className="fixed left-1/2 -translate-x-1/2 top-8 z-999 flex items-center justify-between gap-4 rounded-[50px] border border-solid bg-white px-2 py-1.5 shadow-xl shadow-black/5 dark:border-stone-700 dark:bg-stone-800 dark:shadow-none"
> >
<section className="flex items-center gap-1.5"> <section className="flex items-center gap-1.5">
<LoadingSpinner /> <LoadingSpinner />
<p className="text-[12.5px] font-medium">{t("refreshing")}...</p> <p className="text-[12.5px] font-medium">{t("refreshing")}...</p>
</section> </section>
</m.div> </m.div>
</AnimatePresence> </AnimatePresence>
) );
} }
function DashboardLink() { function DashboardLink() {
const { t } = useTranslation() const { t } = useTranslation();
const { setNeedReconnect } = useWebSocketContext() const { setNeedReconnect } = useWebSocketContext();
const previousLoginState = useRef<boolean | null>(null) const previousLoginState = useRef<boolean | null>(null);
const { const {
data: userData, data: userData,
isFetched, isFetched,
isLoadingError, isLoadingError,
isError, isError,
refetch, refetch,
} = useQuery({ } = useQuery({
queryKey: ["login-user"], queryKey: ["login-user"],
queryFn: () => fetchLoginUser(), queryFn: () => fetchLoginUser(),
refetchOnMount: false, refetchOnMount: false,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
refetchIntervalInBackground: true, refetchIntervalInBackground: true,
refetchInterval: 1000 * 30, refetchInterval: 1000 * 30,
retry: 0, retry: 0,
}) });
const isLogin = isError ? false : userData ? !!userData?.data?.id && !!document.cookie : false const isLogin = isError
? false
: userData
? !!userData?.data?.id && !!document.cookie
: false;
if (isLoadingError) { if (isLoadingError) {
previousLoginState.current = isLogin previousLoginState.current = isLogin;
} }
useEffect(() => { useEffect(() => {
refetch() refetch();
}, [document.cookie]) }, [refetch]);
useEffect(() => { useEffect(() => {
if (isFetched || isError) { if (isFetched || isError) {
// 只有当登录状态发生变化时才设置needReconnect // 只有当登录状态发生变化时才设置needReconnect
if (previousLoginState.current !== null && previousLoginState.current !== isLogin) { if (
setNeedReconnect(true) previousLoginState.current !== null &&
} previousLoginState.current !== isLogin
previousLoginState.current = isLogin ) {
} setNeedReconnect(true);
}, [isLogin]) }
previousLoginState.current = isLogin;
}
}, [isLogin, isError, isFetched, setNeedReconnect]);
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<a <a
href={"/dashboard"} href={"/dashboard"}
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center text-nowrap gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100" className="flex items-center text-nowrap gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
> >
{!isLogin && t("login")} {!isLogin && t("login")}
{isLogin && t("dashboard")} {isLogin && t("dashboard")}
</a> </a>
</div> </div>
) );
} }
function Overview() { function Overview() {
const { t } = useTranslation() const { t } = useTranslation();
const [time, setTime] = useState({ const time = useCurrentTime();
hh: DateTime.now().setLocale("en-US").hour, const [mounted, setMounted] = useState(false);
mm: DateTime.now().setLocale("en-US").minute,
ss: DateTime.now().setLocale("en-US").second,
})
useEffect(() => { useEffect(() => {
const timer = setInterval(() => { setMounted(true);
setTime({ }, []);
hh: DateTime.now().setLocale("en-US").hour,
mm: DateTime.now().setLocale("en-US").minute,
ss: DateTime.now().setLocale("en-US").second,
})
}, 1000)
return () => clearInterval(timer) return (
}, []) <section className={"mt-10 flex flex-col md:mt-16 header-timer"}>
return ( <p className="text-base font-semibold">👋 {t("overview")}</p>
<section className={"mt-10 flex flex-col md:mt-16 header-timer"}> <div className="flex items-center gap-1">
<p className="text-base font-semibold">👋 {t("overview")}</p> <p className="text-sm font-medium opacity-50">{t("whereTheTimeIs")}</p>
<div className="flex items-center gap-1.5"> {mounted ? (
<p className="text-sm font-medium opacity-50">{t("whereTheTimeIs")}</p> <div className="flex items-center font-medium text-sm">
<NumberFlowGroup> <AnimateCountClient count={time.hh} minDigits={2} />
<div style={{ fontVariantNumeric: "tabular-nums" }} className="flex text-sm font-medium mt-0.5"> <span className="mb-px font-medium text-sm opacity-50">:</span>
<NumberFlow trend={1} value={time.hh} format={{ minimumIntegerDigits: 2 }} /> <AnimateCountClient count={time.mm} minDigits={2} />
<NumberFlow prefix=":" trend={1} value={time.mm} digits={{ 1: { max: 5 } }} format={{ minimumIntegerDigits: 2 }} /> <span className="mb-px font-medium text-sm opacity-50">:</span>
<p className="mt-[0.5px]">:{time.ss.toString().padStart(2, "0")}</p> <span className="font-medium text-sm">
</div> <AnimateCountClient count={time.ss} minDigits={2} />
</NumberFlowGroup> </span>
</div> </div>
</section> ) : (
) <Skeleton className="h-[21px] w-16 animate-none rounded-[5px] bg-muted-foreground/10" />
)}
</div>
</section>
);
} }
export default Header export default Header;
+27 -27
View File
File diff suppressed because one or more lines are too long
+69 -45
View File
@@ -1,54 +1,78 @@
"use client" "use client";
import { Button } from "@/components/ui/button" import { CheckCircleIcon, LanguageIcon } from "@heroicons/react/20/solid";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button";
import { CheckCircleIcon, LanguageIcon } from "@heroicons/react/20/solid" import {
import { useTranslation } from "react-i18next" DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
export function LanguageSwitcher() { export function LanguageSwitcher() {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation();
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const locale = i18n.languages[0] const locale = i18n.languages[0];
const handleSelect = (e: Event, newLocale: string) => { const handleSelect = (e: Event, newLocale: string) => {
e.preventDefault() // 阻止默认的关闭行为 e.preventDefault(); // 阻止默认的关闭行为
i18n.changeLanguage(newLocale) i18n.changeLanguage(newLocale);
} };
const localeItems = [ const localeItems = [
{ name: t("language.zh-CN"), code: "zh-CN" }, { name: t("language.zh-CN"), code: "zh-CN" },
{ name: t("language.zh-TW"), code: "zh-TW" }, { name: t("language.zh-TW"), code: "zh-TW" },
{ name: t("language.en-US"), code: "en-US" }, { name: t("language.en-US"), code: "en-US" },
{ name: t("language.ru-RU"), code: "ru-RU" }, { name: t("language.ru-RU"), code: "ru-RU" },
{ name: t("language.es-ES"), code: "es-ES" }, { name: t("language.es-ES"), code: "es-ES" },
{ name: t("language.de-DE"), code: "de-DE" }, { name: t("language.de-DE"), code: "de-DE" },
{ name: t("language.ta-IN"), code: "ta-IN" }, { name: t("language.ta-IN"), code: "ta-IN" },
] ];
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className={cn("rounded-full px-[9px] bg-white dark:bg-black", { className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
"bg-white/70 dark:bg-black/70": customBackgroundImage, "bg-white/70 dark:bg-black/70": customBackgroundImage,
})} })}
> >
<LanguageIcon className="size-4" /> <LanguageIcon className="size-4" />
<span className="sr-only">Change language</span> <span className="sr-only">Change language</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="flex flex-col gap-0.5" align="end"> <DropdownMenuContent className="flex flex-col gap-0.5" align="end">
{localeItems.map((item) => ( {localeItems.map((item, index) => (
<DropdownMenuItem key={item.code} onSelect={(e) => handleSelect(e, item.code)} className={locale === item.code ? "bg-muted gap-3" : ""}> <DropdownMenuItem
{item.name} {locale === item.code && <CheckCircleIcon className="size-4" />} key={item.code}
</DropdownMenuItem> onSelect={(e) => handleSelect(e, item.code)}
))} className={cn(
</DropdownMenuContent> "text-xs",
</DropdownMenu> {
) "gap-3 bg-muted font-semibold": locale === item.code,
},
{
"rounded-t-[5px]": index === localeItems.length - 1,
"rounded-[5px]":
index !== 0 && index !== localeItems.length - 1,
"rounded-b-[5px]": index === 0,
},
)}
>
{item.name}{" "}
{locale === item.code && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
} }
+65 -49
View File
@@ -1,54 +1,70 @@
import useTooltip from "@/hooks/use-tooltip" import { AnimatePresence, m } from "framer-motion";
import { AnimatePresence, m } from "framer-motion" import { memo } from "react";
import { memo } from "react" import { useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom";
import useTooltip from "@/hooks/use-tooltip";
const MapTooltip = memo(function MapTooltip() { const MapTooltip = memo(function MapTooltip() {
const { t } = useTranslation() const { t } = useTranslation();
const { tooltipData } = useTooltip() const navigate = useNavigate();
const { tooltipData } = useTooltip();
if (!tooltipData) return null if (!tooltipData) return null;
return ( return (
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
<m.div <m.div
initial={{ opacity: 0, filter: "blur(10px)" }} initial={{ opacity: 0, filter: "blur(10px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }} animate={{ opacity: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, filter: "blur(10px)" }} exit={{ opacity: 0, filter: "blur(10px)" }}
className="absolute hidden lg:block bg-white dark:bg-neutral-800 px-2 py-1 rounded shadow-lg text-sm dark:border dark:border-neutral-700 z-50" className="absolute hidden lg:block bg-white dark:bg-neutral-800 px-2 py-1 rounded shadow-lg text-sm dark:border dark:border-neutral-700 z-50"
key={tooltipData.country} key={tooltipData.country}
style={{ style={{
left: tooltipData.centroid[0], left: tooltipData.centroid[0],
top: tooltipData.centroid[1], top: tooltipData.centroid[1],
transform: "translate(20%, -50%)", transform: "translate(20%, -50%)",
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.stopPropagation() e.stopPropagation();
}} }}
> >
<div> <div>
<p className="font-medium">{tooltipData.country === "China" ? "Mainland China" : tooltipData.country}</p> <p className="font-medium">
<p className="text-neutral-600 dark:text-neutral-400 mb-1"> {tooltipData.country === "China"
{tooltipData.count} {t("map.Servers")} ? "Mainland China"
</p> : tooltipData.country}
</div> </p>
<div <p className="text-neutral-600 dark:text-neutral-400 text-xs font-light mb-1">
className="border-t dark:border-neutral-700 pt-1" {tooltipData.count} {t("map.Servers")}
style={{ </p>
maxHeight: "200px", </div>
overflowY: "auto", <div
}} className="border-t dark:border-neutral-700 pt-1"
> style={{
{tooltipData.servers.map((server, index: number) => ( maxHeight: "200px",
<div key={index} className="flex items-center gap-1.5 py-0.5"> overflowY: "auto",
<span className={`w-1.5 h-1.5 shrink-0 rounded-full ${server.status ? "bg-green-500" : "bg-red-500"}`}></span> }}
<span className="text-xs">{server.name}</span> >
</div> {tooltipData.servers.map((server) => (
))} <button
</div> key={server.id}
</m.div> type="button"
</AnimatePresence> className="flex items-center gap-1.5 py-0.5 text-neutral-500 transition-colors hover:text-black dark:text-neutral-400 dark:hover:text-white"
) onClick={() => {
}) sessionStorage.setItem("fromMainPage", "true");
navigate(`/server/${server.id}`);
}}
>
<span
className={`h-1.5 w-1.5 shrink-0 rounded-full ${server.status ? "bg-green-500" : "bg-red-500"}`}
/>
<span className="text-xs">{server.name}</span>
</button>
))}
</div>
</m.div>
</AnimatePresence>
);
});
export default MapTooltip export default MapTooltip;
File diff suppressed because it is too large Load Diff
+20 -20
View File
@@ -1,23 +1,23 @@
import { Loader } from "@/components/loading/Loader" import { Loader } from "@/components/loading/Loader";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function NetworkChartLoading() { export default function NetworkChartLoading() {
return ( return (
<Card> <Card>
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row"> <CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5"> <div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5">
<CardTitle className="flex items-center gap-0.5 text-xl"> <CardTitle className="flex items-center gap-0.5 text-xl">
<div className="aspect-auto h-[20px] w-24 bg-muted"></div> <div className="aspect-auto h-[20px] w-24 bg-muted"></div>
</CardTitle> </CardTitle>
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted"></div> <div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted"></div>
</div> </div>
<div className="hidden pr-4 pt-4 sm:block"> <div className="hidden pr-4 pt-4 sm:block">
<Loader visible={true} /> <Loader visible={true} />
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="px-2 sm:p-6"> <CardContent className="px-2 sm:p-6">
<div className="aspect-auto h-[250px] w-full"></div> <div className="aspect-auto h-[250px] w-full"></div>
</CardContent> </CardContent>
</Card> </Card>
) );
} }
+81 -54
View File
@@ -1,58 +1,85 @@
import { PublicNoteData, cn } from "@/lib/utils" import { cn, type PublicNoteData } from "@/lib/utils";
export default function PlanInfo({ parsedData }: { parsedData: PublicNoteData }) { export default function PlanInfo({
if (!parsedData || !parsedData.planDataMod) { parsedData,
return null }: {
} parsedData: PublicNoteData;
}) {
if (!parsedData || !parsedData.planDataMod) {
return null;
}
const extraList = const extraList =
parsedData.planDataMod.extra.split(",").length > 1 parsedData.planDataMod.extra.split(",").length > 1
? parsedData.planDataMod.extra.split(",") ? parsedData.planDataMod.extra.split(",")
: parsedData.planDataMod.extra.split(",")[0] === "" : parsedData.planDataMod.extra.split(",")[0] === ""
? [] ? []
: [parsedData.planDataMod.extra] : [parsedData.planDataMod.extra];
const networkRoutes = parsedData.planDataMod.networkRoute
? parsedData.planDataMod.networkRoute.split(",")
: [];
return ( return (
<section className="flex gap-1 items-center flex-wrap mt-0.5"> <section className="flex gap-1 items-center flex-wrap mt-0.5">
{parsedData.planDataMod.bandwidth !== "" && ( {parsedData.planDataMod.bandwidth !== "" && (
<p className={cn("text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}> <p
{parsedData.planDataMod.bandwidth} className={cn(
</p> "text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)} )}
{parsedData.planDataMod.trafficVol !== "" && ( >
<p className={cn("text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}> {parsedData.planDataMod.bandwidth}
{parsedData.planDataMod.trafficVol} </p>
</p> )}
)} {parsedData.planDataMod.trafficVol !== "" && (
{parsedData.planDataMod.IPv4 === "1" && ( <p
<p className={cn(
className={cn("text-[9px] bg-purple-600 text-purple-200 dark:bg-purple-800 dark:text-purple-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")} "text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
> )}
IPv4 >
</p> {parsedData.planDataMod.trafficVol}
)} </p>
{parsedData.planDataMod.IPv6 === "1" && ( )}
<p className={cn("text-[9px] bg-pink-600 text-pink-200 dark:bg-pink-800 dark:text-pink-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}> {parsedData.planDataMod.IPv4 === "1" && (
IPv6 <p
</p> className={cn(
)} "text-[9px] bg-purple-600 text-purple-200 dark:bg-purple-800 dark:text-purple-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
{parsedData.planDataMod.networkRoute && ( )}
<p className={cn("text-[9px] bg-blue-600 text-blue-200 dark:bg-blue-800 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}> >
{parsedData.planDataMod.networkRoute.split(",").map((route, index) => { IPv4
return route + (index === parsedData.planDataMod!.networkRoute.split(",").length - 1 ? "" : "") </p>
})} )}
</p> {parsedData.planDataMod.IPv6 === "1" && (
)} <p
{extraList.map((extra, index) => { className={cn(
return ( "text-[9px] bg-pink-600 text-pink-200 dark:bg-pink-800 dark:text-pink-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
<p )}
key={index} >
className={cn("text-[9px] bg-stone-600 text-stone-200 dark:bg-stone-800 dark:text-stone-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")} IPv6
> </p>
{extra} )}
</p> {parsedData.planDataMod.networkRoute && (
) <p
})} className={cn(
</section> "text-[9px] bg-blue-600 text-blue-200 dark:bg-blue-800 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
) )}
>
{networkRoutes.map((route, index) => {
return route + (index === networkRoutes.length - 1 ? "" : "");
})}
</p>
)}
{extraList.map((extra, index) => {
return (
<p
key={index}
className={cn(
"text-[9px] bg-stone-600 text-stone-200 dark:bg-stone-800 dark:text-stone-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
)}
>
{extra}
</p>
);
})}
</section>
);
} }
+24 -12
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({
return ( value,
<Progress className,
aria-label={"Server Usage Bar"} }: {
aria-labelledby={"Server Usage Bar"} value: number;
value={value} className?: string;
indicatorClassName={value < 30 ? "bg-red-500" : value < 70 ? "bg-orange-400" : "bg-green-500"} }) {
className={cn("h-[3px] rounded-sm w-[70px]", className)} return (
/> <Progress
) aria-label={"Server Usage Bar"}
aria-labelledby={"Server Usage Bar"}
value={value}
indicatorClassName={
value < 30
? "bg-red-500"
: value < 70
? "bg-orange-400"
: "bg-green-500"
}
className={cn("h-[3px] rounded-sm w-[70px]", className)}
/>
);
} }
+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>
);
}
+271 -187
View File
@@ -1,196 +1,280 @@
import ServerFlag from "@/components/ServerFlag" import { useTranslation } from "react-i18next";
import ServerUsageBar from "@/components/ServerUsageBar" import { useNavigate } from "react-router-dom";
import { formatBytes } from "@/lib/format" import ServerFlag from "@/components/ServerFlag";
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class" import ServerUsageBar from "@/components/ServerUsageBar";
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils" import { formatBytes } from "@/lib/format";
import { NezhaServer } from "@/types/nezha-api" import {
import { useTranslation } from "react-i18next" GetFontLogoClass,
import { useNavigate } from "react-router-dom" GetOsName,
MageMicrosoftWindows,
} from "@/lib/logo-class";
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils";
import type { NezhaServer } from "@/types/nezha-api";
import BillingInfo from "./billingInfo";
import PlanInfo from "./PlanInfo";
import { Badge } from "./ui/badge";
import { Card } from "./ui/card";
import PlanInfo from "./PlanInfo" export default function ServerCard({
import BillingInfo from "./billingInfo" now,
import { Badge } from "./ui/badge" serverInfo,
import { Card } from "./ui/card" }: {
now: number;
serverInfo: NezhaServer;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const {
name,
country_code,
online,
cpu,
up,
down,
mem,
stg,
net_in_transfer,
net_out_transfer,
public_note,
platform,
} = formatNezhaInfo(now, serverInfo);
export default function ServerCard({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) { const cardClick = () => {
const { t } = useTranslation() sessionStorage.setItem("fromMainPage", "true");
const navigate = useNavigate() navigate(`/server/${serverInfo.id}`);
const { name, country_code, online, cpu, up, down, mem, stg, net_in_transfer, net_out_transfer, public_note, platform } = formatNezhaInfo( };
now,
serverInfo,
)
const cardClick = () => { const showFlag = true;
sessionStorage.setItem("fromMainPage", "true")
navigate(`/server/${serverInfo.id}`)
}
const showFlag = true const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined // @ts-expect-error ShowNetTransfer is a global variable
const showNetTransfer = window.ShowNetTransfer as boolean;
// @ts-expect-error ShowNetTransfer is a global variable // @ts-expect-error FixedTopServerName is a global variable
const showNetTransfer = window.ShowNetTransfer as boolean const fixedTopServerName = window.FixedTopServerName as boolean;
// @ts-expect-error FixedTopServerName is a global variable const parsedData = parsePublicNote(public_note);
const fixedTopServerName = window.FixedTopServerName as boolean
const parsedData = parsePublicNote(public_note) return online ? (
<Card
return online ? ( className={cn(
<Card "flex flex-col items-center justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
className={cn( {
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors", "flex-col": fixedTopServerName,
{ "lg:flex-row": !fixedTopServerName,
"flex-col": fixedTopServerName, },
"lg:flex-row": !fixedTopServerName, {
}, "bg-card/70": customBackgroundImage,
{ },
"bg-card/70": customBackgroundImage, )}
}, onClick={cardClick}
)} >
onClick={cardClick} <section
> className={cn("grid items-center gap-2", {
<section "lg:w-40": !fixedTopServerName,
className={cn("grid items-center gap-2", { })}
"lg:w-40": !fixedTopServerName, style={{ gridTemplateColumns: "auto auto 1fr" }}
})} >
style={{ gridTemplateColumns: "auto auto 1fr" }} <span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
> <div
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span> className={cn(
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}> "flex items-center justify-center",
{showFlag ? <ServerFlag country_code={country_code} /> : null} showFlag ? "min-w-[17px]" : "min-w-0",
</div> )}
<div className="relative flex flex-col"> >
<p className={cn("break-normal font-bold tracking-tight", showFlag ? "text-xs " : "text-sm")}>{name}</p> {showFlag ? <ServerFlag country_code={country_code} /> : null}
<div </div>
className={cn("hidden lg:block", { <div className="relative flex flex-col">
"lg:hidden": fixedTopServerName, <p
})} className={cn(
> "break-normal font-bold tracking-tight",
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />} showFlag ? "text-xs " : "text-sm",
</div> )}
</div> >
</section> {name}
<div </p>
className={cn("flex items-center gap-2 -mt-2 lg:hidden", { <div
"lg:flex": fixedTopServerName, className={cn("hidden lg:block", {
})} "lg:hidden": fixedTopServerName,
> })}
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />} >
</div> {parsedData?.billingDataMod && (
<div className="flex flex-col lg:items-start items-center gap-2"> <BillingInfo parsedData={parsedData} />
<section )}
className={cn("grid grid-cols-5 items-center gap-3", { </div>
"lg:grid-cols-6 lg:gap-4": fixedTopServerName, </div>
})} </section>
> <div
{fixedTopServerName && ( className={cn("flex items-center gap-2 -mt-2 lg:hidden", {
<div className={"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"}> "lg:flex": fixedTopServerName,
<div className="text-xs font-semibold"> })}
{platform.includes("Windows") ? ( >
<MageMicrosoftWindows className="size-[10px]" /> {parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
) : ( </div>
<p className={`fl-${GetFontLogoClass(platform)}`} /> <div className="flex flex-col lg:items-start items-center gap-2">
)} <section
</div> className={cn("grid grid-cols-5 items-center gap-3", {
<div className={"flex w-14 flex-col"}> "lg:grid-cols-6 lg:gap-4": fixedTopServerName,
<p className="text-xs text-muted-foreground">{t("serverCard.system")}</p> })}
<div className="flex items-center text-[10.5px] font-semibold">{platform.includes("Windows") ? "Windows" : GetOsName(platform)}</div> >
</div> {fixedTopServerName && (
</div> <div
)} className={
<div className={"flex w-14 flex-col"}> "hidden col-span-1 items-center lg:flex lg:flex-row gap-2"
<p className="text-xs text-muted-foreground">{"CPU"}</p> }
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div> >
<ServerUsageBar value={cpu} /> <div className="text-xs font-semibold">
</div> {platform.includes("Windows") ? (
<div className={"flex w-14 flex-col"}> <MageMicrosoftWindows className="size-[10px]" />
<p className="text-xs text-muted-foreground">{t("serverCard.mem")}</p> ) : (
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div> <p className={`fl-${GetFontLogoClass(platform)}`} />
<ServerUsageBar value={mem} /> )}
</div> </div>
<div className={"flex w-14 flex-col"}> <div className={"flex w-14 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p> <p className="text-xs text-muted-foreground">
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div> {t("serverCard.system")}
<ServerUsageBar value={stg} /> </p>
</div> <div className="flex items-center text-[10.5px] font-semibold">
<div className={"flex w-14 flex-col"}> {platform.includes("Windows")
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p> ? "Windows"
<div className="flex items-center text-xs font-semibold"> : GetOsName(platform)}
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : up >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`} </div>
</div> </div>
</div> </div>
<div className={"flex w-14 flex-col"}> )}
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p> <div className={"flex w-14 flex-col"}>
<div className="flex items-center text-xs font-semibold"> <p className="text-xs text-muted-foreground">{"CPU"}</p>
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : down >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`} <div className="flex items-center text-xs font-semibold">
</div> {cpu.toFixed(2)}%
</div> </div>
</section> <ServerUsageBar value={cpu} />
{showNetTransfer && ( </div>
<section className={"flex items-center w-full justify-between gap-1"}> <div className={"flex w-14 flex-col"}>
<Badge <p className="text-xs text-muted-foreground">
variant="secondary" {t("serverCard.mem")}
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] border-muted-50 shadow-md shadow-neutral-200/30 dark:shadow-none" </p>
> <div className="flex items-center text-xs font-semibold">
{t("serverCard.upload")}:{formatBytes(net_out_transfer)} {mem.toFixed(2)}%
</Badge> </div>
<Badge <ServerUsageBar value={mem} />
variant="outline" </div>
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] shadow-md shadow-neutral-200/30 dark:shadow-none" <div className={"flex w-14 flex-col"}>
> <p className="text-xs text-muted-foreground">
{t("serverCard.download")}:{formatBytes(net_in_transfer)} {t("serverCard.stg")}
</Badge> </p>
</section> <div className="flex items-center text-xs font-semibold">
)} {stg.toFixed(2)}%
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />} </div>
</div> <ServerUsageBar value={stg} />
</Card> </div>
) : ( <div className={"flex w-14 flex-col"}>
<Card <p className="text-xs text-muted-foreground">
className={cn( {t("serverCard.upload")}
"flex flex-col items-center justify-start gap-3 sm:gap-0 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors", </p>
showNetTransfer ? "lg:min-h-[91px] min-h-[123px]" : "lg:min-h-[61px] min-h-[93px]", <div className="flex items-center text-xs font-semibold">
{ {up >= 1024
"flex-col": fixedTopServerName, ? `${(up / 1024).toFixed(2)}G/s`
"lg:flex-row": !fixedTopServerName, : up >= 1
}, ? `${up.toFixed(2)}M/s`
{ : `${(up * 1024).toFixed(2)}K/s`}
"bg-card/70": customBackgroundImage, </div>
}, </div>
)} <div className={"flex w-14 flex-col"}>
onClick={cardClick} <p className="text-xs text-muted-foreground">
> {t("serverCard.download")}
<section </p>
className={cn("grid items-center gap-2", { <div className="flex items-center text-xs font-semibold">
"lg:w-40": !fixedTopServerName, {down >= 1024
})} ? `${(down / 1024).toFixed(2)}G/s`
style={{ gridTemplateColumns: "auto auto 1fr" }} : down >= 1
> ? `${down.toFixed(2)}M/s`
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span> : `${(down * 1024).toFixed(2)}K/s`}
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}> </div>
{showFlag ? <ServerFlag country_code={country_code} /> : null} </div>
</div> </section>
<div className="relative flex flex-col"> {showNetTransfer && (
<p className={cn("break-normal font-bold tracking-tight max-w-[108px]", showFlag ? "text-xs" : "text-sm")}>{name}</p> <section className={"flex items-center w-full justify-between gap-1"}>
<div <Badge
className={cn("hidden lg:block", { variant="secondary"
"lg:hidden": fixedTopServerName, className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] border-muted-50 shadow-md shadow-neutral-200/30 dark:shadow-none"
})} >
> {t("serverCard.upload")}:{formatBytes(net_out_transfer)}
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />} </Badge>
</div> <Badge
</div> variant="outline"
</section> className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] shadow-md shadow-neutral-200/30 dark:shadow-none"
<div >
className={cn("flex items-center gap-2 lg:hidden", { {t("serverCard.download")}:{formatBytes(net_in_transfer)}
"lg:flex": fixedTopServerName, </Badge>
})} </section>
> )}
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />} {parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</div> </div>
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />} </Card>
</Card> ) : (
) <Card
className={cn(
"flex flex-col items-center justify-start gap-3 sm:gap-0 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
showNetTransfer
? "lg:min-h-[91px] min-h-[123px]"
: "lg:min-h-[61px] min-h-[93px]",
{
"flex-col": fixedTopServerName,
"lg:flex-row": !fixedTopServerName,
},
{
"bg-card/70": customBackgroundImage,
},
)}
onClick={cardClick}
>
<section
className={cn("grid items-center gap-2", {
"lg:w-40": !fixedTopServerName,
})}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
<div
className={cn(
"flex items-center justify-center",
showFlag ? "min-w-[17px]" : "min-w-0",
)}
>
{showFlag ? <ServerFlag country_code={country_code} /> : null}
</div>
<div className="relative flex flex-col">
<p
className={cn(
"break-normal font-bold tracking-tight max-w-[108px]",
showFlag ? "text-xs" : "text-sm",
)}
>
{name}
</p>
<div
className={cn("hidden lg:block", {
"lg:hidden": fixedTopServerName,
})}
>
{parsedData?.billingDataMod && (
<BillingInfo parsedData={parsedData} />
)}
</div>
</div>
</section>
<div
className={cn("flex items-center gap-2 lg:hidden", {
"lg:flex": fixedTopServerName,
})}
>
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
</div>
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</Card>
);
} }
+233 -136
View File
@@ -1,143 +1,240 @@
import ServerFlag from "@/components/ServerFlag" import { useTranslation } from "react-i18next";
import ServerUsageBar from "@/components/ServerUsageBar" import { useNavigate } from "react-router-dom";
import { formatBytes } from "@/lib/format" import ServerFlag from "@/components/ServerFlag";
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class" import ServerUsageBar from "@/components/ServerUsageBar";
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils" import { formatBytes } from "@/lib/format";
import { NezhaServer } from "@/types/nezha-api" import {
import { useTranslation } from "react-i18next" GetFontLogoClass,
import { useNavigate } from "react-router-dom" GetOsName,
MageMicrosoftWindows,
} from "@/lib/logo-class";
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils";
import type { NezhaServer } from "@/types/nezha-api";
import BillingInfo from "./billingInfo";
import PlanInfo from "./PlanInfo";
import { Card } from "./ui/card";
import { Separator } from "./ui/separator";
import PlanInfo from "./PlanInfo" export default function ServerCardInline({
import BillingInfo from "./billingInfo" now,
import { Card } from "./ui/card" serverInfo,
import { Separator } from "./ui/separator" }: {
now: number;
serverInfo: NezhaServer;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const {
name,
country_code,
online,
cpu,
up,
down,
mem,
stg,
platform,
uptime,
net_in_transfer,
net_out_transfer,
public_note,
} = formatNezhaInfo(now, serverInfo);
export default function ServerCardInline({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) { const cardClick = () => {
const { t } = useTranslation() sessionStorage.setItem("fromMainPage", "true");
const navigate = useNavigate() navigate(`/server/${serverInfo.id}`);
const { name, country_code, online, cpu, up, down, mem, stg, platform, uptime, net_in_transfer, net_out_transfer, public_note } = formatNezhaInfo( };
now,
serverInfo,
)
const cardClick = () => { const showFlag = true;
sessionStorage.setItem("fromMainPage", "true")
navigate(`/server/${serverInfo.id}`)
}
const showFlag = true const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const parsedData = parsePublicNote(public_note);
const parsedData = parsePublicNote(public_note) return online ? (
<section>
return online ? ( <Card
<section> className={cn(
<Card "flex items-center lg:flex-row justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors min-w-[900px] w-full",
className={cn( {
"flex items-center lg:flex-row justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors min-w-[900px] w-full", "bg-card/70": customBackgroundImage,
{ },
"bg-card/70": customBackgroundImage, )}
}, onClick={cardClick}
)} >
onClick={cardClick} <section
> className={cn("grid items-center gap-2 lg:w-36")}
<section className={cn("grid items-center gap-2 lg:w-36")} style={{ gridTemplateColumns: "auto auto 1fr" }}> style={{ gridTemplateColumns: "auto auto 1fr" }}
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span> >
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}> <span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
{showFlag ? <ServerFlag country_code={country_code} /> : null} <div
</div> className={cn(
<div className="relative w-28 flex flex-col"> "flex items-center justify-center",
<p className={cn("break-normal font-bold tracking-tight", showFlag ? "text-xs " : "text-sm")}>{name}</p> showFlag ? "min-w-[17px]" : "min-w-0",
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />} )}
</div> >
</section> {showFlag ? <ServerFlag country_code={country_code} /> : null}
<Separator orientation="vertical" className="h-8 mx-0 ml-2" /> </div>
<div className="flex flex-col gap-1"> <div className="relative w-28 flex flex-col">
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}> <p
<div className={"items-center flex flex-row gap-2 whitespace-nowrap"}> className={cn(
<div className="text-xs font-semibold"> "break-normal font-bold tracking-tight",
{platform.includes("Windows") ? ( showFlag ? "text-xs " : "text-sm",
<MageMicrosoftWindows className="size-[10px]" /> )}
) : ( >
<p className={`fl-${GetFontLogoClass(platform)}`} /> {name}
)} </p>
</div> {parsedData?.billingDataMod && (
<div className={"flex w-14 flex-col"}> <BillingInfo parsedData={parsedData} />
<p className="text-xs text-muted-foreground">{t("serverCard.system")}</p> )}
<div className="flex items-center text-[10.5px] font-semibold">{platform.includes("Windows") ? "Windows" : GetOsName(platform)}</div> </div>
</div> </section>
</div> <Separator orientation="vertical" className="h-8 mx-0 ml-2" />
<div className={"flex w-20 flex-col"}> <div className="flex flex-col gap-1">
<p className="text-xs text-muted-foreground">{t("serverCard.uptime")}</p> <section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}>
<div className="flex items-center text-xs font-semibold"> <div
{uptime / 86400 >= 1 className={"items-center flex flex-row gap-2 whitespace-nowrap"}
? `${(uptime / 86400).toFixed(0)} ${t("serverCard.days")}` >
: `${(uptime / 3600).toFixed(0)} ${t("serverCard.hours")}`} <div className="text-xs font-semibold">
</div> {platform.includes("Windows") ? (
</div> <MageMicrosoftWindows className="size-[10px]" />
<div className={"flex w-14 flex-col"}> ) : (
<p className="text-xs text-muted-foreground">{"CPU"}</p> <p className={`fl-${GetFontLogoClass(platform)}`} />
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div> )}
<ServerUsageBar value={cpu} /> </div>
</div> <div className={"flex w-14 flex-col"}>
<div className={"flex w-14 flex-col"}> <p className="text-xs text-muted-foreground">
<p className="text-xs text-muted-foreground">{t("serverCard.mem")}</p> {t("serverCard.system")}
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div> </p>
<ServerUsageBar value={mem} /> <div className="flex items-center text-[10.5px] font-semibold">
</div> {platform.includes("Windows")
<div className={"flex w-14 flex-col"}> ? "Windows"
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p> : GetOsName(platform)}
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div> </div>
<ServerUsageBar value={stg} /> </div>
</div> </div>
<div className={"flex w-16 flex-col"}> <div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p> <p className="text-xs text-muted-foreground">
<div className="flex items-center text-xs font-semibold"> {t("serverCard.uptime")}
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : up >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`} </p>
</div> <div className="flex items-center text-xs font-semibold">
</div> {uptime / 86400 >= 1
<div className={"flex w-16 flex-col"}> ? `${(uptime / 86400).toFixed(0)} ${t("serverCard.days")}`
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p> : `${(uptime / 3600).toFixed(0)} ${t("serverCard.hours")}`}
<div className="flex items-center text-xs font-semibold"> </div>
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : down >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`} </div>
</div> <div className={"flex w-14 flex-col"}>
</div> <p className="text-xs text-muted-foreground">{"CPU"}</p>
<div className={"flex w-20 flex-col"}> <div className="flex items-center text-xs font-semibold">
<p className="text-xs text-muted-foreground">{t("serverCard.totalUpload")}</p> {cpu.toFixed(2)}%
<div className="flex items-center text-xs font-semibold">{formatBytes(net_out_transfer)}</div> </div>
</div> <ServerUsageBar value={cpu} />
<div className={"flex w-20 flex-col"}> </div>
<p className="text-xs text-muted-foreground">{t("serverCard.totalDownload")}</p> <div className={"flex w-14 flex-col"}>
<div className="flex items-center text-xs font-semibold">{formatBytes(net_in_transfer)}</div> <p className="text-xs text-muted-foreground">
</div> {t("serverCard.mem")}
</section> </p>
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />} <div className="flex items-center text-xs font-semibold">
</div> {mem.toFixed(2)}%
</Card> </div>
</section> <ServerUsageBar value={mem} />
) : ( </div>
<Card <div className={"flex w-14 flex-col"}>
className={cn( <p className="text-xs text-muted-foreground">
"flex min-h-[61px] min-w-[900px] items-center justify-start p-3 md:px-5 flex-row cursor-pointer hover:bg-accent/50 transition-colors", {t("serverCard.stg")}
{ </p>
"bg-card/70": customBackgroundImage, <div className="flex items-center text-xs font-semibold">
}, {stg.toFixed(2)}%
)} </div>
onClick={cardClick} <ServerUsageBar value={stg} />
> </div>
<section className={cn("grid items-center gap-2 w-40")} style={{ gridTemplateColumns: "auto auto 1fr" }}> <div className={"flex w-16 flex-col"}>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span> <p className="text-xs text-muted-foreground">
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}> {t("serverCard.upload")}
{showFlag ? <ServerFlag country_code={country_code} /> : null} </p>
</div> <div className="flex items-center text-xs font-semibold">
<div className="relative flex flex-col"> {up >= 1024
<p className={cn("break-normal font-bold w-28 tracking-tight", showFlag ? "text-xs" : "text-sm")}>{name}</p> ? `${(up / 1024).toFixed(2)}G/s`
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />} : up >= 1
</div> ? `${up.toFixed(2)}M/s`
</section> : `${(up * 1024).toFixed(2)}K/s`}
<Separator orientation="vertical" className="h-8 ml-3 lg:ml-1 mr-3" /> </div>
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />} </div>
</Card> <div className={"flex w-16 flex-col"}>
) <p className="text-xs text-muted-foreground">
{t("serverCard.download")}
</p>
<div className="flex items-center text-xs font-semibold">
{down >= 1024
? `${(down / 1024).toFixed(2)}G/s`
: down >= 1
? `${down.toFixed(2)}M/s`
: `${(down * 1024).toFixed(2)}K/s`}
</div>
</div>
<div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.totalUpload")}
</p>
<div className="flex items-center text-xs font-semibold">
{formatBytes(net_out_transfer)}
</div>
</div>
<div className={"flex w-20 flex-col"}>
<p className="text-xs text-muted-foreground">
{t("serverCard.totalDownload")}
</p>
<div className="flex items-center text-xs font-semibold">
{formatBytes(net_in_transfer)}
</div>
</div>
</section>
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</div>
</Card>
</section>
) : (
<Card
className={cn(
"flex min-h-[61px] min-w-[900px] items-center justify-start p-3 md:px-5 flex-row cursor-pointer hover:bg-accent/50 transition-colors",
{
"bg-card/70": customBackgroundImage,
},
)}
onClick={cardClick}
>
<section
className={cn("grid items-center gap-2 w-40")}
style={{ gridTemplateColumns: "auto auto 1fr" }}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
<div
className={cn(
"flex items-center justify-center",
showFlag ? "min-w-[17px]" : "min-w-0",
)}
>
{showFlag ? <ServerFlag country_code={country_code} /> : null}
</div>
<div className="relative flex flex-col">
<p
className={cn(
"break-normal font-bold w-28 tracking-tight",
showFlag ? "text-xs" : "text-sm",
)}
>
{name}
</p>
{parsedData?.billingDataMod && (
<BillingInfo parsedData={parsedData} />
)}
</div>
</section>
<Separator orientation="vertical" className="h-8 ml-3 lg:ml-1 mr-3" />
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
</Card>
);
} }
File diff suppressed because it is too large Load Diff
+357 -290
View File
@@ -1,310 +1,377 @@
import { BackIcon } from "@/components/Icon" import countries from "i18n-iso-countries";
import ServerFlag from "@/components/ServerFlag" import enLocale from "i18n-iso-countries/langs/en.json";
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading" import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge" import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card" import { useNavigate } from "react-router-dom";
import { useWebSocketContext } from "@/hooks/use-websocket-context" import { BackIcon } from "@/components/Icon";
import { formatBytes } from "@/lib/format" import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading";
import { cn, formatNezhaInfo } from "@/lib/utils" import ServerFlag from "@/components/ServerFlag";
import { NezhaWebsocketResponse } from "@/types/nezha-api" import { Badge } from "@/components/ui/badge";
import countries from "i18n-iso-countries" import { Card, CardContent } from "@/components/ui/card";
import enLocale from "i18n-iso-countries/langs/en.json" import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { useEffect, useState } from "react" import { formatBytes } from "@/lib/format";
import { useTranslation } from "react-i18next" import { cn, formatNezhaInfo } from "@/lib/utils";
import { useNavigate } from "react-router-dom" import type { NezhaWebsocketResponse } from "@/types/nezha-api";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion" import {
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip" Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "./ui/accordion";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./ui/tooltip";
export default function ServerDetailOverview({ server_id }: { server_id: string }) { export default function ServerDetailOverview({
const { t } = useTranslation() server_id,
const navigate = useNavigate() }: {
server_id: string;
}) {
const { t } = useTranslation();
const navigate = useNavigate();
const [hasHistory, setHasHistory] = useState(false) const [hasHistory, setHasHistory] = useState(false);
useEffect(() => { useEffect(() => {
const previousPath = sessionStorage.getItem("fromMainPage") const previousPath = sessionStorage.getItem("fromMainPage");
if (previousPath) { if (previousPath) {
setHasHistory(true) setHasHistory(true);
} }
}, []) }, []);
const { lastMessage, connected } = useWebSocketContext() const { lastMessage, connected } = useWebSocketContext();
if (!connected && !lastMessage) { if (!connected && !lastMessage) {
return <ServerDetailLoading /> return <ServerDetailLoading />;
} }
const linkClick = () => { const linkClick = () => {
if (hasHistory) { if (hasHistory) {
navigate(-1) navigate(-1);
} else { } else {
navigate("/") navigate("/");
} }
} };
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null const nezhaWsData = lastMessage
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
: null;
if (!nezhaWsData) { if (!nezhaWsData) {
return <ServerDetailLoading /> return <ServerDetailLoading />;
} }
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id)) const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
if (!server) { if (!server) {
return <ServerDetailLoading /> return <ServerDetailLoading />;
} }
const { const {
name, name,
online, online,
uptime, uptime,
version, version,
arch, arch,
mem_total, mem_total,
disk_total, disk_total,
country_code, country_code,
platform, platform,
platform_version, platform_version,
cpu_info, cpu_info,
gpu_info, gpu_info,
load_1, load_1,
load_5, load_5,
load_15, load_15,
net_out_transfer, net_out_transfer,
net_in_transfer, net_in_transfer,
last_active_time_string, last_active_time_string,
boot_time_string, boot_time_string,
} = formatNezhaInfo(nezhaWsData.now, server) } = formatNezhaInfo(nezhaWsData.now, server);
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
countries.registerLocale(enLocale) countries.registerLocale(enLocale);
return ( return (
<div <div
className={cn({ className={cn({
"bg-card/70 p-4 rounded-[10px]": customBackgroundImage, "bg-card/70 p-4 rounded-[10px]": customBackgroundImage,
})} })}
> >
<div <div
onClick={linkClick} onClick={linkClick}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-1 text-xl server-name" className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-1 text-xl server-name"
> >
<BackIcon /> <BackIcon />
{name} {name}
</div> </div>
<section className="flex flex-wrap gap-2 mt-3"> <section className="flex flex-wrap gap-2 mt-3">
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.status")}</p> <p className="text-xs text-muted-foreground">
<Badge {t("serverDetail.status")}
className={cn("text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white", { </p>
" bg-green-800": online, <Badge
" bg-red-600": !online, className={cn(
})} "text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white",
> {
{online ? t("serverDetail.online") : t("serverDetail.offline")} " bg-green-800": online,
</Badge> " bg-red-600": !online,
</section> },
</CardContent> )}
</Card> >
{online && ( {online ? t("serverDetail.online") : t("serverDetail.offline")}
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> </Badge>
<CardContent className="px-1.5 py-1"> </section>
<section className="flex flex-col items-start gap-0.5"> </CardContent>
<p className="text-xs text-muted-foreground">{t("serverDetail.uptime")}</p> </Card>
<div className="text-xs"> {online && (
{" "} <Card className="rounded-[10px] bg-transparent border-none shadow-none">
{uptime / 86400 >= 1 <CardContent className="px-1.5 py-1">
? `${Math.floor(uptime / 86400)} ${t("serverDetail.days")} ${Math.floor((uptime % 86400) / 3600)} ${t("serverDetail.hours")}` <section className="flex flex-col items-start gap-0.5">
: `${Math.floor(uptime / 3600)} ${t("serverDetail.hours")}`} <p className="text-xs text-muted-foreground">
</div> {t("serverDetail.uptime")}
</section> </p>
</CardContent> <div className="text-xs">
</Card> {" "}
)} {uptime / 86400 >= 1
{version && ( ? `${Math.floor(uptime / 86400)} ${t("serverDetail.days")} ${Math.floor((uptime % 86400) / 3600)} ${t("serverDetail.hours")}`
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> : `${Math.floor(uptime / 3600)} ${t("serverDetail.hours")}`}
<CardContent className="px-1.5 py-1"> </div>
<section className="flex flex-col items-start gap-0.5"> </section>
<p className="text-xs text-muted-foreground">{t("serverDetail.version")}</p> </CardContent>
<div className="text-xs">{version} </div> </Card>
</section> )}
</CardContent> {version && (
</Card> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
)} <CardContent className="px-1.5 py-1">
{arch && ( <section className="flex flex-col items-start gap-0.5">
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <p className="text-xs text-muted-foreground">
<CardContent className="px-1.5 py-1"> {t("serverDetail.version")}
<section className="flex flex-col items-start gap-0.5"> </p>
<p className="text-xs text-muted-foreground">{t("serverDetail.arch")}</p> <div className="text-xs">{version} </div>
<div className="text-xs">{arch} </div> </section>
</section> </CardContent>
</CardContent> </Card>
</Card> )}
)} {arch && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">
{t("serverDetail.arch")}
</p>
<div className="text-xs">{arch} </div>
</section>
</CardContent>
</Card>
)}
{mem_total ? ( {mem_total ? (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.mem")}</p> <p className="text-xs text-muted-foreground">
<div className="text-xs">{formatBytes(mem_total)}</div> {t("serverDetail.mem")}
</section> </p>
</CardContent> <div className="text-xs">{formatBytes(mem_total)}</div>
</Card> </section>
) : null} </CardContent>
</Card>
) : null}
{disk_total ? ( {disk_total ? (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.disk")}</p> <p className="text-xs text-muted-foreground">
<div className="text-xs">{formatBytes(disk_total)}</div> {t("serverDetail.disk")}
</section> </p>
</CardContent> <div className="text-xs">{formatBytes(disk_total)}</div>
</Card> </section>
) : null} </CardContent>
</Card>
) : null}
{country_code && ( {country_code && (
<TooltipProvider delayDuration={100}> <TooltipProvider delayDuration={100}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.region")}</p> <p className="text-xs text-muted-foreground">
<section className="flex items-start gap-1"> {t("serverDetail.region")}
<div className="text-xs text-start">{country_code?.toUpperCase()}</div> </p>
{country_code && <ServerFlag className="text-[11px] -mt-[1px]" country_code={country_code} />} <section className="flex items-start gap-1">
</section> <div className="text-xs text-start">
</section> {country_code?.toUpperCase()}
</CardContent> </div>
</Card> {country_code && (
</TooltipTrigger> <ServerFlag
<TooltipContent> className="text-[11px] -mt-px"
<p>{countries.getName(country_code?.toUpperCase(), "en")}</p> country_code={country_code}
</TooltipContent> />
</Tooltip> )}
</TooltipProvider> </section>
)} </section>
</section> </CardContent>
<section className="flex flex-wrap gap-2 mt-1"> </Card>
{platform && ( </TooltipTrigger>
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <TooltipContent>
<CardContent className="px-1.5 py-1"> <p>{countries.getName(country_code?.toUpperCase(), "en")}</p>
<section className="flex flex-col items-start gap-0.5"> </TooltipContent>
<p className="text-xs text-muted-foreground">{t("serverDetail.system")}</p> </Tooltip>
<div className="text-xs"> </TooltipProvider>
{" "} )}
{platform} {platform_version ? " - " + platform_version : ""} </section>
</div> <section className="flex flex-wrap gap-2 mt-1">
</section> {platform && (
</CardContent> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
</Card> <CardContent className="px-1.5 py-1">
)} <section className="flex flex-col items-start gap-0.5">
{cpu_info.length > 0 && ( <p className="text-xs text-muted-foreground">
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> {t("serverDetail.system")}
<CardContent className="px-1.5 py-1"> </p>
<section className="flex flex-col items-start gap-0.5"> <div className="text-xs">
<p className="text-xs text-muted-foreground">{"CPU"}</p> {" "}
<div className="text-xs"> {cpu_info.join(", ")}</div> {platform} {platform_version ? ` - ${platform_version}` : ""}
</section> </div>
</CardContent> </section>
</Card> </CardContent>
)} </Card>
{gpu_info.length > 0 && ( )}
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> {cpu_info.length > 0 && (
<CardContent className="px-1.5 py-1"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<section className="flex flex-col items-start gap-0.5"> <CardContent className="px-1.5 py-1">
<p className="text-xs text-muted-foreground">{"GPU"}</p> <section className="flex flex-col items-start gap-0.5">
<div className="text-xs">{gpu_info.join(", ")}</div> <p className="text-xs text-muted-foreground">{"CPU"}</p>
</section> <div className="text-xs"> {cpu_info.join(", ")}</div>
</CardContent> </section>
</Card> </CardContent>
)} </Card>
</section> )}
<section className="flex flex-wrap gap-2 mt-1"> {gpu_info.length > 0 && (
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{"Load"}</p> <p className="text-xs text-muted-foreground">{"GPU"}</p>
<div className="text-xs"> <div className="text-xs">{gpu_info.join(", ")}</div>
{load_1} / {load_5} / {load_15} </section>
</div> </CardContent>
</section> </Card>
</CardContent> )}
</Card> </section>
{net_out_transfer ? ( <section className="flex flex-wrap gap-2 mt-1">
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.upload")}</p> <p className="text-xs text-muted-foreground">{"Load"}</p>
{net_out_transfer ? ( <div className="text-xs">
<div className="text-xs"> {formatBytes(net_out_transfer)} </div> {load_1} / {load_5} / {load_15}
) : ( </div>
<div className="text-xs"> {t("serverDetail.unknown")}</div> </section>
)} </CardContent>
</section> </Card>
</CardContent> {net_out_transfer ? (
</Card> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
) : null} <CardContent className="px-1.5 py-1">
{net_in_transfer ? ( <section className="flex flex-col items-start gap-0.5">
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <p className="text-xs text-muted-foreground">
<CardContent className="px-1.5 py-1"> {t("serverDetail.upload")}
<section className="flex flex-col items-start gap-0.5"> </p>
<p className="text-xs text-muted-foreground">{t("serverDetail.download")}</p> {net_out_transfer ? (
{net_in_transfer ? ( <div className="text-xs">
<div className="text-xs"> {formatBytes(net_in_transfer)} </div> {" "}
) : ( {formatBytes(net_out_transfer)}{" "}
<div className="text-xs"> {t("serverDetail.unknown")}</div> </div>
)} ) : (
</section> <div className="text-xs"> {t("serverDetail.unknown")}</div>
</CardContent> )}
</Card> </section>
) : null} </CardContent>
</section> </Card>
<section className="flex flex-wrap gap-2 mt-1"> ) : null}
{server?.state.temperatures && server?.state.temperatures.length > 0 && ( {net_in_transfer ? (
<section className="flex flex-wrap gap-2 ml-1.5"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<Accordion type="single" collapsible className="w-fit"> <CardContent className="px-1.5 py-1">
<AccordionItem value="item-1" className="border-none"> <section className="flex flex-col items-start gap-0.5">
<AccordionTrigger className="text-xs py-0 text-muted-foreground font-normal">{t("serverDetail.temperature")}</AccordionTrigger> <p className="text-xs text-muted-foreground">
<AccordionContent className="pb-0"> {t("serverDetail.download")}
<section className="flex items-start flex-wrap gap-2"> </p>
{server?.state.temperatures.map((item, index) => ( {net_in_transfer ? (
<div className="text-xs flex items-center" key={index}> <div className="text-xs">
<p className="font-semibold">{item.Name}</p>: {item.Temperature.toFixed(2)} °C {" "}
</div> {formatBytes(net_in_transfer)}{" "}
))} </div>
</section> ) : (
</AccordionContent> <div className="text-xs"> {t("serverDetail.unknown")}</div>
</AccordionItem> )}
</Accordion> </section>
</section> </CardContent>
)} </Card>
</section> ) : null}
</section>
<section className="flex flex-wrap gap-2 mt-1">
{server?.state.temperatures &&
server?.state.temperatures.length > 0 && (
<section className="flex flex-wrap gap-2 ml-1.5">
<Accordion type="single" collapsible className="w-fit">
<AccordionItem value="item-1" className="border-none">
<AccordionTrigger className="text-xs py-0 text-muted-foreground font-normal">
{t("serverDetail.temperature")}
</AccordionTrigger>
<AccordionContent className="pb-0">
<section className="flex items-start flex-wrap gap-2">
{server?.state.temperatures.map((item, index) => (
<div className="text-xs flex items-center" key={index}>
<p className="font-semibold">{item.Name}</p>:{" "}
{item.Temperature.toFixed(2)} °C
</div>
))}
</section>
</AccordionContent>
</AccordionItem>
</Accordion>
</section>
)}
</section>
<section className="flex flex-wrap gap-2 mt-1"> <section className="flex flex-wrap gap-2 mt-1">
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
<CardContent className="px-1.5 py-1"> <CardContent className="px-1.5 py-1">
<section className="flex flex-col items-start gap-0.5"> <section className="flex flex-col items-start gap-0.5">
<p className="text-xs text-muted-foreground">{t("serverDetail.bootTime")}</p> <p className="text-xs text-muted-foreground">
<div className="text-xs">{boot_time_string ? boot_time_string : "N/A"}</div> {t("serverDetail.bootTime")}
</section> </p>
</CardContent> <div className="text-xs">
</Card> {boot_time_string ? boot_time_string : "N/A"}
<Card className="rounded-[10px] bg-transparent border-none shadow-none"> </div>
<CardContent className="px-1.5 py-1"> </section>
<section className="flex flex-col items-start gap-0.5"> </CardContent>
<p className="text-xs text-muted-foreground">{t("serverDetail.lastActive")}</p> </Card>
<div className="text-xs">{last_active_time_string ? last_active_time_string : "N/A"}</div> <Card className="rounded-[10px] bg-transparent border-none shadow-none">
</section> <CardContent className="px-1.5 py-1">
</CardContent> <section className="flex flex-col items-start gap-0.5">
</Card> <p className="text-xs text-muted-foreground">
</section> {t("serverDetail.lastActive")}
</div> </p>
) <div className="text-xs">
{last_active_time_string ? last_active_time_string : "N/A"}
</div>
</section>
</CardContent>
</Card>
</section>
</div>
);
} }
+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"}
/>
);
}
+43 -33
View File
@@ -1,42 +1,52 @@
import { cn } from "@/lib/utils" import getUnicodeFlagIcon from "country-flag-icons/unicode";
import getUnicodeFlagIcon from "country-flag-icons/unicode" import { useEffect, useState } from "react";
import { useEffect, useState } from "react" import { cn } from "@/lib/utils";
export default function ServerFlag({ country_code, className }: { country_code: string; className?: string }) { export default function ServerFlag({
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false) country_code,
className,
}: {
country_code: string;
className?: string;
}) {
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false);
// @ts-expect-error ForceUseSvgFlag is a global variable // @ts-expect-error ForceUseSvgFlag is a global variable
const forceUseSvgFlag = window.ForceUseSvgFlag as boolean const forceUseSvgFlag = window.ForceUseSvgFlag as boolean;
useEffect(() => { useEffect(() => {
if (forceUseSvgFlag) { if (forceUseSvgFlag) {
// 如果环境变量要求直接使用 SVG,则无需检查 Emoji 支持 // 如果环境变量要求直接使用 SVG,则无需检查 Emoji 支持
setSupportsEmojiFlags(false) setSupportsEmojiFlags(false);
return return;
} }
const checkEmojiSupport = () => { const checkEmojiSupport = () => {
const canvas = document.createElement("canvas") const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d") const ctx = canvas.getContext("2d");
const emojiFlag = "🇺🇸" // 使用美国国旗作为测试 const emojiFlag = "🇺🇸"; // 使用美国国旗作为测试
if (!ctx) return if (!ctx) return;
ctx.fillStyle = "#000" ctx.fillStyle = "#000";
ctx.textBaseline = "top" ctx.textBaseline = "top";
ctx.font = "32px Arial" ctx.font = "32px Arial";
ctx.fillText(emojiFlag, 0, 0) ctx.fillText(emojiFlag, 0, 0);
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0 const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0;
setSupportsEmojiFlags(support) setSupportsEmojiFlags(support);
} };
checkEmojiSupport() checkEmojiSupport();
}, []) }, [forceUseSvgFlag]);
if (!country_code) return null if (!country_code) return null;
return ( return (
<span className={cn("text-[12px] text-muted-foreground", className)}> <span className={cn("text-[12px] text-muted-foreground", className)}>
{forceUseSvgFlag || !supportsEmojiFlags ? <span className={`fi fi-${country_code}`} /> : getUnicodeFlagIcon(country_code)} {forceUseSvgFlag || !supportsEmojiFlags ? (
</span> <span className={`fi fi-${country_code}`} />
) ) : (
getUnicodeFlagIcon(country_code)
)}
</span>
);
} }
+199 -149
View File
@@ -1,158 +1,208 @@
// src/components/ServerOverview.tsx (最终完整版) import {
ArrowDownCircleIcon,
ArrowUpCircleIcon,
} from "@heroicons/react/20/solid";
import { useTranslation } from "react-i18next";
import { Card, CardContent } from "@/components/ui/card";
import { useStatus } from "@/hooks/use-status";
import { formatBytes } from "@/lib/format";
import { cn } from "@/lib/utils";
import { Globe } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card"
import { useStatus } from "@/hooks/use-status"
import { formatBytes} from "@/lib/format"
import { cn } from "@/lib/utils"
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
import { useTranslation } from "react-i18next"
import { Globe } from "lucide-react"
// 扩展 props 类型,以接收域名总数和新的交互逻辑
type ServerOverviewProps = { type ServerOverviewProps = {
online: number online: number;
offline: number offline: number;
total: number total: number;
up: number up: number;
down: number down: number;
upSpeed: number upSpeed: number;
downSpeed: number downSpeed: number;
totalDomains: number // 新增:接收域名总数 totalDomains: number; // 新增:接收域名总数
onViewChange: (view: 'servers' | 'domains') => void // 新增:点击事件回调 onViewChange: (view: 'servers' | 'domains') => void; // 新增:点击事件回调
activeView: 'servers' | 'domains' // 新增:当前激活的视图 activeView: 'servers' | 'domains'; // 新增:当前激活的视图
} };
export default function ServerOverview({ export default function ServerOverview({
online, online,
offline, offline,
total, total,
up, up,
down, down,
upSpeed, upSpeed,
downSpeed, downSpeed,
totalDomains, totalDomains,
onViewChange, onViewChange,
activeView, activeView,
}: ServerOverviewProps) { }: ServerOverviewProps) {
const { t } = useTranslation() const { t } = useTranslation();
const { status, setStatus } = useStatus() const { status, setStatus } = useStatus();
// --- 所有原始变量和逻辑保持不变 --- // @ts-expect-error DisableAnimatedMan is a global variable
const disableAnimatedMan = (window as any).DisableAnimatedMan as boolean const disableAnimatedMan = window.DisableAnimatedMan as boolean;
const customIllustration = (window as any).CustomIllustration || "/animated-man.webp"
const customBackgroundImage = (window as any).CustomBackgroundImage !== "" ? (window as any).CustomBackgroundImage : undefined
// 新增:一个组合了两个动作的点击处理函数 // @ts-expect-error CustomIllustration is a global variable
const handleServerCardClick = (serverStatus: 'all' | 'online' | 'offline') => { const customIllustration = window.CustomIllustration || "/animated-man.webp";
onViewChange('servers'); // 动作1: 确保视图切换回服务器
setStatus(serverStatus); // 动作2: 执行原有的状态筛选
}
return ( const customBackgroundImage =
<> (window.CustomBackgroundImage as string) !== ""
<section className="grid grid-cols-2 gap-4 lg:grid-cols-5 server-overview"> ? window.CustomBackgroundImage
<Card : undefined;
onClick={() => handleServerCardClick("all")}
className={cn(
"hover:border-blue-500 cursor-pointer transition-all",
{ "bg-card/70": customBackgroundImage },
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">{t("serverOverview.totalServers")}</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span>
</span>
<div className="text-lg font-semibold">{total}</div>
</div>
</section>
</CardContent>
</Card>
<Card
onClick={() => handleServerCardClick("online")}
className={cn(
"cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all",
{ "bg-card/70": customBackgroundImage },
{ "ring-green-500 ring-2 border-transparent": activeView === 'servers' && status === "online" }
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">{t("serverOverview.onlineServers")}</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
</span>
<div className="text-lg font-semibold">{online}</div>
</div>
</section>
</CardContent>
</Card>
<Card
onClick={() => handleServerCardClick("offline")}
className={cn(
"cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all",
{ "bg-card/70": customBackgroundImage },
{ "ring-red-500 ring-2 border-transparent": activeView === 'servers' && status === "offline" }
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">{t("serverOverview.offlineServers")}</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
</span>
<div className="text-lg font-semibold">{offline}</div>
</div>
</section>
</CardContent>
</Card>
<Card
onClick={() => onViewChange('domains')}
className={cn(
"cursor-pointer hover:ring-indigo-500 ring-1 ring-transparent transition-all",
{ "bg-card/70": customBackgroundImage },
{ "ring-indigo-500 ring-2 border-transparent": activeView === 'domains' }
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base"></p>
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-muted-foreground" />
<div className="text-lg font-semibold">{totalDomains}</div>
</div>
</section>
</CardContent>
</Card>
<Card // 新增:一个组合了两个动作的点击处理函数
className={cn("hover:ring-purple-500 ring-1 ring-transparent transition-all", { "bg-card/70": customBackgroundImage })} const handleServerCardClick = (serverStatus: 'all' | 'online' | 'offline') => {
> onViewChange('servers'); // 动作1: 确保视图切换回服务器
<CardContent className="flex h-full items-center relative px-6 py-3"> setStatus(serverStatus); // 动作2: 执行原有的状态筛选
<section className="flex flex-col gap-1 w-full"> };
<div className="flex items-center w-full justify-between"><p className="text-sm font-medium md:text-base">{t("serverOverview.network")}</p></div>
<section className="flex items-start flex-row z-10 pr-0 gap-1"> return (
<p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">{formatBytes(up)}</p> <section className="grid grid-cols-2 gap-4 lg:grid-cols-5 server-overview">
<p className="sm:text-[12px] text-[10px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">{formatBytes(down)}</p> <Card
</section> onClick={() => {
<section className="flex flex-col sm:flex-row -mr-1 sm:items-center items-start gap-1"> handleServerCardClick("all");
<p className="text-[11px] flex items-center text-nowrap font-semibold"><ArrowUpCircleIcon className="size-3 mr-0.5 sm:mb-[1px]" />{formatBytes(upSpeed)}/s</p> }}
<p className="text-[11px] flex items-center text-nowrap font-semibold"><ArrowDownCircleIcon className="size-3 mr-0.5" />{formatBytes(downSpeed)}/s</p> className={cn("hover:border-blue-500 cursor-pointer transition-all", {
</section> "bg-card/70": customBackgroundImage,
</section> })}
{!disableAnimatedMan && ( >
<img className="absolute right-[-30px] top-[-120px] z-10 w-40 scale-100 group-hover:opacity-50 md:scale-100 transition-all" alt={"animated-man"} src={customIllustration} loading="eager" /> <CardContent className="flex h-full items-center px-6 py-3">
)} <section className="flex flex-col gap-1">
</CardContent> <p className="text-sm font-medium md:text-base">
</Card> {t("serverOverview.totalServers")}
</section> </p>
</> <div className="flex items-center gap-2">
) <span className="relative flex h-2 w-2">
} <span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span>
</span>
<div className="text-lg font-semibold">{total}</div>
</div>
</section>
</CardContent>
</Card>
<Card
onClick={() => {
handleServerCardClick("online");
}}
className={cn(
"cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all",
{
"bg-card/70": customBackgroundImage,
},
{
"ring-green-500 ring-2 border-transparent": activeView === "servers" && status === "online",
},
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">
{t("serverOverview.onlineServers")}
</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
</span>
<div className="text-lg font-semibold">{online}</div>
</div>
</section>
</CardContent>
</Card>
<Card
onClick={() => {
handleServerCardClick("offline");
}}
className={cn(
"cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all",
{
"bg-card/70": customBackgroundImage,
},
{
"ring-red-500 ring-2 border-transparent": activeView === "servers" && status === "offline",
},
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base">
{t("serverOverview.offlineServers")}
</p>
<div className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"></span>
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
</span>
<div className="text-lg font-semibold">{offline}</div>
</div>
</section>
</CardContent>
</Card>
<Card
onClick={() => onViewChange("domains")}
className={cn(
"cursor-pointer hover:ring-indigo-500 ring-1 ring-transparent transition-all",
{
"bg-card/70": customBackgroundImage,
},
{
"ring-indigo-500 ring-2 border-transparent": activeView === "domains",
},
)}
>
<CardContent className="flex h-full items-center px-6 py-3">
<section className="flex flex-col gap-1">
<p className="text-sm font-medium md:text-base"></p>
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-muted-foreground" />
<div className="text-lg font-semibold">{totalDomains}</div>
</div>
</section>
</CardContent>
</Card>
<Card
className={cn(
"hover:ring-purple-500 ring-1 ring-transparent transition-all",
{
"bg-card/70": customBackgroundImage,
},
)}
>
<CardContent className="flex h-full items-center relative px-6 py-3">
<section className="flex flex-col gap-1 w-full">
<div className="flex items-center w-full justify-between">
<p className="text-sm font-medium md:text-base">
{t("serverOverview.network")}
</p>
</div>
<section className="flex items-start flex-row z-10 pr-0 gap-1">
<p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">
{formatBytes(up)}
</p>
<p className="sm:text-[12px] text-[10px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">
{formatBytes(down)}
</p>
</section>
<section className="flex flex-col sm:flex-row -mr-1 sm:items-center items-start gap-1">
<p className="text-[11px] flex items-center text-nowrap font-semibold">
<ArrowUpCircleIcon className="size-3 mr-0.5 sm:mb-px" />
{formatBytes(upSpeed)}/s
</p>
<p className="text-[11px] flex items-center text-nowrap font-semibold">
<ArrowDownCircleIcon className="size-3 mr-0.5" />
{formatBytes(downSpeed)}/s
</p>
</section>
</section>
{!disableAnimatedMan && (
<img
className="absolute right-3 top-[-85px] z-50 w-20 scale-90 group-hover:opacity-50 md:scale-100 transition-all"
alt={"animated-man"}
src={customIllustration}
loading="eager"
/>
)}
</CardContent>
</Card>
</section>
);
}
+18 -12
View File
@@ -1,17 +1,23 @@
import { Progress } from "@/components/ui/progress" import { Progress } from "@/components/ui/progress";
type ServerUsageBarProps = { type ServerUsageBarProps = {
value: number value: number;
} };
export default function ServerUsageBar({ value }: ServerUsageBarProps) { export default function ServerUsageBar({ value }: ServerUsageBarProps) {
return ( return (
<Progress <Progress
aria-label={"Server Usage Bar"} aria-label={"Server Usage Bar"}
aria-labelledby={"Server Usage Bar"} aria-labelledby={"Server Usage Bar"}
value={value} value={value}
indicatorClassName={value > 90 ? "bg-red-500" : value > 70 ? "bg-orange-400" : "bg-green-500"} indicatorClassName={
className={"h-[3px] rounded-sm"} value > 90
/> ? "bg-red-500"
) : value > 70
? "bg-orange-400"
: "bg-green-500"
}
className={"h-[3px] rounded-sm"}
/>
);
} }
+88 -67
View File
@@ -1,79 +1,100 @@
import { fetchService } from "@/lib/nezha-api" import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
import { NezhaServer, ServiceData } from "@/types/nezha-api" import { useQuery } from "@tanstack/react-query";
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid" import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query" import { fetchService } from "@/lib/nezha-api";
import { useTranslation } from "react-i18next" import type { NezhaServer, ServiceData } from "@/types/nezha-api";
import { CycleTransferStatsCard } from "./CycleTransferStats" import { CycleTransferStatsCard } from "./CycleTransferStats";
import ServiceTrackerClient from "./ServiceTrackerClient" import { Loader } from "./loading/Loader";
import { Loader } from "./loading/Loader" import ServiceTrackerClient from "./ServiceTrackerClient";
export function ServiceTracker({ serverList }: { serverList: NezhaServer[] }) { export function ServiceTracker({ serverList }: { serverList: NezhaServer[] }) {
const { t } = useTranslation() const { t } = useTranslation();
const { data: serviceData, isLoading } = useQuery({ const { data: serviceData, isLoading } = useQuery({
queryKey: ["service"], queryKey: ["service"],
queryFn: () => fetchService(), queryFn: () => fetchService(),
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
refetchInterval: 10000, refetchInterval: 10000,
}) });
const processServiceData = (serviceData: ServiceData) => { const processServiceData = (serviceData: ServiceData) => {
const days = serviceData.up.map((up, index) => { const days = serviceData.up.map((up, index) => {
const totalChecks = up + serviceData.down[index] const totalChecks = up + serviceData.down[index];
const dailyUptime = totalChecks > 0 ? (up / totalChecks) * 100 : 0 const dailyUptime = totalChecks > 0 ? (up / totalChecks) * 100 : 0;
return { return {
completed: up > serviceData.down[index], completed: up > serviceData.down[index],
date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000), date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000),
uptime: dailyUptime, uptime: dailyUptime,
delay: serviceData.delay[index] || 0, delay: serviceData.delay[index] || 0,
} };
}) });
const totalUp = serviceData.up.reduce((a, b) => a + b, 0) const totalUp = serviceData.up.reduce((a, b) => a + b, 0);
const totalChecks = serviceData.up.reduce((a, b) => a + b, 0) + serviceData.down.reduce((a, b) => a + b, 0) const totalChecks =
const uptime = (totalUp / totalChecks) * 100 serviceData.up.reduce((a, b) => a + b, 0) +
serviceData.down.reduce((a, b) => a + b, 0);
const uptime = (totalUp / totalChecks) * 100;
const avgDelay = serviceData.delay.length > 0 ? serviceData.delay.reduce((a, b) => a + b, 0) / serviceData.delay.length : 0 const avgDelay =
serviceData.delay.length > 0
? serviceData.delay.reduce((a, b) => a + b, 0) /
serviceData.delay.length
: 0;
return { days, uptime, avgDelay } return { days, uptime, avgDelay };
} };
if (isLoading) { if (isLoading) {
return ( return (
<div className="mt-4 text-sm font-medium flex items-center gap-1"> <div className="mt-4 text-sm font-medium flex items-center gap-1">
<Loader visible={true} /> <Loader visible={true} />
{t("serviceTracker.loading")} {t("serviceTracker.loading")}
</div> </div>
) );
} }
if (!serviceData?.data?.services && !serviceData?.data?.cycle_transfer_stats) { if (
return ( !serviceData?.data?.services &&
<div className="mt-4 text-sm font-medium flex items-center gap-1"> !serviceData?.data?.cycle_transfer_stats
<ExclamationTriangleIcon className="w-4 h-4" /> ) {
{t("serviceTracker.noService")} return (
</div> <div className="mt-4 text-sm font-medium flex items-center gap-1">
) <ExclamationTriangleIcon className="w-4 h-4" />
} {t("serviceTracker.noService")}
</div>
);
}
return ( return (
<div className="mt-4 w-full mx-auto "> <div className="mt-4 w-full mx-auto ">
{serviceData.data.cycle_transfer_stats && ( {serviceData.data.cycle_transfer_stats && (
<div> <div>
<CycleTransferStatsCard serverList={serverList} cycleStats={serviceData.data.cycle_transfer_stats} /> <CycleTransferStatsCard
</div> serverList={serverList}
)} cycleStats={serviceData.data.cycle_transfer_stats}
{serviceData.data.services && Object.keys(serviceData.data.services).length > 0 && ( />
<section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4"> </div>
{Object.entries(serviceData.data.services).map(([name, data]) => { )}
const { days, uptime, avgDelay } = processServiceData(data) {serviceData.data.services &&
return <ServiceTrackerClient key={name} days={days} title={data.service_name} uptime={uptime} avgDelay={avgDelay} /> Object.keys(serviceData.data.services).length > 0 && (
})} <section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4">
</section> {Object.entries(serviceData.data.services).map(([name, data]) => {
)} const { days, uptime, avgDelay } = processServiceData(data);
</div> return (
) <ServiceTrackerClient
key={name}
days={days}
title={data.service_name}
uptime={uptime}
avgDelay={avgDelay}
/>
);
})}
</section>
)}
</div>
);
} }
export default ServiceTracker export default ServiceTracker;
+154 -106
View File
@@ -1,118 +1,166 @@
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import type React from "react";
import { cn } from "@/lib/utils" import { useTranslation } from "react-i18next";
import React from "react" import {
import { useTranslation } from "react-i18next" Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { Separator } from "./ui/separator" import { Separator } from "./ui/separator";
interface ServiceTrackerProps { interface ServiceTrackerProps {
days: Array<{ days: Array<{
completed: boolean completed: boolean;
date?: Date date?: Date;
uptime: number uptime: number;
delay: number delay: number;
}> }>;
className?: string className?: string;
title?: string title?: string;
uptime?: number uptime?: number;
avgDelay?: number avgDelay?: number;
} }
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, className, title, uptime = 100, avgDelay = 0 }) => { export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
const { t } = useTranslation() days,
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined className,
title,
uptime = 100,
avgDelay = 0,
}) => {
const { t } = useTranslation();
const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const getUptimeColor = (uptime: number) => { const getUptimeColor = (uptime: number) => {
if (uptime >= 99) return "text-emerald-500" if (uptime >= 99) return "text-emerald-500";
if (uptime >= 95) return "text-amber-500" if (uptime >= 95) return "text-amber-500";
return "text-rose-500" return "text-rose-500";
} };
const getDelayColor = (delay: number) => { const getDelayColor = (delay: number) => {
if (delay < 100) return "text-emerald-500" if (delay < 100) return "text-emerald-500";
if (delay < 300) return "text-amber-500" if (delay < 300) return "text-amber-500";
return "text-rose-500" return "text-rose-500";
} };
const getStatusColor = (uptime: number) => { const getStatusColor = (uptime: number) => {
if (uptime >= 99) return "bg-emerald-500" if (uptime >= 99) return "bg-emerald-500";
if (uptime >= 95) return "bg-amber-500" if (uptime >= 95) return "bg-amber-500";
return "bg-rose-500" return "bg-rose-500";
} };
return ( return (
<div <div
className={cn( className={cn(
"w-full space-y-3 bg-white px-4 py-4 rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none", "w-full space-y-3 bg-white px-4 py-4 rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
className, className,
{ {
"bg-card/70": customBackgroundImage, "bg-card/70": customBackgroundImage,
}, },
)} )}
> >
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className={cn("w-2.5 h-2.5 rounded-full transition-colors", getStatusColor(uptime))} /> <div
<span className="font-medium text-sm">{title}</span> className={cn(
</div> "w-2.5 h-2.5 rounded-full transition-colors",
<div className="flex items-center gap-3"> getStatusColor(uptime),
<span className={cn("font-medium text-sm transition-colors", getDelayColor(avgDelay))}>{avgDelay.toFixed(0)}ms</span> )}
<Separator className="h-4" orientation="vertical" /> />
<span className={cn("font-medium text-sm transition-colors", getUptimeColor(uptime))}> <span className="font-medium text-sm">{title}</span>
{uptime.toFixed(1)}% {t("serviceTracker.uptime")} </div>
</span> <div className="flex items-center gap-3">
</div> <span
</div> className={cn(
"font-medium text-sm transition-colors",
getDelayColor(avgDelay),
)}
>
{avgDelay.toFixed(0)}ms
</span>
<Separator className="h-4" orientation="vertical" />
<span
className={cn(
"font-medium text-sm transition-colors",
getUptimeColor(uptime),
)}
>
{uptime.toFixed(1)}% {t("serviceTracker.uptime")}
</span>
</div>
</div>
<div className="flex gap-[3px] bg-muted/30 p-1 rounded-lg"> <div className="flex gap-[3px] bg-muted/30 p-1 rounded-lg">
{days.map((day, index) => ( {days.map((day, index) => (
<TooltipProvider delayDuration={50} key={index}> <TooltipProvider delayDuration={50} key={index}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div <div
className={cn( className={cn(
"relative flex-1 h-7 rounded-[4px] transition-all duration-200 cursor-help", "relative flex-1 h-7 rounded-[4px] transition-all duration-200 cursor-help",
"before:absolute before:inset-0 before:rounded-[4px] before:opacity-0 hover:before:opacity-100 before:bg-white/10 before:transition-opacity", "before:absolute before:inset-0 before:rounded-[4px] before:opacity-0 hover:before:opacity-100 before:bg-white/10 before:transition-opacity",
"after:absolute after:inset-0 after:rounded-[4px] after:shadow-[inset_0_1px_theme(colors.white/10%)]", "after:absolute after:inset-0 after:rounded-[4px] after:shadow-[inset_0_1px_--theme(--color-white/10%)]",
day.completed day.completed
? "bg-gradient-to-b from-green-500/90 to-green-600 shadow-[0_1px_2px_theme(colors.green.600/30%)]" ? "bg-linear-to-b from-green-500/90 to-green-600 shadow-[0_1px_2px_--theme(--color-green-600/30%)]"
: "bg-gradient-to-b from-red-500/80 to-red-600/90 shadow-[0_1px_2px_theme(colors.red.600/30%)]", : "bg-linear-to-b from-red-500/80 to-red-600/90 shadow-[0_1px_2px_--theme(--color-red-600/30%)]",
)} )}
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="p-0 overflow-hidden"> <TooltipContent className="p-0 overflow-hidden">
<div className="px-3 py-2 bg-popover"> <div className="px-3 py-2 bg-popover">
<p className="font-medium text-sm mb-2">{day.date?.toLocaleDateString()}</p> <p className="font-medium text-sm mb-2">
<div className="space-y-1.5"> {day.date?.toLocaleDateString()}
<div className="flex items-center justify-between gap-3"> </p>
<span className="text-xs text-muted-foreground">{t("serviceTracker.uptime")}:</span> <div className="space-y-1.5">
<span className={cn("text-xs font-medium", day.uptime > 95 ? "text-green-500" : "text-red-500")}>{day.uptime.toFixed(1)}%</span> <div className="flex items-center justify-between gap-3">
</div> <span className="text-xs text-muted-foreground">
<div className="flex items-center justify-between gap-3"> {t("serviceTracker.uptime")}:
<span className="text-xs text-muted-foreground">{t("serviceTracker.delay")}:</span> </span>
<span <span
className={cn( className={cn(
"text-xs font-medium", "text-xs font-medium",
day.delay < 100 ? "text-green-500" : day.delay < 300 ? "text-yellow-500" : "text-red-500", day.uptime > 95 ? "text-green-500" : "text-red-500",
)} )}
> >
{day.delay.toFixed(0)}ms {day.uptime.toFixed(1)}%
</span> </span>
</div> </div>
</div> <div className="flex items-center justify-between gap-3">
</div> <span className="text-xs text-muted-foreground">
</TooltipContent> {t("serviceTracker.delay")}:
</Tooltip> </span>
</TooltipProvider> <span
))} className={cn(
</div> "text-xs font-medium",
day.delay < 100
? "text-green-500"
: day.delay < 300
? "text-yellow-500"
: "text-red-500",
)}
>
{day.delay.toFixed(0)}ms
</span>
</div>
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
))}
</div>
<div className="flex justify-between text-xs text-stone-500 dark:text-stone-400"> <div className="flex justify-between text-xs text-stone-500 dark:text-stone-400">
<span>30 {t("serviceTracker.daysAgo")}</span> <span>30 {t("serviceTracker.daysAgo")}</span>
<span>{t("serviceTracker.today")}</span> <span>{t("serviceTracker.today")}</span>
</div> </div>
</div> </div>
) );
} };
export default ServiceTrackerClient export default ServiceTrackerClient;
+56 -40
View File
@@ -1,42 +1,58 @@
import { cn } from "@/lib/utils" import { m } from "framer-motion";
import { m } from "framer-motion" import { useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next" import { cn } from "@/lib/utils";
export default function TabSwitch({ tabs, currentTab, setCurrentTab }: { tabs: string[]; currentTab: string; setCurrentTab: (tab: string) => void }) { export default function TabSwitch({
const { t } = useTranslation() tabs,
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined currentTab,
return ( setCurrentTab,
<div className="z-50 flex flex-col items-start rounded-[50px] server-info-tab"> }: {
<div tabs: string[];
className={cn("flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800", { currentTab: string;
"bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage, setCurrentTab: (tab: string) => void;
})} }) {
> const { t } = useTranslation();
{tabs.map((tab: string) => ( const customBackgroundImage =
<div (window.CustomBackgroundImage as string) !== ""
key={tab} ? window.CustomBackgroundImage
onClick={() => setCurrentTab(tab)} : undefined;
className={cn( return (
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-[600] transition-all duration-500", <div className="z-50 flex flex-col items-start rounded-[50px] server-info-tab">
currentTab === tab ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500", <div
)} className={cn(
> "flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800",
{currentTab === tab && ( {
<m.div "bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage,
layoutId="tab-switch-active" },
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5" )}
style={{ >
originY: "0px", {tabs.map((tab: string) => (
borderRadius: 46, <div
}} key={tab}
/> onClick={() => setCurrentTab(tab)}
)} className={cn(
<div className="relative z-20 flex items-center gap-1"> "relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-semibold transition-all duration-500",
<p className="whitespace-nowrap">{t("tabSwitch." + tab)}</p> currentTab === tab
</div> ? "text-black dark:text-white"
</div> : "text-stone-400 dark:text-stone-500",
))} )}
</div> >
</div> {currentTab === tab && (
) <m.div
layoutId="tab-switch-active"
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5"
style={{
originY: "0px",
borderRadius: 46,
}}
/>
)}
<div className="relative z-20 flex items-center gap-1">
<p className="whitespace-nowrap">{t(`tabSwitch.${tab}`)}</p>
</div>
</div>
))}
</div>
</div>
);
} }
+29 -27
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() { export function ThemeColorManager() {
const { theme } = useTheme() const { theme } = useTheme();
useEffect(() => { useEffect(() => {
const updateThemeColor = () => { const updateThemeColor = () => {
const currentTheme = theme const currentTheme = theme;
const meta = document.querySelector('meta[name="theme-color"]') const meta = document.querySelector('meta[name="theme-color"]');
if (!meta) { if (!meta) {
const newMeta = document.createElement("meta") const newMeta = document.createElement("meta");
newMeta.name = "theme-color" newMeta.name = "theme-color";
document.head.appendChild(newMeta) document.head.appendChild(newMeta);
} }
const themeColor = const themeColor =
currentTheme === "dark" currentTheme === "dark"
? "hsl(30 15% 8%)" // 深色模式背景色 ? "hsl(30 15% 8%)" // 深色模式背景色
: "hsl(0 0% 98%)" // 浅色模式背景色 : "hsl(0 0% 98%)"; // 浅色模式背景色
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor) document
} .querySelector('meta[name="theme-color"]')
?.setAttribute("content", themeColor);
};
// Update on mount and theme change // Update on mount and theme change
updateThemeColor() updateThemeColor();
// Listen for system theme changes // Listen for system theme changes
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener("change", updateThemeColor) mediaQuery.addEventListener("change", updateThemeColor);
return () => mediaQuery.removeEventListener("change", updateThemeColor) return () => mediaQuery.removeEventListener("change", updateThemeColor);
}, [theme]) }, [theme]);
return null return null;
} }
+72 -47
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 = { type ThemeProviderProps = {
children: ReactNode children: ReactNode;
defaultTheme?: Theme defaultTheme?: Theme;
storageKey?: string storageKey?: string;
} };
type ThemeProviderState = { type ThemeProviderState = {
theme: Theme theme: Theme;
setTheme: (theme: Theme) => void setTheme: (theme: Theme) => void;
} };
const initialState: ThemeProviderState = { const initialState: ThemeProviderState = {
theme: "system", theme: "system",
setTheme: () => null, setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
storageKey = "vite-ui-theme",
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || "system",
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.add("disable-transitions");
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
const themeColor =
systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
document
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", themeColor);
const timeoutId = window.setTimeout(() => {
root.classList.remove("disable-transitions");
}, 0);
return () => window.clearTimeout(timeoutId);
}
root.classList.add(theme);
const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
document
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", themeColor);
const timeoutId = window.setTimeout(() => {
root.classList.remove("disable-transitions");
}, 0);
return () => window.clearTimeout(timeoutId);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider value={value}>
{children}
</ThemeProviderContext.Provider>
);
} }
const ThemeProviderContext = createContext<ThemeProviderState>(initialState) export { ThemeProviderContext };
export function ThemeProvider({ children, storageKey = "vite-ui-theme" }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || "system")
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
root.classList.add(systemTheme)
const themeColor = systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
return
}
root.classList.add(theme)
const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return <ThemeProviderContext.Provider value={value}>{children}</ThemeProviderContext.Provider>
}
export { ThemeProviderContext }
+69 -46
View File
@@ -1,53 +1,76 @@
import { Theme } from "@/components/ThemeProvider" import { CheckCircleIcon } from "@heroicons/react/20/solid";
import { Button } from "@/components/ui/button" import { Moon, Sun } from "lucide-react";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils" import type { Theme } from "@/components/ThemeProvider";
import { CheckCircleIcon } from "@heroicons/react/20/solid" import { Button } from "@/components/ui/button";
import { Moon, Sun } from "lucide-react" import {
import { useTranslation } from "react-i18next" DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { useTheme } from "../hooks/use-theme" import { useTheme } from "../hooks/use-theme";
export function ModeToggle() { export function ModeToggle() {
const { t } = useTranslation() const { t } = useTranslation();
const { setTheme, theme } = useTheme() const { setTheme, theme } = useTheme();
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined const customBackgroundImage =
(window.CustomBackgroundImage as string) !== ""
? window.CustomBackgroundImage
: undefined;
const handleSelect = (e: Event, newTheme: Theme) => { const handleSelect = (e: Event, newTheme: Theme) => {
e.preventDefault() e.preventDefault();
setTheme(newTheme) setTheme(newTheme);
} };
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className={cn("rounded-full px-[9px] bg-white dark:bg-black", { className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
"bg-white/70 dark:bg-black/70": customBackgroundImage, "bg-white/70 dark:bg-black/70": customBackgroundImage,
})} })}
> >
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span> <span className="sr-only">Toggle theme</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="flex flex-col gap-0.5" align="end"> <DropdownMenuContent className="flex flex-col gap-0.5" align="end">
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "light" })} onSelect={(e) => handleSelect(e, "light")}> <DropdownMenuItem
{t("theme.light")} className={cn("rounded-b-[5px] text-xs", {
{theme === "light" && <CheckCircleIcon className="size-4" />} "gap-3 bg-muted font-semibold": theme === "light",
</DropdownMenuItem> })}
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "dark" })} onSelect={(e) => handleSelect(e, "dark")}> onSelect={(e) => handleSelect(e, "light")}
{t("theme.dark")} >
{theme === "dark" && <CheckCircleIcon className="size-4" />} {t("theme.light")}
</DropdownMenuItem> {theme === "light" && <CheckCircleIcon className="size-4" />}
<DropdownMenuItem className={cn({ "gap-3 bg-muted": theme === "system" })} onSelect={(e) => handleSelect(e, "system")}> </DropdownMenuItem>
{t("theme.system")} <DropdownMenuItem
{theme === "system" && <CheckCircleIcon className="size-4" />} className={cn("rounded-[5px] text-xs", {
</DropdownMenuItem> "gap-3 bg-muted font-semibold": theme === "dark",
</DropdownMenuContent> })}
</DropdownMenu> onSelect={(e) => handleSelect(e, "dark")}
) >
{t("theme.dark")}
{theme === "dark" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem>
<DropdownMenuItem
className={cn("rounded-t-[5px] text-xs", {
"gap-3 bg-muted font-semibold": theme === "system",
})}
onSelect={(e) => handleSelect(e, "system")}
>
{t("theme.system")}
{theme === "system" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
} }
+100 -62
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 }) { export default function BillingInfo({
const { t } = useTranslation() parsedData,
if (!parsedData || !parsedData.billingDataMod) { }: {
return null parsedData: PublicNoteData;
} }) {
const { t } = useTranslation();
if (!parsedData || !parsedData.billingDataMod) {
return null;
}
let isNeverExpire = false let isNeverExpire = false;
let daysLeftObject = { let daysLeftObject = {
days: 0, days: 0,
cycleLabel: "", cycleLabel: "",
remainingPercentage: 0, remainingPercentage: 0,
} };
const hasBillingDates =
Boolean(parsedData.billingDataMod.startDate) ||
Boolean(parsedData.billingDataMod.endDate);
if (parsedData?.billingDataMod?.endDate) { if (parsedData?.billingDataMod?.endDate) {
if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) { if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) {
isNeverExpire = true isNeverExpire = true;
} else { } else {
try { try {
daysLeftObject = getDaysBetweenDatesWithAutoRenewal(parsedData.billingDataMod) daysLeftObject = getDaysBetweenDatesWithAutoRenewal(
} catch (error) { parsedData.billingDataMod,
console.error(error) );
return ( } catch (error) {
<div className={cn("text-[10px] text-muted-foreground text-red-600")}> console.error(error);
{t("billingInfo.remaining")}: {t("billingInfo.error")} return (
</div> <div className={cn("text-[10px] text-muted-foreground text-red-600")}>
) {t("billingInfo.remaining")}: {t("billingInfo.error")}
} </div>
} );
} }
}
}
return daysLeftObject.days >= 0 ? ( return daysLeftObject.days >= 0 ? (
<> <>
{parsedData.billingDataMod.amount && parsedData.billingDataMod.amount !== "0" && parsedData.billingDataMod.amount !== "-1" ? ( {parsedData.billingDataMod.amount &&
<p className={cn("text-[10px] text-muted-foreground ")}> parsedData.billingDataMod.amount !== "0" &&
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle} parsedData.billingDataMod.amount !== "-1" ? (
</p> <p className={cn("text-[10px] text-muted-foreground ")}>
) : parsedData.billingDataMod.amount === "0" ? ( {t("billingInfo.price")}: {parsedData.billingDataMod.amount}/
<p className={cn("text-[10px] text-green-600 ")}>{t("billingInfo.free")}</p> {parsedData.billingDataMod.cycle}
) : parsedData.billingDataMod.amount === "-1" ? ( </p>
<p className={cn("text-[10px] text-pink-600 ")}>{t("billingInfo.usage-baseed")}</p> ) : parsedData.billingDataMod.amount === "0" ? (
) : null} <p className={cn("text-[10px] text-green-600 ")}>
<div className={cn("text-[10px] text-muted-foreground")}> {t("billingInfo.free")}
{t("billingInfo.remaining")}: {isNeverExpire ? t("billingInfo.indefinite") : daysLeftObject.days + " " + t("billingInfo.days")} </p>
</div> ) : parsedData.billingDataMod.amount === "-1" ? (
{!isNeverExpire && <RemainPercentBar className="mt-0.5" value={daysLeftObject.remainingPercentage * 100} />} <p className={cn("text-[10px] text-pink-600 ")}>
</> {t("billingInfo.usage-baseed")}
) : ( </p>
<> ) : null}
{parsedData.billingDataMod.amount && parsedData.billingDataMod.amount !== "0" && parsedData.billingDataMod.amount !== "-1" ? ( {hasBillingDates && (
<p className={cn("text-[10px] text-muted-foreground ")}> <div className={cn("text-[10px] text-muted-foreground")}>
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle} {t("billingInfo.remaining")}:{" "}
</p> {isNeverExpire
) : parsedData.billingDataMod.amount === "0" ? ( ? t("billingInfo.indefinite")
<p className={cn("text-[10px] text-green-600 ")}>{t("billingInfo.free")}</p> : `${daysLeftObject.days} ${t("billingInfo.days")}`}
) : parsedData.billingDataMod.amount === "-1" ? ( </div>
<p className={cn("text-[10px] text-pink-600 ")}>{t("billingInfo.usage-baseed")}</p> )}
) : null} {hasBillingDates && !isNeverExpire && (
<p className={cn("text-[10px] text-muted-foreground text-red-600")}> <RemainPercentBar
{t("billingInfo.expired")}: {daysLeftObject.days * -1} {t("billingInfo.days")} className="mt-0.5"
</p> value={daysLeftObject.remainingPercentage * 100}
</> />
) )}
</>
) : (
<>
{parsedData.billingDataMod.amount &&
parsedData.billingDataMod.amount !== "0" &&
parsedData.billingDataMod.amount !== "-1" ? (
<p className={cn("text-[10px] text-muted-foreground ")}>
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/
{parsedData.billingDataMod.cycle}
</p>
) : parsedData.billingDataMod.amount === "0" ? (
<p className={cn("text-[10px] text-green-600 ")}>
{t("billingInfo.free")}
</p>
) : parsedData.billingDataMod.amount === "-1" ? (
<p className={cn("text-[10px] text-pink-600 ")}>
{t("billingInfo.usage-baseed")}
</p>
) : null}
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
{t("billingInfo.expired")}: {daysLeftObject.days * -1}{" "}
{t("billingInfo.days")}
</p>
</>
);
} }
+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>
);
}
+28 -28
View File
@@ -1,32 +1,32 @@
const bars = Array(8).fill(0) const bars = Array(8).fill(0);
export const Loader = ({ visible }: { visible: boolean }) => { export const Loader = ({ visible }: { visible: boolean }) => {
return ( return (
<div className="hamster-loading-wrapper" data-visible={visible}> <div className="hamster-loading-wrapper" data-visible={visible}>
<div className="hamster-spinner"> <div className="hamster-spinner">
{bars.map((_, i) => ( {bars.map((_, i) => (
<div className="hamster-loading-bar" key={`hamster-bar-${i}`} /> <div className="hamster-loading-bar" key={`hamster-bar-${i}`} />
))} ))}
</div> </div>
</div> </div>
) );
} };
export const LoadingSpinner = () => { export const LoadingSpinner = () => {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
strokeWidth="2" strokeWidth="2"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
className={"size-4 animate-spin"} className={"size-4 animate-spin"}
> >
<path d="M21 12a9 9 0 1 1-6.219-8.56" /> <path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg> </svg>
) );
} };
+30 -30
View File
@@ -1,38 +1,38 @@
import { Skeleton } from "@/components/ui/skeleton" import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom" import { Skeleton } from "@/components/ui/skeleton";
import { BackIcon } from "../Icon" import { BackIcon } from "../Icon";
export function ServerDetailChartLoading() { export function ServerDetailChartLoading() {
return ( return (
<div> <div>
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3"> <section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton> <Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton> <Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton> <Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton> <Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton> <Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton> <Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</section> </section>
</div> </div>
) );
} }
export function ServerDetailLoading() { export function ServerDetailLoading() {
const navigate = useNavigate() const navigate = useNavigate();
return ( return (
<div className="mx-auto w-full max-w-5xl px-0"> <div className="mx-auto w-full max-w-5xl px-0">
<div <div
onClick={() => { onClick={() => {
navigate("/") navigate("/");
}} }}
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl" className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
> >
<BackIcon /> <BackIcon />
<Skeleton className="h-[20px] w-24 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton> <Skeleton className="h-[20px] w-24 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</div> </div>
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton> <Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
</div> </div>
) );
} }
+1 -1
View File
@@ -1 +1 @@
export { domMax as default } from "framer-motion" export { domMax as default } from "framer-motion";
+9 -8
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 }) => { export const MotionProvider = ({ children }: { children: React.ReactNode }) => {
return ( return (
<LazyMotion features={loadFeatures} strict key="framer"> <LazyMotion features={loadFeatures} strict key="framer">
{children} {children}
</LazyMotion> </LazyMotion>
) );
} };
+44 -38
View File
@@ -1,49 +1,55 @@
import { cn } from "@/lib/utils" import * as AccordionPrimitive from "@radix-ui/react-accordion";
import * as AccordionPrimitive from "@radix-ui/react-accordion" import { ChevronDown } from "lucide-react";
import { ChevronDown } from "lucide-react" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef< const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>, React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />) >(({ className, ...props }, ref) => (
AccordionItem.displayName = "AccordionItem" <AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef< const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>, React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex"> <AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger <AccordionPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex flex-1 items-center justify-start py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180", "flex flex-1 items-center justify-start py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" /> <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger> </AccordionPrimitive.Trigger>
</AccordionPrimitive.Header> </AccordionPrimitive.Header>
)) ));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef< const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>, React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content <AccordionPrimitive.Content
ref={ref} ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props} {...props}
> >
<div className={cn("pb-4 pt-0", className)}>{children}</div> <div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content> </AccordionPrimitive.Content>
)) ));
AccordionContent.displayName = AccordionPrimitive.Content.displayName AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -1,89 +1,107 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
interface Props { interface Props {
max: number max: number;
value: number value: number;
min: number min: number;
className?: string className?: string;
primaryColor?: string primaryColor?: string;
} }
export default function AnimatedCircularProgressBar({ max = 100, min = 0, value = 0, primaryColor, className }: Props) { export default function AnimatedCircularProgressBar({
const circumference = 2 * Math.PI * 45 max = 100,
const percentPx = circumference / 100 min = 0,
const currentPercent = ((value - min) / (max - min)) * 100 value = 0,
primaryColor,
className,
}: Props) {
const circumference = 2 * Math.PI * 45;
const percentPx = circumference / 100;
const currentPercent = ((value - min) / (max - min)) * 100;
return ( return (
<div <div
className={cn("relative size-40 text-2xl font-semibold", className)} className={cn("relative size-40 text-2xl font-semibold", className)}
style={ style={
{ {
"--circle-size": "100px", "--circle-size": "100px",
"--circumference": circumference, "--circumference": circumference,
"--percent-to-px": `${percentPx}px`, "--percent-to-px": `${percentPx}px`,
"--gap-percent": "5", "--gap-percent": "5",
"--offset-factor": "0", "--offset-factor": "0",
"--transition-length": "1s", "--transition-length": "1s",
"--transition-step": "200ms", "--transition-step": "200ms",
"--delay": "0s", "--delay": "0s",
"--percent-to-deg": "3.6deg", "--percent-to-deg": "3.6deg",
transform: "translateZ(0)", transform: "translateZ(0)",
} as React.CSSProperties } as React.CSSProperties
} }
> >
<svg fill="none" className="size-full" strokeWidth="2" viewBox="0 0 100 100"> <svg
{currentPercent <= 90 && currentPercent >= 0 && ( fill="none"
<circle className="size-full"
cx="50" strokeWidth="2"
cy="50" viewBox="0 0 100 100"
r="45" >
strokeWidth="10" {currentPercent <= 90 && currentPercent >= 0 && (
strokeDashoffset="0" <circle
strokeLinecap="round" cx="50"
strokeLinejoin="round" cy="50"
className="opacity-100 stroke-muted" r="45"
style={ strokeWidth="10"
{ strokeDashoffset="0"
"--stroke-percent": 90 - currentPercent, strokeLinecap="round"
"--offset-factor-secondary": "calc(1 - var(--offset-factor))", strokeLinejoin="round"
strokeDasharray: "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)", className="opacity-100 stroke-muted"
transform: "rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)", style={
transition: "all var(--transition-length) ease var(--delay)", {
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)", "--stroke-percent": 90 - currentPercent,
} as React.CSSProperties "--offset-factor-secondary": "calc(1 - var(--offset-factor))",
} strokeDasharray:
/> "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
)} transform:
<circle "rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
cx="50" transition: "all var(--transition-length) ease var(--delay)",
cy="50" transformOrigin:
r="45" "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
strokeWidth="10" } as React.CSSProperties
strokeDashoffset="0" }
strokeLinecap="round" />
strokeLinejoin="round" )}
className={cn("opacity-100 stroke-current", { <circle
"stroke-[var(--stroke-primary-color)]": primaryColor, cx="50"
})} cy="50"
style={ r="45"
{ strokeWidth="10"
"--stroke-primary-color": primaryColor, strokeDashoffset="0"
"--stroke-percent": currentPercent, strokeLinecap="round"
strokeDasharray: "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)", strokeLinejoin="round"
transition: "var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)", className={cn("opacity-100 stroke-current", {
transitionProperty: "stroke-dasharray,transform", "stroke-(--stroke-primary-color)": primaryColor,
transform: "rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))", })}
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)", style={
} as React.CSSProperties {
} "--stroke-primary-color": primaryColor,
/> "--stroke-percent": currentPercent,
</svg> strokeDasharray:
<span "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
data-current-value={currentPercent} transition:
className="duration-&lsqb;var(--transition-length)&rsqb; delay-&lsqb;var(--delay)&rsqb; absolute inset-0 m-auto size-fit ease-linear animate-in fade-in" "var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)",
> transitionProperty: "stroke-dasharray,transform",
{currentPercent} transform:
</span> "rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))",
</div> transformOrigin:
) "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
} as React.CSSProperties
}
/>
</svg>
<span
data-current-value={currentPercent}
className="duration-&lsqb;var(--transition-length)&rsqb; delay-&lsqb;var(--delay)&rsqb; absolute inset-0 m-auto size-fit ease-linear animate-in fade-in"
>
{currentPercent}
</span>
</div>
);
} }
+28 -21
View File
@@ -1,28 +1,35 @@
import { cn } from "@/lib/utils" import { cva, type VariantProps } from "class-variance-authority";
import { type VariantProps, cva } from "class-variance-authority" import type * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
{ {
variants: { variants: {
variant: { variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", default:
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", secondary:
outline: "text-foreground", "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
}, destructive:
}, "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
defaultVariants: { outline: "text-foreground",
variant: "default", },
}, },
}, defaultVariants: {
) variant: "default",
},
},
);
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {} export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} /> return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
} }
export { Badge, badgeVariants } export { Badge, badgeVariants };
+49 -36
View File
@@ -1,42 +1,55 @@
import { cn } from "@/lib/utils" import { Slot } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority";
import { type VariantProps, cva } from "class-variance-authority" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", destructive:
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", "bg-destructive text-destructive-foreground hover:bg-destructive/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", outline:
ghost: "hover:bg-accent hover:text-accent-foreground", "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline", secondary:
}, "bg-secondary text-secondary-foreground hover:bg-secondary/80",
size: { ghost: "hover:bg-accent hover:text-accent-foreground",
default: "h-10 px-4 py-2", link: "text-primary underline-offset-4 hover:underline",
sm: "h-9 rounded-md px-3", },
lg: "h-11 rounded-md px-8", size: {
icon: "h-10 w-10", default: "h-10 px-4 py-2",
}, sm: "h-9 rounded-md px-3",
}, lg: "h-11 rounded-md px-8",
defaultVariants: { icon: "h-10 w-10",
variant: "default", },
size: "default", },
}, defaultVariants: {
}, variant: "default",
) size: "default",
},
},
);
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { export interface ButtonProps
asChild?: boolean extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => { const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
const Comp = asChild ? Slot : "button" ({ className, variant, size, asChild = false, ...props }, ref) => {
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} /> const Comp = asChild ? Slot : "button";
}) return (
Button.displayName = "Button" <Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants } export { Button, buttonVariants };
+78 -31
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<
<div HTMLDivElement,
ref={ref} React.HTMLAttributes<HTMLDivElement>
className={cn("rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none", className)} >(({ className, ...props }, ref) => (
{...props} <div
/> ref={ref}
)) className={cn(
Card.displayName = "Card" "rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => ( const CardHeader = React.forwardRef<
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} /> HTMLDivElement,
)) React.HTMLAttributes<HTMLDivElement>
CardHeader.displayName = "CardHeader" >(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(({ className, ...props }, ref) => ( const CardTitle = React.forwardRef<
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} /> HTMLParagraphElement,
)) React.HTMLAttributes<HTMLHeadingElement>
CardTitle.displayName = "CardTitle" >(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(({ className, ...props }, ref) => ( const CardDescription = React.forwardRef<
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} /> HTMLParagraphElement,
)) React.HTMLAttributes<HTMLParagraphElement>
CardDescription.displayName = "CardDescription" >(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => ( const CardContent = React.forwardRef<
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> HTMLDivElement,
)) React.HTMLAttributes<HTMLDivElement>
CardContent.displayName = "CardContent" >(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => ( const CardFooter = React.forwardRef<
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} /> HTMLDivElement,
)) React.HTMLAttributes<HTMLDivElement>
CardFooter.displayName = "CardFooter" >(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
+346 -226
View File
@@ -1,277 +1,397 @@
import { cn } from "@/lib/utils" "use client";
import * as React from "react"
import * as RechartsPrimitive from "recharts" import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR } // Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = { export type ChartConfig = {
[k in string]: { [k: string]: {
label?: React.ReactNode label?: React.ReactNode;
icon?: React.ComponentType icon?: React.ComponentType;
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> }) } & (
} | { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
type ChartContextProps = { type ChartContextProps = {
config: ChartConfig config: ChartConfig;
} };
const ChartContext = React.createContext<ChartContextProps | null>(null) const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() { function useChart() {
const context = React.useContext(ChartContext) const context = React.useContext(ChartContext);
if (!context) { if (!context) {
throw new Error("useChart must be used within a <ChartContainer />") throw new Error("useChart must be used within a <ChartContainer />");
} }
return context return context;
} }
const ChartContainer = React.forwardRef< const ChartContainer = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & { React.ComponentProps<"div"> & {
config: ChartConfig config: ChartConfig;
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"] children: React.ComponentProps<
} typeof RechartsPrimitive.ResponsiveContainer
>["children"];
}
>(({ id, className, children, config, ...props }, ref) => { >(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId() const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return ( return (
<ChartContext.Provider value={{ config }}> <ChartContext.Provider value={{ config }}>
<div <div
data-chart={chartId} data-chart={chartId}
ref={ref} ref={ref}
className={cn( className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none", "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden",
className, className,
)} )}
{...props} {...props}
> >
<ChartStyle id={chartId} config={config} /> <ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer> <RechartsPrimitive.ResponsiveContainer>
</div> {children}
</ChartContext.Provider> </RechartsPrimitive.ResponsiveContainer>
) </div>
}) </ChartContext.Provider>
ChartContainer.displayName = "Chart" );
});
ChartContainer.displayName = "Chart";
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color) const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color,
);
if (!colorConfig.length) { if (!colorConfig.length) {
return null return null;
} }
return ( return (
<style <style
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: Object.entries(THEMES) __html: Object.entries(THEMES)
.map( .map(
([theme, prefix]) => ` ([theme, prefix]) => `
${prefix} [data-chart=${id}] { ${prefix} [data-chart=${id}] {
${colorConfig ${colorConfig
.map(([key, itemConfig]) => { .map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color const color =
return color ? ` --color-${key}: ${color};` : null itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
}) itemConfig.color;
.join("\n")} return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
} }
`, `,
) )
.join("\n"), .join("\n"),
}} }}
/> />
) );
} };
const ChartTooltip = RechartsPrimitive.Tooltip const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef< const ChartTooltipContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> & React.ComponentProps<"div"> & {
React.ComponentProps<"div"> & { active?: boolean;
hideLabel?: boolean payload?: any[];
hideIndicator?: boolean label?: any;
indicator?: "line" | "dot" | "dashed" hideLabel?: boolean;
nameKey?: string hideIndicator?: boolean;
labelKey?: string indicator?: "line" | "dot" | "dashed";
} nameKey?: string;
labelKey?: string;
labelFormatter?: (value: any, payload: any[]) => React.ReactNode;
formatter?: (
value: any,
name: any,
item: any,
index: number,
payload: any,
) => React.ReactNode;
color?: string;
labelClassName?: string;
}
>( >(
( (
{ {
active, active,
payload, payload,
className, className,
indicator = "dot", indicator = "dot",
hideLabel = false, hideLabel = false,
hideIndicator = false, hideIndicator = false,
label, label,
labelFormatter, labelFormatter,
labelClassName, labelClassName,
formatter, formatter,
color, color,
nameKey, nameKey,
labelKey, labelKey,
}, },
ref, ref,
) => { ) => {
const { config } = useChart() const { config } = useChart();
const tooltipLabel = React.useMemo(() => { const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) { if (hideLabel || !payload?.length) {
return null return null;
} }
const [item] = payload const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || "value"}` const key = `${labelKey || item.dataKey || item.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value = !labelKey && typeof label === "string" ? config[label as keyof typeof config]?.label || label : itemConfig?.label const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) { if (labelFormatter) {
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div> return (
} <div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
}
if (!value) { if (!value) {
return null return null;
} }
return <div className={cn("font-medium", labelClassName)}>{value}</div> return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]) }, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
if (!active || !payload?.length) { if (!active || !payload?.length) {
return null return null;
} }
const nestLabel = payload.length === 1 && indicator !== "dot" payload.sort((a, b) => {
return Number(b.value) - Number(a.value);
});
return ( const nestLabel = payload.length === 1 && indicator !== "dot";
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return ( return (
<div <div
key={item.dataKey} ref={ref}
className={cn( className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", "grid min-w-32 items-start gap-1.5 overflow-hidden rounded-sm border border-border/50 bg-stone-100 text-xs dark:bg-stone-900",
indicator === "dot" && "items-center", className,
)} )}
> >
{formatter && item?.value !== undefined && item.name ? ( {!nestLabel && (
formatter(item.value, item.name, item, index, item.payload) <div className="mx-auto -mb-1 px-2.5 pt-1">
) : ( {!nestLabel ? tooltipLabel : null}
<> </div>
{itemConfig?.icon ? ( )}
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
})}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div className={cn("flex flex-1 justify-between leading-none", nestLabel ? "items-end" : "items-center")}>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
</div>
{item.value && <span className="font-mono font-medium tabular-nums text-foreground">{item.value.toLocaleString()}</span>}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
},
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend <div
className={cn("grid gap-1.5 bg-white px-2.5 py-1.5 dark:bg-black", {
"border-t": !nestLabel,
})}
>
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-border bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
},
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span
className={cn(
"ml-2 font-medium text-foreground tabular-nums",
payload.length === 1 && "-ml-9",
)}
>
{typeof item.value === "number"
? item.value.toFixed(2).toLocaleString()
: item.value}{" "}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
},
);
ChartTooltipContent.displayName = "ChartTooltip";
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef< const ChartLegendContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & React.ComponentProps<"div"> & {
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { payload?: any[];
hideIcon?: boolean verticalAlign?: "top" | "bottom" | "middle";
nameKey?: string hideIcon?: boolean;
} nameKey?: string;
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => { }
const { config } = useChart() >(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref,
) => {
const { config } = useChart();
if (!payload?.length) { if (!payload?.length) {
return null return null;
} }
return ( return (
<div ref={ref} className={cn("flex flex-wrap items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}> <div
{payload.map((item) => { ref={ref}
const key = `${nameKey || item.dataKey || "value"}` className={cn(
const itemConfig = getPayloadConfigFromPayload(config, item, key) "flex flex-wrap items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className,
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return ( return (
<div key={item.value} className={cn("flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground")}> <div
{itemConfig?.icon && !hideIcon ? ( key={item.value}
<itemConfig.icon /> className={cn(
) : ( "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
<div )}
className="h-2 w-2 shrink-0 rounded-[2px]" >
style={{ {itemConfig?.icon && !hideIcon ? (
backgroundColor: item.color, <itemConfig.icon />
}} ) : (
/> <div
)} className="h-2 w-2 shrink-0 rounded-[2px]"
{itemConfig?.label} style={{
</div> backgroundColor: item.color,
) }}
})} />
</div> )}
) {key}
}) </div>
ChartLegendContent.displayName = "ChartLegend" );
})}
</div>
);
},
);
ChartLegendContent.displayName = "ChartLegend";
// Helper to extract item config from a payload. // Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) { function getPayloadConfigFromPayload(
if (typeof payload !== "object" || payload === null) { config: ChartConfig,
return undefined payload: unknown,
} key: string,
) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload = "payload" in payload && typeof payload.payload === "object" && payload.payload !== null ? payload.payload : undefined const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key let configLabelKey: string = key;
if (key in payload && typeof payload[key as keyof typeof payload] === "string") { if (
configLabelKey = payload[key as keyof typeof payload] as string key in payload &&
} else if (payloadPayload && key in payloadPayload && typeof payloadPayload[key as keyof typeof payloadPayload] === "string") { typeof payload[key as keyof typeof payload] === "string"
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string ) {
} configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config] return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
} }
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle } export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
};
+25 -22
View File
@@ -1,24 +1,27 @@
import { cn } from "@/lib/utils" import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import { Check } from "lucide-react";
import { Check } from "lucide-react" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<React.ElementRef<typeof CheckboxPrimitive.Root>, React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>>( const Checkbox = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof CheckboxPrimitive.Root>,
<CheckboxPrimitive.Root React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
ref={ref} >(({ className, ...props }, ref) => (
className={cn( <CheckboxPrimitive.Root
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", ref={ref}
className, className={cn(
)} "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
{...props} className,
> )}
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}> {...props}
<Check className="h-4 w-4" /> >
</CheckboxPrimitive.Indicator> <CheckboxPrimitive.Indicator
</CheckboxPrimitive.Root> className={cn("flex items-center justify-center text-current")}
), >
) <Check className="h-4 w-4" />
Checkbox.displayName = CheckboxPrimitive.Root.displayName </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox } export { Checkbox };
+141 -89
View File
@@ -1,107 +1,159 @@
"use client" "use client";
import { Dialog, DialogContent } from "@/components/ui/dialog" import { type DialogProps, DialogTitle } from "@radix-ui/react-dialog";
import { cn } from "@/lib/utils" import { Command as CommandPrimitive } from "cmdk";
import { type DialogProps, DialogTitle } from "@radix-ui/react-dialog" import { Search } from "lucide-react";
import { Command as CommandPrimitive } from "cmdk" import * as React from "react";
import { Search } from "lucide-react" import { Dialog, DialogContent } from "@/components/ui/dialog";
import * as React from "react" import { cn } from "@/lib/utils";
const Command = React.forwardRef<React.ElementRef<typeof CommandPrimitive>, React.ComponentPropsWithoutRef<typeof CommandPrimitive>>( const Command = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof CommandPrimitive>,
<CommandPrimitive React.ComponentPropsWithoutRef<typeof CommandPrimitive>
ref={ref} >(({ className, ...props }, ref) => (
className={cn("flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", className)} <CommandPrimitive
{...props} ref={ref}
/> className={cn(
), "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
) className,
Command.displayName = CommandPrimitive.displayName )}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
const CommandDialog = ({ children, ...props }: DialogProps) => { const CommandDialog = ({ children, ...props }: DialogProps) => {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogTitle /> <DialogTitle />
<DialogContent className="overflow-hidden p-0 shadow-lg"> <DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-4 [&_[cmdk-input-wrapper]_svg]:w-4 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4"> <Command className="**:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 **:[[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-4 [&_[cmdk-input-wrapper]_svg]:w-4 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4">
{children} {children}
</Command> </Command>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} };
const CommandInput = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Input>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>>( const CommandInput = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof CommandPrimitive.Input>,
<div className="flex items-center bg-stone-100 dark:bg-stone-900 px-3" cmdk-input-wrapper=""> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> >(({ className, ...props }, ref) => (
<CommandPrimitive.Input <div
ref={ref} className="flex items-center bg-stone-100 dark:bg-stone-900 px-3"
className={cn( cmdk-input-wrapper=""
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", >
className, <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
)} <CommandPrimitive.Input
{...props} ref={ref}
/> className={cn(
</div> "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
), className,
) )}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<React.ElementRef<typeof CommandPrimitive.List>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>>( const CommandList = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof CommandPrimitive.List>,
<CommandPrimitive.List ref={ref} className={cn("max-h-[300px] mb-1 overflow-y-auto overflow-x-hidden", className)} {...props} /> React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
), >(({ className, ...props }, ref) => (
) <CommandPrimitive.List
ref={ref}
className={cn(
"max-h-[300px] mb-1 overflow-y-auto overflow-x-hidden",
className,
)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Empty>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>>( const CommandEmpty = React.forwardRef<
(props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />, React.ElementRef<typeof CommandPrimitive.Empty>,
) React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Group>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>>( const CommandGroup = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof CommandPrimitive.Group>,
<CommandPrimitive.Group React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
ref={ref} >(({ className, ...props }, ref) => (
className={cn( <CommandPrimitive.Group
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", ref={ref}
className, className={cn(
)} "overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
{...props} className,
/> )}
), {...props}
) />
));
CommandGroup.displayName = CommandPrimitive.Group.displayName CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef< const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>, React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => <CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />) >(({ className, ...props }, ref) => (
CommandSeparator.displayName = CommandPrimitive.Separator.displayName <CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Item>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>>( const CommandItem = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof CommandPrimitive.Item>,
<CommandPrimitive.Item React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
ref={ref} >(({ className, ...props }, ref) => (
className={cn( <CommandPrimitive.Item
"relative flex cursor-default gap-2 select-none items-center rounded-[8px] px-2 py-1.5 text-xs outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-stone-100 dark:data-[selected='true']:bg-stone-900 data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", ref={ref}
className, className={cn(
)} "relative flex cursor-default gap-2 select-none items-center rounded-[8px] px-2 py-1.5 text-xs outline-hidden data-[disabled=true]:pointer-events-none data-[selected='true']:bg-stone-100 dark:data-[selected='true']:bg-stone-900 data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{...props} className,
/> )}
), {...props}
) />
));
CommandItem.displayName = CommandPrimitive.Item.displayName CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { const CommandShortcut = ({
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} /> className,
} ...props
CommandShortcut.displayName = "CommandShortcut" }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className,
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator } export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};
+104 -60
View File
@@ -1,76 +1,120 @@
import { cn } from "@/lib/utils" import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as DialogPrimitive from "@radix-ui/react-dialog" import { X } from "lucide-react";
import { X } from "lucide-react" import * as React from "react";
import * as React from "react"
const Dialog = DialogPrimitive.Root import { cn } from "@/lib/utils";
const DialogTrigger = DialogPrimitive.Trigger const Dialog = DialogPrimitive.Root;
const DialogPortal = DialogPrimitive.Portal const DialogTrigger = DialogPrimitive.Trigger;
const DialogClose = DialogPrimitive.Close const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className, className,
)} )}
{...props} {...props}
/> />
)) ));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DialogPortal> <DialogPortal>
<DialogOverlay /> <DialogOverlay />
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)) ));
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogHeader = ({
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} /> className,
) ...props
DialogHeader.displayName = "DialogHeader" }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogFooter = ({
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} /> className,
) ...props
DialogFooter.displayName = "DialogFooter" }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Title>, React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>>( const DialogTitle = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof DialogPrimitive.Title>,
<DialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} /> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
), >(({ className, ...props }, ref) => (
) <DialogPrimitive.Title
DialogTitle.displayName = DialogPrimitive.Title.displayName ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => <DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />) >(({ className, ...props }, ref) => (
DialogDescription.displayName = DialogPrimitive.Description.displayName <DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription } export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
+165 -140
View File
@@ -1,172 +1,197 @@
import { cn } from "@/lib/utils" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import { Check, ChevronRight, Circle } from "lucide-react";
import { Check, ChevronRight, Circle } from "lucide-react" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef< const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, children, ...props }, ref) => ( >(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent", "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8", inset && "pl-8",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRight className="ml-auto h-4 w-4" /> <ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
)) ));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className, className,
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className, className,
)} )}
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
)) ));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef< const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>, React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default justify-between select-none items-center gap-2 rounded-[10px] px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "relative flex cursor-default justify-between select-none items-center gap-2 rounded-[10px] px-2 py-1.5 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8", inset && "pl-8",
className, className,
)} )}
{...props} {...props}
/> />
)) ));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef< const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => ( >(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className, className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
)) ));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className, className,
)} )}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" /> <Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
)) ));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef< const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>, React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)} {...props} /> <DropdownMenuPrimitive.Label
)) ref={ref}
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef< const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => <DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />) >(({ className, ...props }, ref) => (
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName <DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { const DropdownMenuShortcut = ({
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} /> className,
} ...props
DropdownMenuShortcut.displayName = "DropdownMenuShortcut" }: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export { export {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuRadioItem, DropdownMenuRadioItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuShortcut,
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuPortal, DropdownMenuPortal,
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuRadioGroup, DropdownMenuRadioGroup,
} };
+21 -18
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>(
return ( ({ className, type, ...props }, ref) => {
<input return (
type={type} <input
className={cn( type={type}
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50", className={cn(
className, "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
)} className,
ref={ref} )}
{...props} ref={ref}
/> {...props}
) />
}) );
Input.displayName = "Input" },
);
Input.displayName = "Input";
export { Input } export { Input };
+19 -10
View File
@@ -1,14 +1,23 @@
import { cn } from "@/lib/utils" import * as LabelPrimitive from "@radix-ui/react-label";
import * as LabelPrimitive from "@radix-ui/react-label" import { cva, type VariantProps } from "class-variance-authority";
import { type VariantProps, cva } from "class-variance-authority" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70") const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants> React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
>(({ className, ...props }, ref) => <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />) VariantProps<typeof labelVariants>
Label.displayName = LabelPrimitive.Root.displayName >(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label } export { Label };
+22 -22
View File
@@ -1,28 +1,28 @@
import { cn } from "@/lib/utils" import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as PopoverPrimitive from "@radix-ui/react-popover" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef< const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>, React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
<PopoverPrimitive.Content <PopoverPrimitive.Content
ref={ref} ref={ref}
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-2xl outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-2xl outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className, className,
)} )}
{...props} {...props}
/> />
</PopoverPrimitive.Portal> </PopoverPrimitive.Portal>
)) ));
PopoverContent.displayName = PopoverPrimitive.Content.displayName PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent } export { Popover, PopoverTrigger, PopoverContent };
+26 -16
View File
@@ -1,20 +1,30 @@
import { cn } from "@/lib/utils" import * as ProgressPrimitive from "@radix-ui/react-progress";
import * as ProgressPrimitive from "@radix-ui/react-progress" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Progress = React.forwardRef< const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>, React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & { React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
indicatorClassName?: string indicatorClassName?: string;
} }
>(({ className, value, indicatorClassName, ...props }, ref) => ( >(({ className, value, indicatorClassName, ...props }, ref) => (
<ProgressPrimitive.Root ref={ref} className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)} {...props}> <ProgressPrimitive.Root
<ProgressPrimitive.Indicator ref={ref}
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)} className={cn(
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} "relative h-4 w-full overflow-hidden rounded-full bg-secondary",
/> className,
</ProgressPrimitive.Root> )}
)) {...props}
Progress.displayName = ProgressPrimitive.Root.displayName >
<ProgressPrimitive.Indicator
className={cn(
"h-full w-full flex-1 bg-primary transition-all",
indicatorClassName,
)}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress } export { Progress };
+135 -104
View File
@@ -1,126 +1,157 @@
import { cn } from "@/lib/utils" import * as SelectPrimitive from "@radix-ui/react-select";
import * as SelectPrimitive from "@radix-ui/react-select" import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { Check, ChevronDown, ChevronUp } from "lucide-react" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef< const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
<SelectPrimitive.Icon asChild> <SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" /> <ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
)) ));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef< const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}> <SelectPrimitive.ScrollUpButton
<ChevronUp className="h-4 w-4" /> ref={ref}
</SelectPrimitive.ScrollUpButton> className={cn(
)) "flex cursor-default items-center justify-center py-1",
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName className,
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef< const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}> <SelectPrimitive.ScrollDownButton
<ChevronDown className="h-4 w-4" /> ref={ref}
</SelectPrimitive.ScrollDownButton> className={cn(
)) "flex cursor-default items-center justify-center py-1",
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName className,
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef< const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => ( >(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" && position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className, className,
)} )}
position={position} position={position}
{...props} {...props}
> >
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
className={cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")} className={cn(
> "p-1",
{children} position === "popper" &&
</SelectPrimitive.Viewport> "h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)",
<SelectScrollDownButton /> )}
</SelectPrimitive.Content> >
</SelectPrimitive.Portal> {children}
)) </SelectPrimitive.Viewport>
SelectContent.displayName = SelectPrimitive.Content.displayName <SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Label>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>>( const SelectLabel = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof SelectPrimitive.Label>,
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} /> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
), >(({ className, ...props }, ref) => (
) <SelectPrimitive.Label
SelectLabel.displayName = SelectPrimitive.Label.displayName ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>>( const SelectItem = React.forwardRef<
({ className, children, ...props }, ref) => ( React.ElementRef<typeof SelectPrimitive.Item>,
<SelectPrimitive.Item React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
ref={ref} >(({ className, children, ...props }, ref) => (
className={cn( <SelectPrimitive.Item
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", ref={ref}
className, className={cn(
)} "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
{...props} className,
> )}
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> {...props}
<SelectPrimitive.ItemIndicator> >
<Check className="h-4 w-4" /> <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
</SelectPrimitive.ItemIndicator> <SelectPrimitive.ItemIndicator>
</span> <Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
), ));
) SelectItem.displayName = SelectPrimitive.Item.displayName;
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef< const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>, React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => <SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />) >(({ className, ...props }, ref) => (
SelectSeparator.displayName = SelectPrimitive.Separator.displayName <SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export { export {
Select, Select,
SelectGroup, SelectGroup,
SelectValue, SelectValue,
SelectTrigger, SelectTrigger,
SelectContent, SelectContent,
SelectLabel, SelectLabel,
SelectItem, SelectItem,
SelectSeparator, SelectSeparator,
SelectScrollUpButton, SelectScrollUpButton,
SelectScrollDownButton, SelectScrollDownButton,
} };
+26 -16
View File
@@ -1,18 +1,28 @@
import { cn } from "@/lib/utils" import * as SeparatorPrimitive from "@radix-ui/react-separator";
import * as SeparatorPrimitive from "@radix-ui/react-separator" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Separator = React.forwardRef<React.ElementRef<typeof SeparatorPrimitive.Root>, React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>>( const Separator = React.forwardRef<
({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( React.ElementRef<typeof SeparatorPrimitive.Root>,
<SeparatorPrimitive.Root React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
ref={ref} >(
decorative={decorative} (
orientation={orientation} { className, orientation = "horizontal", decorative = true, ...props },
className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)} ref,
{...props} ) => (
/> <SeparatorPrimitive.Root
), ref={ref}
) decorative={decorative}
Separator.displayName = SeparatorPrimitive.Root.displayName orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
className,
)}
{...props}
/>
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator } export { Separator };
+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>) { function Skeleton({
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} /> className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
} }
export { Skeleton } export { Skeleton };
+24 -23
View File
@@ -1,25 +1,26 @@
import { cn } from "@/lib/utils" import * as SwitchPrimitives from "@radix-ui/react-switch";
import * as SwitchPrimitives from "@radix-ui/react-switch" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>, React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>>( const Switch = React.forwardRef<
({ className, ...props }, ref) => ( React.ElementRef<typeof SwitchPrimitives.Root>,
<SwitchPrimitives.Root React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
className={cn( >(({ className, ...props }, ref) => (
"peer inline-flex h-3 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", <SwitchPrimitives.Root
className, className={cn(
)} "peer inline-flex h-3 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
{...props} className,
ref={ref} )}
> {...props}
<SwitchPrimitives.Thumb ref={ref}
className={cn( >
"pointer-events-none block h-2 w-2 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0", <SwitchPrimitives.Thumb
)} className={cn(
/> "pointer-events-none block h-2 w-2 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0",
</SwitchPrimitives.Root> )}
), />
) </SwitchPrimitives.Root>
Switch.displayName = SwitchPrimitives.Root.displayName ));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch } export { Switch };
+107 -41
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<
<div className="relative w-full overflow-auto"> HTMLTableElement,
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} /> React.HTMLAttributes<HTMLTableElement>
</div> >(({ className, ...props }, ref) => (
)) <div className="relative w-full overflow-auto">
Table.displayName = "Table" <table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => ( const TableHeader = React.forwardRef<
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> HTMLTableSectionElement,
)) React.HTMLAttributes<HTMLTableSectionElement>
TableHeader.displayName = "TableHeader" >(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => ( const TableBody = React.forwardRef<
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} /> HTMLTableSectionElement,
)) React.HTMLAttributes<HTMLTableSectionElement>
TableBody.displayName = "TableBody" >(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => ( const TableFooter = React.forwardRef<
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} /> HTMLTableSectionElement,
)) React.HTMLAttributes<HTMLTableSectionElement>
TableFooter.displayName = "TableFooter" >(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium last:[&>tr]:border-b-0",
className,
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(({ className, ...props }, ref) => ( const TableRow = React.forwardRef<
<tr ref={ref} className={cn("border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", className)} {...props} /> HTMLTableRowElement,
)) React.HTMLAttributes<HTMLTableRowElement>
TableRow.displayName = "TableRow" >(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => ( const TableHead = React.forwardRef<
<th HTMLTableCellElement,
ref={ref} React.ThHTMLAttributes<HTMLTableCellElement>
className={cn("h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", className)} >(({ className, ...props }, ref) => (
{...props} <th
/> ref={ref}
)) className={cn(
TableHead.displayName = "TableHead" "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => ( const TableCell = React.forwardRef<
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} /> HTMLTableCellElement,
)) React.TdHTMLAttributes<HTMLTableCellElement>
TableCell.displayName = "TableCell" >(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(({ className, ...props }, ref) => ( const TableCaption = React.forwardRef<
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} /> HTMLTableCaptionElement,
)) React.HTMLAttributes<HTMLTableCaptionElement>
TableCaption.displayName = "TableCaption" >(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};
+20 -20
View File
@@ -1,27 +1,27 @@
import { cn } from "@/lib/utils" import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import * as React from "react";
import * as React from "react" import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef< const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>, React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content <TooltipPrimitive.Content
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 overflow-hidden rounded-[8px] border font-medium bg-popover px-1.5 py-0.5 text-xs text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 overflow-hidden rounded-[8px] border font-medium bg-popover px-1.5 py-0.5 text-xs text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className, className,
)} )}
{...props} {...props}
/> />
)) ));
TooltipContent.displayName = TooltipPrimitive.Content.displayName TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+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 { export interface SortContextType {
sortType: SortType sortType: SortType;
sortOrder: SortOrder sortOrder: SortOrder;
setSortType: (sortType: SortType) => void setSortType: (sortType: SortType) => void;
setSortOrder: (sortOrder: SortOrder) => void setSortOrder: (sortOrder: SortOrder) => void;
} }
export const SortContext = createContext<SortContextType | undefined>(undefined) export const SortContext = createContext<SortContextType | undefined>(
undefined,
);
+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 }) { export function SortProvider({ children }: { children: ReactNode }) {
const [sortType, setSortType] = useState<SortType>("default") const [sortType, setSortType] = useState<SortType>("default");
const [sortOrder, setSortOrder] = useState<SortOrder>("desc") const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
return <SortContext.Provider value={{ sortType, setSortType, sortOrder, setSortOrder }}>{children}</SortContext.Provider> return (
<SortContext.Provider
value={{ sortType, setSortType, sortOrder, setSortOrder }}
>
{children}
</SortContext.Provider>
);
} }
+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 { export interface StatusContextType {
status: Status status: Status;
setStatus: (status: Status) => void setStatus: (status: Status) => void;
} }
export const StatusContext = createContext<StatusContextType | undefined>(undefined) export const StatusContext = createContext<StatusContextType | undefined>(
undefined,
);
+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 }) { 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>
);
} }
+14 -11
View File
@@ -1,18 +1,21 @@
import { createContext } from "react" import { createContext } from "react";
export interface TooltipData { export interface TooltipData {
centroid: [number, number] centroid: [number, number];
country: string country: string;
count: number count: number;
servers: Array<{ servers: Array<{
name: string id: number;
status: boolean name: string;
}> status: boolean;
}>;
} }
interface TooltipContextType { interface TooltipContextType {
tooltipData: TooltipData | null tooltipData: TooltipData | null;
setTooltipData: (data: TooltipData | null) => void setTooltipData: (data: TooltipData | null) => void;
} }
export const TooltipContext = createContext<TooltipContextType | undefined>(undefined) export const TooltipContext = createContext<TooltipContextType | undefined>(
undefined,
);
+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 }) { 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>
);
} }
+14 -14
View File
@@ -1,19 +1,19 @@
import { createContext } from "react" import { createContext } from "react";
export interface WebSocketContextType { export interface WebSocketContextType {
lastMessage: { data: string } | null lastMessage: { data: string } | null;
connected: boolean connected: boolean;
messageHistory: { data: string }[] messageHistory: { data: string }[];
reconnect: () => void reconnect: () => void;
needReconnect: boolean needReconnect: boolean;
setNeedReconnect: (needReconnect: boolean) => void setNeedReconnect: (needReconnect: boolean) => void;
} }
export const WebSocketContext = createContext<WebSocketContextType>({ export const WebSocketContext = createContext<WebSocketContextType>({
lastMessage: null, lastMessage: null,
connected: false, connected: false,
messageHistory: [], messageHistory: [],
reconnect: () => {}, reconnect: () => {},
needReconnect: false, needReconnect: false,
setNeedReconnect: () => {}, setNeedReconnect: () => {},
}) });
+123 -109
View File
@@ -1,132 +1,146 @@
import React, { useEffect, useRef, useState } from "react" import type React from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { WebSocketContext, WebSocketContextType } from "./websocket-context" import {
WebSocketContext,
type WebSocketContextType,
} from "./websocket-context";
interface WebSocketProviderProps { interface WebSocketProviderProps {
url: string url: string;
children: React.ReactNode children: React.ReactNode;
} }
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, children }) => { export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null) url,
const [messageHistory, setMessageHistory] = useState<{ data: string }[]>([]) // 新增历史消息状态 children,
const [connected, setConnected] = useState(false) }) => {
const [needReconnect, setNeedReconnect] = useState(false) const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null);
const ws = useRef<WebSocket | null>(null) const [messageHistory, setMessageHistory] = useState<{ data: string }[]>([]); // 新增历史消息状态
const reconnectTimeout = useRef<NodeJS.Timeout>(null) const [connected, setConnected] = useState(false);
const maxReconnectAttempts = 30 const [needReconnect, setNeedReconnect] = useState(false);
const reconnectAttempts = useRef(0) const ws = useRef<WebSocket | null>(null);
const isConnecting = useRef(false) const reconnectTimeout = useRef<NodeJS.Timeout>(null);
const maxReconnectAttempts = 30;
const reconnectAttempts = useRef(0);
const isConnecting = useRef(false);
const cleanup = () => { const cleanup = useCallback(() => {
if (ws.current) { if (ws.current) {
// 移除所有事件监听器 // 移除所有事件监听器
ws.current.onopen = null ws.current.onopen = null;
ws.current.onclose = null ws.current.onclose = null;
ws.current.onmessage = null ws.current.onmessage = null;
ws.current.onerror = null ws.current.onerror = null;
if (ws.current.readyState === WebSocket.OPEN || ws.current.readyState === WebSocket.CONNECTING) { if (
ws.current.close() ws.current.readyState === WebSocket.OPEN ||
} ws.current.readyState === WebSocket.CONNECTING
ws.current = null ) {
} ws.current.close();
if (reconnectTimeout.current) { }
clearTimeout(reconnectTimeout.current) ws.current = null;
reconnectTimeout.current = null }
} if (reconnectTimeout.current) {
setConnected(false) clearTimeout(reconnectTimeout.current);
} reconnectTimeout.current = null;
}
setConnected(false);
}, []);
const connect = () => { const connect = useCallback(() => {
if (isConnecting.current) { if (isConnecting.current) {
console.log("Connection already in progress") console.log("Connection already in progress");
return return;
} }
cleanup() cleanup();
isConnecting.current = true isConnecting.current = true;
try { try {
const wsUrl = new URL(url, window.location.origin) const wsUrl = new URL(url, window.location.origin);
wsUrl.protocol = wsUrl.protocol.replace("http", "ws") wsUrl.protocol = wsUrl.protocol.replace("http", "ws");
ws.current = new WebSocket(wsUrl.toString()) ws.current = new WebSocket(wsUrl.toString());
ws.current.onopen = () => { ws.current.onopen = () => {
console.log("WebSocket connected") console.log("WebSocket connected");
setConnected(true) setConnected(true);
reconnectAttempts.current = 0 reconnectAttempts.current = 0;
isConnecting.current = false isConnecting.current = false;
} };
ws.current.onclose = () => { ws.current.onclose = () => {
console.log("WebSocket disconnected") console.log("WebSocket disconnected");
setConnected(false) setConnected(false);
ws.current = null ws.current = null;
isConnecting.current = false isConnecting.current = false;
if (reconnectAttempts.current < maxReconnectAttempts) { if (reconnectAttempts.current < maxReconnectAttempts) {
reconnectTimeout.current = setTimeout(() => { reconnectTimeout.current = setTimeout(() => {
reconnectAttempts.current++ reconnectAttempts.current++;
connect() connect();
}, 3000) }, 3000);
} }
} };
ws.current.onmessage = (event) => { ws.current.onmessage = (event) => {
const newMessage = { data: event.data } const newMessage = { data: event.data };
setLastMessage(newMessage) setLastMessage(newMessage);
// 更新历史消息,保持最新的30条记录 // 更新历史消息,保持最新的30条记录
setMessageHistory((prev) => { setMessageHistory((prev) => {
const updated = [newMessage, ...prev] const updated = [newMessage, ...prev];
return updated.slice(0, 30) return updated.slice(0, 30);
}) });
} };
ws.current.onerror = (error) => { ws.current.onerror = (error) => {
console.error("WebSocket error:", error) console.error("WebSocket error:", error);
isConnecting.current = false isConnecting.current = false;
} };
} catch (error) { } catch (error) {
console.error("WebSocket connection error:", error) console.error("WebSocket connection error:", error);
isConnecting.current = false isConnecting.current = false;
} }
} }, [cleanup, url]);
const reconnect = () => { const reconnect = () => {
reconnectAttempts.current = 0 reconnectAttempts.current = 0;
// 等待一个小延时确保清理完成 // 等待一个小延时确保清理完成
cleanup() cleanup();
setTimeout(() => { setTimeout(() => {
connect() connect();
}, 1000) }, 1000);
} };
useEffect(() => { useEffect(() => {
connect() connect();
// 添加页面卸载事件监听 // 添加页面卸载事件监听
const handleBeforeUnload = () => { const handleBeforeUnload = () => {
cleanup() cleanup();
} };
window.addEventListener("beforeunload", handleBeforeUnload) window.addEventListener("beforeunload", handleBeforeUnload);
return () => { return () => {
cleanup() cleanup();
window.removeEventListener("beforeunload", handleBeforeUnload) window.removeEventListener("beforeunload", handleBeforeUnload);
} };
}, [url]) }, [cleanup, connect]);
const contextValue: WebSocketContextType = { const contextValue: WebSocketContextType = {
lastMessage, lastMessage,
connected, connected,
messageHistory, messageHistory,
reconnect, reconnect,
needReconnect, needReconnect,
setNeedReconnect, setNeedReconnect,
} };
return <WebSocketContext.Provider value={contextValue}>{children}</WebSocketContext.Provider> return (
} <WebSocketContext.Provider value={contextValue}>
{children}
</WebSocketContext.Provider>
);
};
+54 -46
View File
@@ -1,60 +1,68 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react";
declare global { declare global {
interface Window { interface Window {
CustomBackgroundImage: string CustomBackgroundImage: string;
CustomMobileBackgroundImage: string CustomMobileBackgroundImage: string;
ForceShowServices: boolean ForceShowServices: boolean;
ForceCardInline: boolean ForceCardInline: boolean;
ForceShowMap: boolean ForceShowMap: boolean;
ForcePeakCutEnabled: boolean ForcePeakCutEnabled: boolean;
} }
} }
const BACKGROUND_CHANGE_EVENT = "backgroundChange" const BACKGROUND_CHANGE_EVENT = "backgroundChange";
export function useBackground() { export function useBackground() {
const [backgroundImage, setBackgroundImage] = useState<string | undefined>(undefined) const [backgroundImage, setBackgroundImage] = useState<string | undefined>(
undefined,
);
useEffect(() => { useEffect(() => {
// 监听背景变化 // 监听背景变化
const handleBackgroundChange = () => { const handleBackgroundChange = () => {
setBackgroundImage(window.CustomBackgroundImage || undefined) setBackgroundImage(window.CustomBackgroundImage || undefined);
} };
// 初始化检查 // 初始化检查
const checkInitialBackground = () => { const checkInitialBackground = () => {
if (window.CustomBackgroundImage) { if (window.CustomBackgroundImage) {
setBackgroundImage(window.CustomBackgroundImage) setBackgroundImage(window.CustomBackgroundImage);
} else { } else {
const savedImage = sessionStorage.getItem("savedBackgroundImage") const savedImage = sessionStorage.getItem("savedBackgroundImage");
if (savedImage) { if (savedImage) {
window.CustomBackgroundImage = savedImage window.CustomBackgroundImage = savedImage;
setBackgroundImage(savedImage) setBackgroundImage(savedImage);
} }
} }
} };
// 设置一个轮询来检查初始背景 // 设置一个轮询来检查初始背景
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
if (window.CustomBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) { if (
checkInitialBackground() window.CustomBackgroundImage ||
clearInterval(intervalId) sessionStorage.getItem("savedBackgroundImage")
} ) {
}, 100) checkInitialBackground();
clearInterval(intervalId);
}
}, 100);
window.addEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange) window.addEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange);
return () => { return () => {
window.removeEventListener(BACKGROUND_CHANGE_EVENT, handleBackgroundChange) window.removeEventListener(
clearInterval(intervalId) BACKGROUND_CHANGE_EVENT,
} handleBackgroundChange,
}, []) );
clearInterval(intervalId);
};
}, []);
const updateBackground = (newBackground: string | undefined) => { const updateBackground = (newBackground: string | undefined) => {
window.CustomBackgroundImage = newBackground || "" window.CustomBackgroundImage = newBackground || "";
window.dispatchEvent(new Event(BACKGROUND_CHANGE_EVENT)) window.dispatchEvent(new Event(BACKGROUND_CHANGE_EVENT));
} };
return { backgroundImage, updateBackground } return { backgroundImage, updateBackground };
} }
+19 -19
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>( export function useChartHistory<T>(
messageHistory: { data: string }[], messageHistory: { data: string }[],
serverId: number, serverId: number,
formatFn: (wsData: NezhaWebsocketResponse, serverId: number) => T | null, formatFn: (wsData: NezhaWebsocketResponse, serverId: number) => T | null,
) { ) {
const [data, setData] = useState<T[]>([]) const [data, setData] = useState<T[]>([]);
useEffect(() => { useEffect(() => {
if (messageHistory.length > 0 && data.length === 0) { if (messageHistory.length > 0 && data.length === 0) {
const historyData = messageHistory const historyData = messageHistory
.map((msg) => { .map((msg) => {
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse;
return formatFn(wsData, serverId) return formatFn(wsData, serverId);
}) })
.filter((item): item is T => item !== null) .filter((item): item is T => item !== null)
.reverse() .reverse();
setData(historyData) setData(historyData);
} }
}, [messageHistory]) }, [messageHistory, data.length, formatFn, serverId]);
return data return data;
} }
+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;
}
+7 -7
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() { export function useSort() {
const context = useContext(SortContext) const context = useContext(SortContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useStatus must be used within a SortProvider") throw new Error("useStatus must be used within a SortProvider");
} }
return context return context;
} }
+7 -7
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() { export function useStatus() {
const context = useContext(StatusContext) const context = useContext(StatusContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useStatus must be used within a StatusProvider") throw new Error("useStatus must be used within a StatusProvider");
} }
return context return context;
} }
+8 -8
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 = () => { export const useTheme = () => {
const context = useContext(ThemeProviderContext) const context = useContext(ThemeProviderContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider") throw new Error("useTheme must be used within a ThemeProvider");
} }
return context return context;
} };
+9 -9
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 = () => { export const useTooltip = () => {
const context = useContext(TooltipContext) const context = useContext(TooltipContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useTooltip must be used within a TooltipProvider") throw new Error("useTooltip must be used within a TooltipProvider");
} }
return context return context;
} };
export default useTooltip export default useTooltip;
+10 -8
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 = () => { export const useWebSocketContext = () => {
const context = useContext(WebSocketContext) const context = useContext(WebSocketContext);
if (context === undefined) { if (context === undefined) {
throw new Error("useWebSocketContext must be used within a WebSocketProvider") throw new Error(
} "useWebSocketContext must be used within a WebSocketProvider",
return context );
} }
return context;
};
+43 -43
View File
@@ -1,54 +1,54 @@
import i18n from "i18next" import i18n from "i18next";
import { initReactI18next } from "react-i18next" import { initReactI18next } from "react-i18next";
import deTranslation from "./locales/de/translation.json" import deTranslation from "./locales/de/translation.json";
import enTranslation from "./locales/en/translation.json" import enTranslation from "./locales/en/translation.json";
import esTranslation from "./locales/es/translation.json" import esTranslation from "./locales/es/translation.json";
import ruTranslation from "./locales/ru/translation.json" import ruTranslation from "./locales/ru/translation.json";
import taTranslation from "./locales/ta/translation.json" import taTranslation from "./locales/ta/translation.json";
import zhCNTranslation from "./locales/zh-CN/translation.json" import zhCNTranslation from "./locales/zh-CN/translation.json";
import zhTWTranslation from "./locales/zh-TW/translation.json" import zhTWTranslation from "./locales/zh-TW/translation.json";
const resources = { const resources = {
"en-US": { "en-US": {
translation: enTranslation, translation: enTranslation,
}, },
"zh-CN": { "zh-CN": {
translation: zhCNTranslation, translation: zhCNTranslation,
}, },
"zh-TW": { "zh-TW": {
translation: zhTWTranslation, translation: zhTWTranslation,
}, },
"de-DE": { "de-DE": {
translation: deTranslation, translation: deTranslation,
}, },
"es-ES": { "es-ES": {
translation: esTranslation, translation: esTranslation,
}, },
"ru-RU": { "ru-RU": {
translation: ruTranslation, translation: ruTranslation,
}, },
"ta-IN": { "ta-IN": {
translation: taTranslation, translation: taTranslation,
}, },
} };
const getStoredLanguage = () => { const getStoredLanguage = () => {
return localStorage.getItem("language") || "en-US" return localStorage.getItem("language") || "en-US";
} };
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({
resources, resources,
lng: getStoredLanguage(), // 使用localStorage中存储的语言或默认值 lng: getStoredLanguage(), // 使用localStorage中存储的语言或默认值
fallbackLng: "en-US", // 当前语言的翻译没有找到时,使用的备选语言 fallbackLng: "en-US", // 当前语言的翻译没有找到时,使用的备选语言
interpolation: { interpolation: {
escapeValue: false, // react已经安全地转义 escapeValue: false, // react已经安全地转义
}, },
}) });
// 添加语言改变时的处理函数 // 添加语言改变时的处理函数
i18n.on("languageChanged", (lng) => { i18n.on("languageChanged", (lng) => {
localStorage.setItem("language", lng) localStorage.setItem("language", lng);
}) });
export default i18n export default i18n;
+337 -164
View File
@@ -1,234 +1,407 @@
@tailwind base; @import "tailwindcss";
@tailwind components; @import url("https://cdn.jsdelivr.net/npm/lxgw-wenkai-screen-webfont@1.7.0/style.css");
@tailwind utilities;
:root { @plugin "tailwindcss-animate";
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark; @custom-variant dark (&:is(.dark *));
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none; @theme {
text-rendering: optimizeLegibility; --font-sans: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; --radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-chart-1: hsl(var(--chart-1));
--color-chart-2: hsl(var(--chart-2));
--color-chart-3: hsl(var(--chart-3));
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
} }
@layer base { /*
:root { The default border color has changed to `currentcolor` in Tailwind CSS v4,
--background: 0 0% 98%; so we've added these compatibility styles to make sure everything still
--foreground: 20 14.3% 4.1%; looks the same as it did with Tailwind CSS v3.
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 24 9.8% 10%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%;
--radius: 1rem;
--chart-1: 220 70% 50%;
--chart-2: 340 75% 55%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 160 60% 45%;
--chart-6: 180 50% 50%;
--chart-7: 216 50% 50%;
--chart-8: 252 50% 50%;
--chart-9: 288 50% 50%;
--chart-10: 324 50% 50%;
}
.dark { If we ever want to remove these styles, we need to add an explicit border
--background: 30 15% 8%; color utility to any element that depends on these defaults.
--foreground: 60 9.1% 97.8%; */
--card: 20 14.3% 4.1%; @layer base {
--card-foreground: 60 9.1% 97.8%; *,
--popover: 20 14.3% 4.1%; ::after,
--popover-foreground: 60 9.1% 97.8%; ::before,
--primary: 60 9.1% 97.8%; ::backdrop,
--primary-foreground: 24 9.8% 10%; ::file-selector-button {
--secondary: 12 6.5% 15.1%; border-color: var(--color-gray-200, currentcolor);
--secondary-foreground: 60 9.1% 97.8%; }
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 24 5.7% 82.9%;
--chart-1: 220 70% 50%;
--chart-2: 340 75% 55%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 160 60% 45%;
--chart-6: 180 50% 50%;
--chart-7: 216 50% 50%;
--chart-8: 252 50% 50%;
--chart-9: 288 50% 50%;
--chart-10: 324 50% 50%;
}
} }
@layer base { @utility step {
* { counter-increment: step;
@apply border-border;
} &:before {
html { @apply border-background bg-muted absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 text-center -indent-px font-mono text-base font-medium;
@apply scroll-smooth; @apply mt-[-4px] ml-[-50px];
} content: counter(step);
body { }
@apply bg-background text-foreground;
/* font-feature-settings: "rlig" 1, "calt" 1; */
font-synthesis-weight: none;
text-rendering: optimizeLegibility;
}
} }
@layer utilities { @layer utilities {
.step { :root {
counter-increment: step; font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
} line-height: 1.5;
font-weight: 400;
.step:before { color-scheme: light dark;
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium; color: rgba(255, 255, 255, 0.87);
@apply ml-[-50px] mt-[-4px]; background-color: #242424;
content: counter(step);
} font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer base {
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}
@layer base {
:root {
--background: 0 0% 98%;
--foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 24 9.8% 10%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%;
--radius: 1rem;
--chart-1: 220 70% 50%;
--chart-2: 340 75% 55%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 160 60% 45%;
--chart-6: 180 50% 50%;
--chart-7: 216 50% 50%;
--chart-8: 252 50% 50%;
--chart-9: 288 50% 50%;
--chart-10: 324 50% 50%;
--timing: cubic-bezier(0.4, 0, 0.2, 1);
}
.dark {
--background: 30 15% 8%;
--foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 97.8%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--primary: 60 9.1% 97.8%;
--primary-foreground: 24 9.8% 10%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 24 5.7% 82.9%;
--chart-1: 220 70% 50%;
--chart-2: 340 75% 55%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 160 60% 45%;
--chart-6: 180 50% 50%;
--chart-7: 216 50% 50%;
--chart-8: 252 50% 50%;
--chart-9: 288 50% 50%;
--chart-10: 324 50% 50%;
}
}
@layer base {
* {
@apply border-border;
}
html {
@apply scroll-smooth;
}
body {
@apply bg-background text-foreground;
/* font-feature-settings: "rlig" 1, "calt" 1; */
font-synthesis-weight: none;
text-rendering: optimizeLegibility;
font-family: 'LXGW WenKai Screen', sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'LXGW WenKai Screen', sans-serif;
}
}
@layer base {
/* Avoid color fade when toggling themes. */
html.disable-transitions *,
html.disable-transitions *::before,
html.disable-transitions *::after {
transition: none;
}
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.container { .container {
@apply px-4; @apply px-4;
} }
} }
::selection { ::selection {
@apply bg-stone-300 dark:bg-stone-800; @apply bg-stone-300 dark:bg-stone-800;
} }
.hamster-loading-wrapper { .hamster-loading-wrapper {
--size: 12px; --size: 12px;
height: var(--size); height: var(--size);
width: var(--size); width: var(--size);
inset: 0; inset: 0;
z-index: 10; z-index: 10;
} }
.hamster-loading-wrapper[data-visible="false"] { .hamster-loading-wrapper[data-visible="false"] {
transform-origin: center; transform-origin: center;
animation: hamster-fade-out 0.2s ease forwards; animation: hamster-fade-out 0.2s ease forwards;
} }
.hamster-spinner { .hamster-spinner {
position: relative; position: relative;
top: 50%; top: 50%;
left: 50%; left: 50%;
height: var(--size); height: var(--size);
width: var(--size); width: var(--size);
} }
.hamster-loading-bar { .hamster-loading-bar {
--gray11: hsl(0, 0%, 43.5%); --gray11: hsl(0, 0%, 43.5%);
animation: hamster-spin 0.8s linear infinite; animation: hamster-spin 0.8s linear infinite;
background: var(--gray11); background: var(--gray11);
border-radius: 6px; border-radius: 6px;
height: 13%; height: 13%;
left: -10%; left: -10%;
position: absolute; position: absolute;
top: -3.9%; top: -3.9%;
width: 30%; width: 30%;
} }
.hamster-loading-bar:nth-child(1) { .hamster-loading-bar:nth-child(1) {
animation-delay: -0.8s; animation-delay: -0.8s;
transform: rotate(0deg) translate(120%); transform: rotate(0deg) translate(120%);
} }
.hamster-loading-bar:nth-child(2) { .hamster-loading-bar:nth-child(2) {
animation-delay: -0.7s; animation-delay: -0.7s;
transform: rotate(45deg) translate(120%); transform: rotate(45deg) translate(120%);
} }
.hamster-loading-bar:nth-child(3) { .hamster-loading-bar:nth-child(3) {
animation-delay: -0.6s; animation-delay: -0.6s;
transform: rotate(90deg) translate(120%); transform: rotate(90deg) translate(120%);
} }
.hamster-loading-bar:nth-child(4) { .hamster-loading-bar:nth-child(4) {
animation-delay: -0.5s; animation-delay: -0.5s;
transform: rotate(135deg) translate(120%); transform: rotate(135deg) translate(120%);
} }
.hamster-loading-bar:nth-child(5) { .hamster-loading-bar:nth-child(5) {
animation-delay: -0.4s; animation-delay: -0.4s;
transform: rotate(180deg) translate(120%); transform: rotate(180deg) translate(120%);
} }
.hamster-loading-bar:nth-child(6) { .hamster-loading-bar:nth-child(6) {
animation-delay: -0.3s; animation-delay: -0.3s;
transform: rotate(225deg) translate(120%); transform: rotate(225deg) translate(120%);
} }
.hamster-loading-bar:nth-child(7) { .hamster-loading-bar:nth-child(7) {
animation-delay: -0.2s; animation-delay: -0.2s;
transform: rotate(270deg) translate(120%); transform: rotate(270deg) translate(120%);
} }
.hamster-loading-bar:nth-child(8) { .hamster-loading-bar:nth-child(8) {
animation-delay: -0.1s; animation-delay: -0.1s;
transform: rotate(315deg) translate(120%); transform: rotate(315deg) translate(120%);
} }
@keyframes hamster-fade-in { @keyframes hamster-fade-in {
0% { 0% {
opacity: 0; opacity: 0;
transform: scale(0.8); transform: scale(0.8);
} }
100% { 100% {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
} }
@keyframes hamster-fade-out { @keyframes hamster-fade-out {
0% { 0% {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
100% { 100% {
opacity: 0; opacity: 0;
transform: scale(0.8); transform: scale(0.8);
} }
} }
@keyframes hamster-spin { @keyframes hamster-spin {
0% { 0% {
opacity: 1; opacity: 1;
} }
100% { 100% {
opacity: 0.15; opacity: 0.15;
} }
} }
.scrollbar-hidden { .scrollbar-hidden {
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
} }
.scrollbar-hidden::-webkit-scrollbar { .scrollbar-hidden::-webkit-scrollbar {
display: none; /* Chrome, Safari 和 Opera */ display: none; /* Chrome, Safari 和 Opera */
}
/* Thanks to next.js. */
[data-issues-count-animation] {
display: flex;
justify-content: center;
align-items: center;
}
[data-issues-count-animation] > div {
text-align: center;
}
[data-issues-count-exit].animate {
animation: fadeOut 300ms var(--timing) forwards;
}
[data-issues-count-enter].animate {
animation: fadeIn 300ms var(--timing) forwards;
}
[data-issues-count-plural] {
display: inline-block;
animation: fadeIn 300ms var(--timing) forwards;
}
@keyframes fadeIn {
0% {
opacity: 0;
filter: blur(2px);
transform: translateY(8px);
}
100% {
opacity: 1;
filter: blur(0px);
transform: translateY(0);
}
}
@keyframes fadeOut {
0% {
opacity: 1;
filter: blur(0px);
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-12px);
filter: blur(2px);
}
}
/* Custom background overlays */
.bg-cover::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
transition: backdrop-filter 0.3s ease, background 0.3s ease;
}
.dark .bg-cover::after {
backdrop-filter: blur(8px);
background: rgba(0, 0, 0, .6);
}
.light .bg-cover::after {
backdrop-filter: blur(0);
background: rgba(255, 255, 255, .3);
} }
+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) { export function formatBytes(bytes: number, decimals: number = 2) {
if (!+bytes) return "0 Bytes" if (!+bytes) return "0 Bytes";
const k = 1024 const k = 1024;
const dm = decimals < 0 ? 0 : decimals const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] const sizes = [
"Bytes",
"KiB",
"MiB",
"GiB",
"TiB",
"PiB",
"EiB",
"ZiB",
"YiB",
];
const i = Math.floor(Math.log(bytes) / Math.log(k)) const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}` return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
} }
File diff suppressed because one or more lines are too long
+207 -203
View File
@@ -1,208 +1,212 @@
export const countryCoordinates: Record<string, { lat: number; lng: number; name: string }> = { export const countryCoordinates: Record<
// 亚洲 string,
AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗 { lat: number; lng: number; name: string }
AM: { lat: 40.0, lng: 45.0, name: "Armenia" }, // 亚美尼亚 > = {
AZ: { lat: 40.5, lng: 47.5, name: "Azerbaijan" }, // 阿塞拜疆 // 亚洲
BD: { lat: 24.0, lng: 90.0, name: "Bangladesh" }, // 孟加拉国 AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗
BH: { lat: 26.0, lng: 50.55, name: "Bahrain" }, // 巴林 AM: { lat: 40.0, lng: 45.0, name: "Armenia" }, // 亚美尼亚
BT: { lat: 27.5, lng: 90.5, name: "Bhutan" }, // 不丹 AZ: { lat: 40.5, lng: 47.5, name: "Azerbaijan" }, // 阿塞拜疆
BN: { lat: 4.5, lng: 114.6667, name: "Brunei" }, // 文莱 BD: { lat: 24.0, lng: 90.0, name: "Bangladesh" }, // 孟加拉国
KH: { lat: 13.0, lng: 105.0, name: "Cambodia" }, // 柬埔寨 BH: { lat: 26.0, lng: 50.55, name: "Bahrain" }, // 巴林
CN: { lat: 35.0, lng: 105.0, name: "China" }, // 中国 BT: { lat: 27.5, lng: 90.5, name: "Bhutan" }, // 不丹
HK: { lat: 22.0, lng: 114.0, name: "Hong Kong" }, // 香港 BN: { lat: 4.5, lng: 114.6667, name: "Brunei" }, // 文莱
CY: { lat: 35.0, lng: 33.0, name: "Cyprus" }, // 塞浦路斯 KH: { lat: 13.0, lng: 105.0, name: "Cambodia" }, // 柬埔寨
GE: { lat: 42.0, lng: 43.5, name: "Georgia" }, // 格鲁吉亚 CN: { lat: 35.0, lng: 105.0, name: "China" }, // 中国
IN: { lat: 20.0, lng: 77.0, name: "India" }, // 印度 HK: { lat: 22.0, lng: 114.0, name: "Hong Kong" }, // 香港
ID: { lat: -5.0, lng: 120.0, name: "Indonesia" }, // 印度尼西亚 MO: { lat: 22.1667, lng: 113.55, name: "Macau" }, // 澳门
IR: { lat: 32.0, lng: 53.0, name: "Iran" }, // 伊朗 CY: { lat: 35.0, lng: 33.0, name: "Cyprus" }, // 塞浦路斯
IQ: { lat: 33.0, lng: 44.0, name: "Iraq" }, // 伊拉克 GE: { lat: 42.0, lng: 43.5, name: "Georgia" }, // 格鲁吉亚
IL: { lat: 31.5, lng: 34.75, name: "Israel" }, // 以色列 IN: { lat: 20.0, lng: 77.0, name: "India" }, // 印度
JP: { lat: 36.0, lng: 138.0, name: "Japan" }, // 日本 ID: { lat: -5.0, lng: 120.0, name: "Indonesia" }, // 印度尼西亚
JO: { lat: 31.0, lng: 36.0, name: "Jordan" }, // 约旦 IR: { lat: 32.0, lng: 53.0, name: "Iran" }, // 伊朗
KZ: { lat: 48.0, lng: 68.0, name: "Kazakhstan" }, // 哈萨克斯坦 IQ: { lat: 33.0, lng: 44.0, name: "Iraq" }, // 伊拉克
KW: { lat: 29.3375, lng: 47.6581, name: "Kuwait" }, // 科威特 IL: { lat: 31.5, lng: 34.75, name: "Israel" }, // 以色列
KG: { lat: 41.0, lng: 75.0, name: "Kyrgyzstan" }, // 吉尔吉斯斯坦 JP: { lat: 36.0, lng: 138.0, name: "Japan" }, // 日本
LA: { lat: 18.0, lng: 105.0, name: "Laos" }, // 老挝 JO: { lat: 31.0, lng: 36.0, name: "Jordan" }, // 约旦
LB: { lat: 33.8333, lng: 35.8333, name: "Lebanon" }, // 黎巴嫩 KZ: { lat: 48.0, lng: 68.0, name: "Kazakhstan" }, // 哈萨克斯坦
MY: { lat: 2.5, lng: 112.5, name: "Malaysia" }, // 马来西亚 KW: { lat: 29.3375, lng: 47.6581, name: "Kuwait" }, // 科威特
MV: { lat: 3.25, lng: 73.0, name: "Maldives" }, // 马尔代夫 KG: { lat: 41.0, lng: 75.0, name: "Kyrgyzstan" }, // 吉尔吉斯斯坦
MN: { lat: 46.0, lng: 105.0, name: "Mongolia" }, // 蒙古 LA: { lat: 18.0, lng: 105.0, name: "Laos" }, // 老挝
MM: { lat: 22.0, lng: 98.0, name: "Myanmar" }, // 缅甸 LB: { lat: 33.8333, lng: 35.8333, name: "Lebanon" }, // 黎巴嫩
NP: { lat: 28.0, lng: 84.0, name: "Nepal" }, // 尼泊尔 MY: { lat: 2.5, lng: 112.5, name: "Malaysia" }, // 马来西亚
OM: { lat: 21.0, lng: 57.0, name: "Oman" }, // 阿曼 MV: { lat: 3.25, lng: 73.0, name: "Maldives" }, // 马尔代夫
PK: { lat: 30.0, lng: 70.0, name: "Pakistan" }, // 巴基斯坦 MN: { lat: 46.0, lng: 105.0, name: "Mongolia" }, // 蒙古
PH: { lat: 13.0, lng: 122.0, name: "Philippines" }, // 菲律宾 MM: { lat: 22.0, lng: 98.0, name: "Myanmar" }, // 缅甸
QA: { lat: 25.5, lng: 51.25, name: "Qatar" }, // 卡塔 NP: { lat: 28.0, lng: 84.0, name: "Nepal" }, // 尼泊
SA: { lat: 25.0, lng: 45.0, name: "Saudi Arabia" }, // 沙特阿拉伯 OM: { lat: 21.0, lng: 57.0, name: "Oman" }, // 阿曼
SG: { lat: 1.3667, lng: 103.8, name: "Singapore" }, // 新加坡 PK: { lat: 30.0, lng: 70.0, name: "Pakistan" }, // 巴基斯坦
KR: { lat: 37.0, lng: 127.5, name: "South Korea" }, // 韩国 PH: { lat: 13.0, lng: 122.0, name: "Philippines" }, // 菲律宾
LK: { lat: 7.0, lng: 81.0, name: "Sri Lanka" }, // 斯里兰 QA: { lat: 25.5, lng: 51.25, name: "Qatar" }, // 卡塔尔
SY: { lat: 35.0, lng: 38.0, name: "Syria" }, // 叙利亚 SA: { lat: 25.0, lng: 45.0, name: "Saudi Arabia" }, // 沙特阿拉伯
TW: { lat: 23.5, lng: 121.0, name: "Taiwan" }, // 台湾 SG: { lat: 1.3667, lng: 103.8, name: "Singapore" }, // 新加坡
TJ: { lat: 39.0, lng: 71.0, name: "Tajikistan" }, // 塔吉克斯坦 KR: { lat: 37.0, lng: 127.5, name: "South Korea" }, // 韩国
TH: { lat: 15.0, lng: 100.0, name: "Thailand" }, // 泰国 LK: { lat: 7.0, lng: 81.0, name: "Sri Lanka" }, // 斯里兰卡
TR: { lat: 39.0, lng: 35.0, name: "Turkey" }, // 土耳其 SY: { lat: 35.0, lng: 38.0, name: "Syria" }, // 叙利亚
TM: { lat: 40.0, lng: 60.0, name: "Turkmenistan" }, // 土库曼斯坦 TW: { lat: 23.5, lng: 121.0, name: "Taiwan" }, // 台湾
AE: { lat: 24.0, lng: 54.0, name: "United Arab Emirates" }, // 阿联酋 TJ: { lat: 39.0, lng: 71.0, name: "Tajikistan" }, // 塔吉克斯坦
UZ: { lat: 41.0, lng: 64.0, name: "Uzbekistan" }, // 乌兹别克斯坦 TH: { lat: 15.0, lng: 100.0, name: "Thailand" }, // 泰国
VN: { lat: 16.0, lng: 106.0, name: "Vietnam" }, // 越南 TR: { lat: 39.0, lng: 35.0, name: "Turkey" }, // 土耳其
YE: { lat: 15.0, lng: 48.0, name: "Yemen" }, // 也门 TM: { lat: 40.0, lng: 60.0, name: "Turkmenistan" }, // 土库曼斯坦
PS: { lat: 32.0, lng: 35.25, name: "Palestine" }, // 巴勒斯坦 AE: { lat: 24.0, lng: 54.0, name: "United Arab Emirates" }, // 阿联酋
UZ: { lat: 41.0, lng: 64.0, name: "Uzbekistan" }, // 乌兹别克斯坦
VN: { lat: 16.0, lng: 106.0, name: "Vietnam" }, // 越南
YE: { lat: 15.0, lng: 48.0, name: "Yemen" }, // 也门
PS: { lat: 32.0, lng: 35.25, name: "Palestine" }, // 巴勒斯坦
// 欧洲 // 欧洲
AL: { lat: 41.0, lng: 20.0, name: "Albania" }, // 阿尔巴尼亚 AL: { lat: 41.0, lng: 20.0, name: "Albania" }, // 阿尔巴尼亚
AD: { lat: 42.5, lng: 1.6, name: "Andorra" }, // 安道尔 AD: { lat: 42.5, lng: 1.6, name: "Andorra" }, // 安道尔
AT: { lat: 47.3333, lng: 13.3333, name: "Austria" }, // 奥地利 AT: { lat: 47.3333, lng: 13.3333, name: "Austria" }, // 奥地利
BY: { lat: 53.0, lng: 28.0, name: "Belarus" }, // 白俄罗斯 BY: { lat: 53.0, lng: 28.0, name: "Belarus" }, // 白俄罗斯
BE: { lat: 50.8333, lng: 4.0, name: "Belgium" }, // 比利时 BE: { lat: 50.8333, lng: 4.0, name: "Belgium" }, // 比利时
BA: { lat: 44.0, lng: 18.0, name: "Bosnia and Herzegovina" }, // 波黑 BA: { lat: 44.0, lng: 18.0, name: "Bosnia and Herzegovina" }, // 波黑
BG: { lat: 43.0, lng: 25.0, name: "Bulgaria" }, // 保加利亚 BG: { lat: 43.0, lng: 25.0, name: "Bulgaria" }, // 保加利亚
HR: { lat: 45.1667, lng: 15.5, name: "Croatia" }, // 克罗地亚 HR: { lat: 45.1667, lng: 15.5, name: "Croatia" }, // 克罗地亚
CZ: { lat: 49.75, lng: 15.5, name: "Czech Republic" }, // 捷克 CZ: { lat: 49.75, lng: 15.5, name: "Czech Republic" }, // 捷克
DK: { lat: 56.0, lng: 10.0, name: "Denmark" }, // 丹麦 DK: { lat: 56.0, lng: 10.0, name: "Denmark" }, // 丹麦
EE: { lat: 59.0, lng: 26.0, name: "Estonia" }, // 爱沙尼亚 EE: { lat: 59.0, lng: 26.0, name: "Estonia" }, // 爱沙尼亚
FI: { lat: 64.0, lng: 26.0, name: "Finland" }, // 芬兰 FI: { lat: 64.0, lng: 26.0, name: "Finland" }, // 芬兰
FR: { lat: 46.0, lng: 2.0, name: "France" }, // 法国 FR: { lat: 46.0, lng: 2.0, name: "France" }, // 法国
DE: { lat: 51.0, lng: 9.0, name: "Germany" }, // 德国 DE: { lat: 51.0, lng: 9.0, name: "Germany" }, // 德国
GR: { lat: 39.0, lng: 22.0, name: "Greece" }, // 希腊 GR: { lat: 39.0, lng: 22.0, name: "Greece" }, // 希腊
HU: { lat: 47.0, lng: 20.0, name: "Hungary" }, // 匈牙利 HU: { lat: 47.0, lng: 20.0, name: "Hungary" }, // 匈牙利
IS: { lat: 65.0, lng: -18.0, name: "Iceland" }, // 冰岛 IS: { lat: 65.0, lng: -18.0, name: "Iceland" }, // 冰岛
IE: { lat: 53.0, lng: -8.0, name: "Ireland" }, // 爱尔兰 IE: { lat: 53.0, lng: -8.0, name: "Ireland" }, // 爱尔兰
IT: { lat: 42.8333, lng: 12.8333, name: "Italy" }, // 意大利 IT: { lat: 42.8333, lng: 12.8333, name: "Italy" }, // 意大利
LV: { lat: 57.0, lng: 25.0, name: "Latvia" }, // 拉脱维亚 LV: { lat: 57.0, lng: 25.0, name: "Latvia" }, // 拉脱维亚
LI: { lat: 47.1667, lng: 9.5333, name: "Liechtenstein" }, // 列支敦士登 LI: { lat: 47.1667, lng: 9.5333, name: "Liechtenstein" }, // 列支敦士登
LT: { lat: 56.0, lng: 24.0, name: "Lithuania" }, // 立陶宛 LT: { lat: 56.0, lng: 24.0, name: "Lithuania" }, // 立陶宛
LU: { lat: 49.75, lng: 6.1667, name: "Luxembourg" }, // 卢森堡 LU: { lat: 49.75, lng: 6.1667, name: "Luxembourg" }, // 卢森堡
MT: { lat: 35.8333, lng: 14.5833, name: "Malta" }, // 马耳他 MT: { lat: 35.8333, lng: 14.5833, name: "Malta" }, // 马耳他
MD: { lat: 47.0, lng: 29.0, name: "Moldova" }, // 摩尔多瓦 MD: { lat: 47.0, lng: 29.0, name: "Moldova" }, // 摩尔多瓦
MC: { lat: 43.7333, lng: 7.4, name: "Monaco" }, // 摩纳哥 MC: { lat: 43.7333, lng: 7.4, name: "Monaco" }, // 摩纳哥
ME: { lat: 42.0, lng: 19.0, name: "Montenegro" }, // 黑山 ME: { lat: 42.0, lng: 19.0, name: "Montenegro" }, // 黑山
NL: { lat: 52.5, lng: 5.75, name: "Netherlands" }, // 荷兰 NL: { lat: 52.5, lng: 5.75, name: "Netherlands" }, // 荷兰
NO: { lat: 62.0, lng: 10.0, name: "Norway" }, // 挪威 NO: { lat: 62.0, lng: 10.0, name: "Norway" }, // 挪威
PL: { lat: 52.0, lng: 20.0, name: "Poland" }, // 波兰 PL: { lat: 52.0, lng: 20.0, name: "Poland" }, // 波兰
PT: { lat: 39.5, lng: -8.0, name: "Portugal" }, // 葡萄牙 PT: { lat: 39.5, lng: -8.0, name: "Portugal" }, // 葡萄牙
RO: { lat: 46.0, lng: 25.0, name: "Romania" }, // 罗马尼亚 RO: { lat: 46.0, lng: 25.0, name: "Romania" }, // 罗马尼亚
RU: { lat: 60.0, lng: 100.0, name: "Russia" }, // 俄罗斯 RU: { lat: 60.0, lng: 100.0, name: "Russia" }, // 俄罗斯
SM: { lat: 43.7667, lng: 12.4167, name: "San Marino" }, // 圣马力诺 SM: { lat: 43.7667, lng: 12.4167, name: "San Marino" }, // 圣马力诺
RS: { lat: 44.0, lng: 21.0, name: "Serbia" }, // 塞尔维亚 RS: { lat: 44.0, lng: 21.0, name: "Serbia" }, // 塞尔维亚
SK: { lat: 48.6667, lng: 19.5, name: "Slovakia" }, // 斯洛伐克 SK: { lat: 48.6667, lng: 19.5, name: "Slovakia" }, // 斯洛伐克
SI: { lat: 46.0, lng: 15.0, name: "Slovenia" }, // 斯洛文尼亚 SI: { lat: 46.0, lng: 15.0, name: "Slovenia" }, // 斯洛文尼亚
ES: { lat: 40.0, lng: -4.0, name: "Spain" }, // 西班牙 ES: { lat: 40.0, lng: -4.0, name: "Spain" }, // 西班牙
SE: { lat: 62.0, lng: 15.0, name: "Sweden" }, // 瑞典 SE: { lat: 62.0, lng: 15.0, name: "Sweden" }, // 瑞典
CH: { lat: 47.0, lng: 8.0, name: "Switzerland" }, // 瑞士 CH: { lat: 47.0, lng: 8.0, name: "Switzerland" }, // 瑞士
UA: { lat: 49.0, lng: 32.0, name: "Ukraine" }, // 乌克兰 UA: { lat: 49.0, lng: 32.0, name: "Ukraine" }, // 乌克兰
GB: { lat: 54.0, lng: -2.0, name: "United Kingdom" }, // 英国 GB: { lat: 54.0, lng: -2.0, name: "United Kingdom" }, // 英国
VA: { lat: 41.9, lng: 12.45, name: "Vatican City" }, // 梵蒂冈 VA: { lat: 41.9, lng: 12.45, name: "Vatican City" }, // 梵蒂冈
// 北美洲 // 北美洲
AG: { lat: 17.05, lng: -61.8, name: "Antigua and Barbuda" }, // 安提瓜和巴布达 AG: { lat: 17.05, lng: -61.8, name: "Antigua and Barbuda" }, // 安提瓜和巴布达
BS: { lat: 24.25, lng: -76.0, name: "Bahamas" }, // 巴哈马 BS: { lat: 24.25, lng: -76.0, name: "Bahamas" }, // 巴哈马
BB: { lat: 13.1667, lng: -59.5333, name: "Barbados" }, // 巴巴多斯 BB: { lat: 13.1667, lng: -59.5333, name: "Barbados" }, // 巴巴多斯
BZ: { lat: 17.25, lng: -88.75, name: "Belize" }, // 伯利兹 BZ: { lat: 17.25, lng: -88.75, name: "Belize" }, // 伯利兹
CA: { lat: 60.0, lng: -95.0, name: "Canada" }, // 加拿大 CA: { lat: 60.0, lng: -95.0, name: "Canada" }, // 加拿大
CR: { lat: 10.0, lng: -84.0, name: "Costa Rica" }, // 哥斯达黎加 CR: { lat: 10.0, lng: -84.0, name: "Costa Rica" }, // 哥斯达黎加
CU: { lat: 21.5, lng: -80.0, name: "Cuba" }, // 古巴 CU: { lat: 21.5, lng: -80.0, name: "Cuba" }, // 古巴
DM: { lat: 15.4167, lng: -61.3333, name: "Dominica" }, // 多米尼克 DM: { lat: 15.4167, lng: -61.3333, name: "Dominica" }, // 多米尼克
DO: { lat: 19.0, lng: -70.6667, name: "Dominican Republic" }, // 多米尼加共和国 DO: { lat: 19.0, lng: -70.6667, name: "Dominican Republic" }, // 多米尼加共和国
SV: { lat: 13.8333, lng: -88.9167, name: "El Salvador" }, // 萨尔瓦多 SV: { lat: 13.8333, lng: -88.9167, name: "El Salvador" }, // 萨尔瓦多
GD: { lat: 12.1167, lng: -61.6667, name: "Grenada" }, // 格林纳达 GD: { lat: 12.1167, lng: -61.6667, name: "Grenada" }, // 格林纳达
GT: { lat: 15.5, lng: -90.25, name: "Guatemala" }, // 危地马拉 GT: { lat: 15.5, lng: -90.25, name: "Guatemala" }, // 危地马拉
HT: { lat: 19.0, lng: -72.4167, name: "Haiti" }, // 海地 HT: { lat: 19.0, lng: -72.4167, name: "Haiti" }, // 海地
HN: { lat: 15.0, lng: -86.5, name: "Honduras" }, // 洪都拉斯 HN: { lat: 15.0, lng: -86.5, name: "Honduras" }, // 洪都拉斯
JM: { lat: 18.25, lng: -77.5, name: "Jamaica" }, // 牙买加 JM: { lat: 18.25, lng: -77.5, name: "Jamaica" }, // 牙买加
MX: { lat: 23.0, lng: -102.0, name: "Mexico" }, // 墨西哥 MX: { lat: 23.0, lng: -102.0, name: "Mexico" }, // 墨西哥
NI: { lat: 13.0, lng: -85.0, name: "Nicaragua" }, // 尼加拉瓜 NI: { lat: 13.0, lng: -85.0, name: "Nicaragua" }, // 尼加拉瓜
PA: { lat: 9.0, lng: -80.0, name: "Panama" }, // 巴拿马 PA: { lat: 9.0, lng: -80.0, name: "Panama" }, // 巴拿马
KN: { lat: 17.3333, lng: -62.75, name: "Saint Kitts and Nevis" }, // 圣基茨和尼维斯 KN: { lat: 17.3333, lng: -62.75, name: "Saint Kitts and Nevis" }, // 圣基茨和尼维斯
LC: { lat: 13.8833, lng: -61.1333, name: "Saint Lucia" }, // 圣卢西亚 LC: { lat: 13.8833, lng: -61.1333, name: "Saint Lucia" }, // 圣卢西亚
VC: { lat: 13.25, lng: -61.2, name: "Saint Vincent and the Grenadines" }, // 圣文森特和格林纳丁斯 VC: { lat: 13.25, lng: -61.2, name: "Saint Vincent and the Grenadines" }, // 圣文森特和格林纳丁斯
TT: { lat: 11.0, lng: -61.0, name: "Trinidad and Tobago" }, // 特立尼达和多巴哥 TT: { lat: 11.0, lng: -61.0, name: "Trinidad and Tobago" }, // 特立尼达和多巴哥
US: { lat: 38.0, lng: -97.0, name: "United States" }, // 美国 US: { lat: 38.0, lng: -97.0, name: "United States" }, // 美国
// 南美洲 // 南美洲
AR: { lat: -34.0, lng: -64.0, name: "Argentina" }, // 阿根廷 AR: { lat: -34.0, lng: -64.0, name: "Argentina" }, // 阿根廷
BO: { lat: -17.0, lng: -65.0, name: "Bolivia" }, // 玻利维亚 BO: { lat: -17.0, lng: -65.0, name: "Bolivia" }, // 玻利维亚
BR: { lat: -10.0, lng: -55.0, name: "Brazil" }, // 巴西 BR: { lat: -10.0, lng: -55.0, name: "Brazil" }, // 巴西
CL: { lat: -30.0, lng: -71.0, name: "Chile" }, // 智利 CL: { lat: -30.0, lng: -71.0, name: "Chile" }, // 智利
CO: { lat: 4.0, lng: -72.0, name: "Colombia" }, // 哥伦比亚 CO: { lat: 4.0, lng: -72.0, name: "Colombia" }, // 哥伦比亚
EC: { lat: -2.0, lng: -77.5, name: "Ecuador" }, // 厄瓜多尔 EC: { lat: -2.0, lng: -77.5, name: "Ecuador" }, // 厄瓜多尔
GY: { lat: 5.0, lng: -59.0, name: "Guyana" }, // 圭亚那 GY: { lat: 5.0, lng: -59.0, name: "Guyana" }, // 圭亚那
PY: { lat: -23.0, lng: -58.0, name: "Paraguay" }, // 巴拉圭 PY: { lat: -23.0, lng: -58.0, name: "Paraguay" }, // 巴拉圭
PE: { lat: -10.0, lng: -76.0, name: "Peru" }, // 秘鲁 PE: { lat: -10.0, lng: -76.0, name: "Peru" }, // 秘鲁
SR: { lat: 4.0, lng: -56.0, name: "Suriname" }, // 苏里南 SR: { lat: 4.0, lng: -56.0, name: "Suriname" }, // 苏里南
UY: { lat: -33.0, lng: -56.0, name: "Uruguay" }, // 乌拉圭 UY: { lat: -33.0, lng: -56.0, name: "Uruguay" }, // 乌拉圭
VE: { lat: 8.0, lng: -66.0, name: "Venezuela" }, // 委内瑞拉 VE: { lat: 8.0, lng: -66.0, name: "Venezuela" }, // 委内瑞拉
// 大洋洲 // 大洋洲
AU: { lat: -27.0, lng: 133.0, name: "Australia" }, // 澳大利亚 AU: { lat: -27.0, lng: 133.0, name: "Australia" }, // 澳大利亚
FJ: { lat: -18.0, lng: 175.0, name: "Fiji" }, // 斐济 FJ: { lat: -18.0, lng: 175.0, name: "Fiji" }, // 斐济
KI: { lat: 1.4167, lng: 173.0, name: "Kiribati" }, // 基里巴斯 KI: { lat: 1.4167, lng: 173.0, name: "Kiribati" }, // 基里巴斯
MH: { lat: 9.0, lng: 168.0, name: "Marshall Islands" }, // 马绍尔群岛 MH: { lat: 9.0, lng: 168.0, name: "Marshall Islands" }, // 马绍尔群岛
FM: { lat: 6.9167, lng: 158.25, name: "Micronesia" }, // 密克罗尼西亚 FM: { lat: 6.9167, lng: 158.25, name: "Micronesia" }, // 密克罗尼西亚
NR: { lat: -0.5333, lng: 166.9167, name: "Nauru" }, // 瑙鲁 NR: { lat: -0.5333, lng: 166.9167, name: "Nauru" }, // 瑙鲁
NZ: { lat: -41.0, lng: 174.0, name: "New Zealand" }, // 新西兰 NZ: { lat: -41.0, lng: 174.0, name: "New Zealand" }, // 新西兰
PW: { lat: 7.5, lng: 134.5, name: "Palau" }, // 帕劳 PW: { lat: 7.5, lng: 134.5, name: "Palau" }, // 帕劳
PG: { lat: -6.0, lng: 147.0, name: "Papua New Guinea" }, // 巴布亚新几内亚 PG: { lat: -6.0, lng: 147.0, name: "Papua New Guinea" }, // 巴布亚新几内亚
WS: { lat: -13.5833, lng: -172.3333, name: "Samoa" }, // 萨摩亚 WS: { lat: -13.5833, lng: -172.3333, name: "Samoa" }, // 萨摩亚
SB: { lat: -8.0, lng: 159.0, name: "Solomon Islands" }, // 所罗门群岛 SB: { lat: -8.0, lng: 159.0, name: "Solomon Islands" }, // 所罗门群岛
TO: { lat: -20.0, lng: -175.0, name: "Tonga" }, // 汤加 TO: { lat: -20.0, lng: -175.0, name: "Tonga" }, // 汤加
TV: { lat: -8.0, lng: 178.0, name: "Tuvalu" }, // 图瓦卢 TV: { lat: -8.0, lng: 178.0, name: "Tuvalu" }, // 图瓦卢
VU: { lat: -16.0, lng: 167.0, name: "Vanuatu" }, // 瓦努阿图 VU: { lat: -16.0, lng: 167.0, name: "Vanuatu" }, // 瓦努阿图
// 非洲 // 非洲
DZ: { lat: 28.0, lng: 3.0, name: "Algeria" }, // 阿尔及利亚 DZ: { lat: 28.0, lng: 3.0, name: "Algeria" }, // 阿尔及利亚
AO: { lat: -12.5, lng: 18.5, name: "Angola" }, // 安哥拉 AO: { lat: -12.5, lng: 18.5, name: "Angola" }, // 安哥拉
BJ: { lat: 9.5, lng: 2.25, name: "Benin" }, // 贝宁 BJ: { lat: 9.5, lng: 2.25, name: "Benin" }, // 贝宁
BW: { lat: -22.0, lng: 24.0, name: "Botswana" }, // 博茨瓦纳 BW: { lat: -22.0, lng: 24.0, name: "Botswana" }, // 博茨瓦纳
BF: { lat: 13.0, lng: -2.0, name: "Burkina Faso" }, // 布基纳法索 BF: { lat: 13.0, lng: -2.0, name: "Burkina Faso" }, // 布基纳法索
BI: { lat: -3.5, lng: 30.0, name: "Burundi" }, // 布隆迪 BI: { lat: -3.5, lng: 30.0, name: "Burundi" }, // 布隆迪
CM: { lat: 6.0, lng: 12.0, name: "Cameroon" }, // 喀麦隆 CM: { lat: 6.0, lng: 12.0, name: "Cameroon" }, // 喀麦隆
CV: { lat: 16.0, lng: -24.0, name: "Cape Verde" }, // 佛得角 CV: { lat: 16.0, lng: -24.0, name: "Cape Verde" }, // 佛得角
CF: { lat: 7.0, lng: 21.0, name: "Central African Republic" }, // 中非共和国 CF: { lat: 7.0, lng: 21.0, name: "Central African Republic" }, // 中非共和国
TD: { lat: 15.0, lng: 19.0, name: "Chad" }, // 乍得 TD: { lat: 15.0, lng: 19.0, name: "Chad" }, // 乍得
KM: { lat: -12.1667, lng: 44.25, name: "Comoros" }, // 科摩罗 KM: { lat: -12.1667, lng: 44.25, name: "Comoros" }, // 科摩罗
CG: { lat: -1.0, lng: 15.0, name: "Congo" }, // 刚果 CG: { lat: -1.0, lng: 15.0, name: "Congo" }, // 刚果
CD: { lat: 0.0, lng: 25.0, name: "Democratic Republic of the Congo" }, // 刚果民主共和国 CD: { lat: 0.0, lng: 25.0, name: "Democratic Republic of the Congo" }, // 刚果民主共和国
CI: { lat: 8.0, lng: -5.0, name: "Côte d'Ivoire" }, // 科特迪瓦 CI: { lat: 8.0, lng: -5.0, name: "Côte d'Ivoire" }, // 科特迪瓦
DJ: { lat: 11.5, lng: 43.0, name: "Djibouti" }, // 吉布提 DJ: { lat: 11.5, lng: 43.0, name: "Djibouti" }, // 吉布提
EG: { lat: 27.0, lng: 30.0, name: "Egypt" }, // 埃及 EG: { lat: 27.0, lng: 30.0, name: "Egypt" }, // 埃及
GQ: { lat: 2.0, lng: 10.0, name: "Equatorial Guinea" }, // 赤道几内亚 GQ: { lat: 2.0, lng: 10.0, name: "Equatorial Guinea" }, // 赤道几内亚
ER: { lat: 15.0, lng: 39.0, name: "Eritrea" }, // 厄立特里亚 ER: { lat: 15.0, lng: 39.0, name: "Eritrea" }, // 厄立特里亚
ET: { lat: 8.0, lng: 38.0, name: "Ethiopia" }, // 埃塞俄比亚 ET: { lat: 8.0, lng: 38.0, name: "Ethiopia" }, // 埃塞俄比亚
GA: { lat: -1.0, lng: 11.75, name: "Gabon" }, // 加蓬 GA: { lat: -1.0, lng: 11.75, name: "Gabon" }, // 加蓬
GM: { lat: 13.4667, lng: -16.5667, name: "Gambia" }, // 冈比亚 GM: { lat: 13.4667, lng: -16.5667, name: "Gambia" }, // 冈比亚
GH: { lat: 8.0, lng: -2.0, name: "Ghana" }, // 加纳 GH: { lat: 8.0, lng: -2.0, name: "Ghana" }, // 加纳
GN: { lat: 11.0, lng: -10.0, name: "Guinea" }, // 几内亚 GN: { lat: 11.0, lng: -10.0, name: "Guinea" }, // 几内亚
GW: { lat: 12.0, lng: -15.0, name: "Guinea-Bissau" }, // 几内亚比绍 GW: { lat: 12.0, lng: -15.0, name: "Guinea-Bissau" }, // 几内亚比绍
KE: { lat: 1.0, lng: 38.0, name: "Kenya" }, // 肯尼亚 KE: { lat: 1.0, lng: 38.0, name: "Kenya" }, // 肯尼亚
LS: { lat: -29.5, lng: 28.5, name: "Lesotho" }, // 莱索托 LS: { lat: -29.5, lng: 28.5, name: "Lesotho" }, // 莱索托
LR: { lat: 6.5, lng: -9.5, name: "Liberia" }, // 利比里亚 LR: { lat: 6.5, lng: -9.5, name: "Liberia" }, // 利比里亚
LY: { lat: 25.0, lng: 17.0, name: "Libya" }, // 利比亚 LY: { lat: 25.0, lng: 17.0, name: "Libya" }, // 利比亚
MG: { lat: -20.0, lng: 47.0, name: "Madagascar" }, // 马达加斯加 MG: { lat: -20.0, lng: 47.0, name: "Madagascar" }, // 马达加斯加
MW: { lat: -13.5, lng: 34.0, name: "Malawi" }, // 马拉维 MW: { lat: -13.5, lng: 34.0, name: "Malawi" }, // 马拉维
ML: { lat: 17.0, lng: -4.0, name: "Mali" }, // 马里 ML: { lat: 17.0, lng: -4.0, name: "Mali" }, // 马里
MR: { lat: 20.0, lng: -12.0, name: "Mauritania" }, // 毛里塔尼亚 MR: { lat: 20.0, lng: -12.0, name: "Mauritania" }, // 毛里塔尼亚
MU: { lat: -20.2833, lng: 57.55, name: "Mauritius" }, // 毛里求斯 MU: { lat: -20.2833, lng: 57.55, name: "Mauritius" }, // 毛里求斯
YT: { lat: -12.8333, lng: 45.1667, name: "Mayotte" }, // 马约特 YT: { lat: -12.8333, lng: 45.1667, name: "Mayotte" }, // 马约特
MA: { lat: 32.0, lng: -5.0, name: "Morocco" }, // 摩洛哥 MA: { lat: 32.0, lng: -5.0, name: "Morocco" }, // 摩洛哥
MZ: { lat: -18.25, lng: 35.0, name: "Mozambique" }, // 莫桑比克 MZ: { lat: -18.25, lng: 35.0, name: "Mozambique" }, // 莫桑比克
NA: { lat: -22.0, lng: 17.0, name: "Namibia" }, // 纳米比亚 NA: { lat: -22.0, lng: 17.0, name: "Namibia" }, // 纳米比亚
NE: { lat: 16.0, lng: 8.0, name: "Niger" }, // 尼日尔 NE: { lat: 16.0, lng: 8.0, name: "Niger" }, // 尼日尔
NG: { lat: 10.0, lng: 8.0, name: "Nigeria" }, // 尼日利亚 NG: { lat: 10.0, lng: 8.0, name: "Nigeria" }, // 尼日利亚
RW: { lat: -2.0, lng: 30.0, name: "Rwanda" }, // 卢旺达 RW: { lat: -2.0, lng: 30.0, name: "Rwanda" }, // 卢旺达
ST: { lat: 1.0, lng: 7.0, name: "São Tomé and Principe" }, // 圣多美和普林西比 ST: { lat: 1.0, lng: 7.0, name: "São Tomé and Principe" }, // 圣多美和普林西比
SN: { lat: 14.0, lng: -14.0, name: "Senegal" }, // 塞内加尔 SN: { lat: 14.0, lng: -14.0, name: "Senegal" }, // 塞内加尔
SC: { lat: -4.5833, lng: 55.6667, name: "Seychelles" }, // 塞舌尔 SC: { lat: -4.5833, lng: 55.6667, name: "Seychelles" }, // 塞舌尔
SL: { lat: 8.5, lng: -11.5, name: "Sierra Leone" }, // 塞拉利昂 SL: { lat: 8.5, lng: -11.5, name: "Sierra Leone" }, // 塞拉利昂
SO: { lat: 10.0, lng: 49.0, name: "Somalia" }, // 索马里 SO: { lat: 10.0, lng: 49.0, name: "Somalia" }, // 索马里
ZA: { lat: -29.0, lng: 24.0, name: "South Africa" }, // 南非 ZA: { lat: -29.0, lng: 24.0, name: "South Africa" }, // 南非
SD: { lat: 15.0, lng: 30.0, name: "Sudan" }, // 苏丹 SD: { lat: 15.0, lng: 30.0, name: "Sudan" }, // 苏丹
SZ: { lat: -26.5, lng: 31.5, name: "Swaziland" }, // 斯威士兰 SZ: { lat: -26.5, lng: 31.5, name: "Swaziland" }, // 斯威士兰
TZ: { lat: -6.0, lng: 35.0, name: "Tanzania" }, // 坦桑尼亚 TZ: { lat: -6.0, lng: 35.0, name: "Tanzania" }, // 坦桑尼亚
TG: { lat: 8.0, lng: 1.1667, name: "Togo" }, // 多哥 TG: { lat: 8.0, lng: 1.1667, name: "Togo" }, // 多哥
TN: { lat: 34.0, lng: 9.0, name: "Tunisia" }, // 突尼斯 TN: { lat: 34.0, lng: 9.0, name: "Tunisia" }, // 突尼斯
UG: { lat: 1.0, lng: 32.0, name: "Uganda" }, // 乌干达 UG: { lat: 1.0, lng: 32.0, name: "Uganda" }, // 乌干达
EH: { lat: 24.5, lng: -13.0, name: "Western Sahara" }, // 西撒哈拉 EH: { lat: 24.5, lng: -13.0, name: "Western Sahara" }, // 西撒哈拉
ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚 ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚
ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦 ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦
} };
+97 -89
View File
@@ -1,99 +1,107 @@
export const InjectContext = (content: string) => { export const InjectContext = (content: string) => {
const tempDiv = document.createElement("div") const tempDiv = document.createElement("div");
tempDiv.innerHTML = content tempDiv.innerHTML = content;
const INJECTION_MARK = "data-injected" // 自定义属性标识 const INJECTION_MARK = "data-injected"; // 自定义属性标识
// 清理已有的注入资源 // 清理已有的注入资源
const cleanInjectedResources = () => { const cleanInjectedResources = () => {
document.querySelectorAll(`[${INJECTION_MARK}]`).forEach((node) => node.remove()) document.querySelectorAll(`[${INJECTION_MARK}]`).forEach((node) => {
} node.remove();
});
};
const loadExternalScript = (scriptElement: HTMLScriptElement): Promise<void> => { const loadExternalScript = (
return new Promise((resolve, reject) => { scriptElement: HTMLScriptElement,
const script = document.createElement("script") ): Promise<void> => {
script.src = scriptElement.src return new Promise((resolve, reject) => {
script.async = false // 保持顺序执行 const script = document.createElement("script");
script.setAttribute(INJECTION_MARK, "true") // 添加标识 script.src = scriptElement.src;
script.onload = () => resolve() script.async = false; // 保持顺序执行
script.onerror = () => reject(new Error(`Failed to load script: ${scriptElement.src}`)) script.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.head.appendChild(script) script.onload = () => resolve();
}) script.onerror = () =>
} reject(new Error(`Failed to load script: ${scriptElement.src}`));
document.head.appendChild(script);
});
};
const executeInlineScript = (scriptContent: string): Promise<void> => { const executeInlineScript = (scriptContent: string): Promise<void> => {
return new Promise((resolve) => { return new Promise((resolve) => {
const script = document.createElement("script") const script = document.createElement("script");
script.textContent = scriptContent script.textContent = scriptContent;
script.setAttribute(INJECTION_MARK, "true") // 添加标识 script.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.body.appendChild(script) document.body.appendChild(script);
resolve() resolve();
}) });
} };
const loadStyle = (styleElement: HTMLStyleElement): Promise<void> => { const loadStyle = (styleElement: HTMLStyleElement): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if ((styleElement as any).href) { if ((styleElement as any).href) {
// 处理 <link> // 处理 <link>
const link = document.createElement("link") const link = document.createElement("link");
link.rel = "stylesheet" link.rel = "stylesheet";
link.href = (styleElement as any).href link.href = (styleElement as any).href;
link.setAttribute(INJECTION_MARK, "true") // 添加标识 link.setAttribute(INJECTION_MARK, "true"); // 添加标识
link.onload = () => resolve() link.onload = () => resolve();
link.onerror = () => reject(new Error(`Failed to load stylesheet: ${link.href}`)) link.onerror = () =>
document.head.appendChild(link) reject(new Error(`Failed to load stylesheet: ${link.href}`));
} else { document.head.appendChild(link);
const style = document.createElement("style") } else {
style.textContent = styleElement.textContent const style = document.createElement("style");
style.setAttribute(INJECTION_MARK, "true") // 添加标识 style.textContent = styleElement.textContent;
document.head.appendChild(style) style.setAttribute(INJECTION_MARK, "true"); // 添加标识
resolve() document.head.appendChild(style);
} resolve();
}) }
} });
};
const handlers: { [key: string]: (element: HTMLElement) => Promise<void> } = { const handlers: { [key: string]: (element: HTMLElement) => Promise<void> } = {
SCRIPT: (element) => { SCRIPT: (element) => {
const scriptElement = element as HTMLScriptElement const scriptElement = element as HTMLScriptElement;
if (scriptElement.src) { if (scriptElement.src) {
// 加载外部脚本 // 加载外部脚本
return loadExternalScript(scriptElement) return loadExternalScript(scriptElement);
} else { } else {
// 执行内联脚本 // 执行内联脚本
return executeInlineScript(scriptElement.textContent || "") return executeInlineScript(scriptElement.textContent || "");
} }
}, },
STYLE: (element) => loadStyle(element as HTMLStyleElement), STYLE: (element) => loadStyle(element as HTMLStyleElement),
META: (element) => { META: (element) => {
const meta = element.cloneNode(true) as HTMLElement const meta = element.cloneNode(true) as HTMLElement;
meta.setAttribute(INJECTION_MARK, "true") // 添加标识 meta.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.head.appendChild(meta) // 将 meta 标签插入到 <head> document.head.appendChild(meta); // 将 meta 标签插入到 <head>
return Promise.resolve() return Promise.resolve();
}, },
DEFAULT: (element) => { DEFAULT: (element) => {
element.setAttribute(INJECTION_MARK, "true") // 添加标识 element.setAttribute(INJECTION_MARK, "true"); // 添加标识
document.body.appendChild(element) document.body.appendChild(element);
return Promise.resolve() return Promise.resolve();
}, },
} };
// 开始注入前清理已有资源 // 开始注入前清理已有资源
cleanInjectedResources() cleanInjectedResources();
const executeSequentially = async () => { const executeSequentially = async () => {
for (const node of Array.from(tempDiv.childNodes)) { for (const node of Array.from(tempDiv.childNodes)) {
if (node.nodeType === Node.ELEMENT_NODE) { if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as HTMLElement const element = node as HTMLElement;
const handler = handlers[element.tagName] || handlers.DEFAULT const handler = handlers[element.tagName] || handlers.DEFAULT;
await handler(element) // 按顺序等待当前脚本或资源完成处理 await handler(element); // 按顺序等待当前脚本或资源完成处理
} else if (node.nodeType === Node.TEXT_NODE) { } else if (node.nodeType === Node.TEXT_NODE) {
document.body.appendChild(document.createTextNode(node.textContent || "")) document.body.appendChild(
} document.createTextNode(node.textContent || ""),
} );
console.log("All resources have been injected and executed in sequence.") }
} }
console.log("All resources have been injected and executed in sequence.");
};
return executeSequentially().catch((error) => { return executeSequentially().catch((error) => {
console.error("Error during resource injection:", error) console.error("Error during resource injection:", error);
}) });
} };

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