feat: 替换 OpenBridge 组件并实现航海规范主题系统

- 使用 shadcn/ui 重新实现 TopBar、ThemeSidebar、AlertBadge 组件
- 解决 @oicl/openbridge-webcomponents ESM 模块解析问题
- 添加 OpenBridge 四种主题 CSS 变量 (day/bright/dusk/night)
- Night 主题使用暗黄色文字保护夜视能力
- 更新 API 端点适配新的按模型分组数据结构
This commit is contained in:
2026-01-26 21:17:56 +08:00
parent fa625ca301
commit d22a0f8d69
14 changed files with 1025 additions and 219 deletions

View File

@@ -12,6 +12,8 @@
"@orpc/server": "^1.13.4",
"@orpc/tanstack-query": "^1.13.4",
"@orpc/zod": "^1.13.4",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-slot": "^1.2.4",
"@t3-oss/env-core": "^0.13.10",
"@tanstack/react-query": "^5.90.18",
"@tanstack/react-router": "^1.151.0",
@@ -20,10 +22,14 @@
"@tauri-apps/api": "^2.9.1",
"@types/react": "^19.2.9",
"@types/react-dom": "^19.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3",
"lucide-react": "^0.563.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"tailwind-merge": "^3.4.0",
"zod": "^4.3.5",
},
"devDependencies": {
@@ -42,6 +48,7 @@
"nitro": "npm:nitro-nightly@latest",
"tailwindcss": "^4.1.18",
"turbo": "^2.7.5",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"vite": "^8.0.0-beta.8",
"vite-tsconfig-paths": "^6.0.4",
@@ -347,6 +354,40 @@
"@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.108.0", "", { "os": "win32", "cpu": "x64" }, "sha512-k+7tuCMULfB7zr57jb68sVzxbyleZBasyr1h1Ieiu1U95XHYe64pbSrwHmlaSmiNHqV91ikM3809+ps68jZZhw=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@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-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-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-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-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.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-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.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-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-slot": ["@radix-ui/react-slot@1.2.4", "", { "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-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@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-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-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.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.60", "", { "os": "android", "cpu": "arm64" }, "sha512-hOW6iQXtpG4uCW1zGK56+KhEXGttSkTp2ykncW/nkOIF/jOKTqbM944Q73HVeMXP1mPRvE2cZwNp3xeLIeyIGQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.60", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vyDA4HXY2mP8PPtl5UE17uGPxUNG4m1wkfa3kAkR8JWrFbarV97UmLq22IWrNhtBPa89xqerzLK8KoVmz5JqCQ=="],
@@ -597,6 +638,8 @@
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
@@ -631,6 +674,8 @@
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"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=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
@@ -661,6 +706,8 @@
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
@@ -711,6 +758,8 @@
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -815,6 +864,8 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -871,6 +922,12 @@
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "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-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
"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-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=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
@@ -911,6 +968,8 @@
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
@@ -943,6 +1002,8 @@
"turbo-windows-arm64": ["turbo-windows-arm64@2.7.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-G377Gxn6P42RnCzfMyDvsqQV7j69kVHKlhz9J4RhtJOB5+DyY4yYh/w0oTIxZQ4JRMmhjwLu3w9zncMoQ6nNDw=="],
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"type-fest": ["type-fest@5.4.1", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
@@ -961,6 +1022,10 @@
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"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=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
@@ -991,6 +1056,10 @@
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"@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-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=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],

22
components.json Normal file
View File

@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

View File

@@ -27,6 +27,8 @@
"@orpc/server": "^1.13.4",
"@orpc/tanstack-query": "^1.13.4",
"@orpc/zod": "^1.13.4",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-slot": "^1.2.4",
"@t3-oss/env-core": "^0.13.10",
"@tanstack/react-query": "^5.90.18",
"@tanstack/react-router": "^1.151.0",
@@ -35,10 +37,14 @@
"@tauri-apps/api": "^2.9.1",
"@types/react": "^19.2.9",
"@types/react-dom": "^19.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3",
"lucide-react": "^0.563.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"tailwind-merge": "^3.4.0",
"zod": "^4.3.5"
},
"devDependencies": {
@@ -57,6 +63,7 @@
"nitro": "npm:nitro-nightly@latest",
"tailwindcss": "^4.1.18",
"turbo": "^2.7.5",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"vite": "^8.0.0-beta.8",
"vite-tsconfig-paths": "^6.0.4"

View File

@@ -117,7 +117,7 @@ export const HealthRing = ({
<span className="text-2xl font-bold" style={{ color: ringColor }}>
{percentage}%
</span>
<span className="text-sm text-[var(--on-container-color)] opacity-70">
<span className="text-sm text-[var(--element-inactive-color,#707070)]">
{countdown.formatted}
</span>
</div>
@@ -126,13 +126,13 @@ export const HealthRing = ({
{/* 底部标签 */}
<div className="text-center">
<div
className="text-sm font-medium truncate max-w-[140px] text-[var(--on-container-color)]"
className="text-sm font-medium truncate max-w-[140px] text-[var(--element-active-color,#3d3d3d)]"
title={displayName || model}
>
{displayName || 'Claude Opus 4.5'}
</div>
<div
className="text-xs truncate max-w-[140px] text-[var(--on-container-color)] opacity-50"
className="text-xs truncate max-w-[140px] text-[var(--element-inactive-color,#707070)]"
title={account}
>
{account}

View File

@@ -2,64 +2,25 @@
* TokenUsageDashboard 组件
*
* 主仪表盘,展示多个账户的 claude-opus-4-5-thinking 配额使用情况。
* 使用 OpenBridge 设计系统的 TopBar 和 Alert 组件
* 使用自定义组件替代 OpenBridge 组件,基于 shadcn/ui 实现
*
* 特性:
* - 多账户配额可视化 (根据 API 返回的账户数量动态显示)
* - 实时告警通知 (低于 20% 警告,低于 5% 紧急)
* - 支持 OpenBridge 四种主题切换 (day/bright/dusk/night)
* - OpenBridge 组件懒加载以避免 SSR 问题
* - 支持四种主题切换 (day/bright/dusk/night)
*/
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-day.js'
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-day-bright.js'
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-dusk.js'
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-night.js'
import { AlertType } from '@oicl/openbridge-webcomponents/dist/types'
import { lazy, Suspense, useCallback, useMemo, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { HealthRing } from '@/components/HealthRing'
import { type ObcTheme, useTheme } from '@/hooks/useTheme'
import {
AlertBadge,
AlertNotification,
AlertType,
} from '@/components/ui/AlertBadge'
import { ThemeSidebar } from '@/components/ui/ThemeSidebar'
import { TopBar } from '@/components/ui/TopBar'
import { useTheme } from '@/hooks/useTheme'
import type { ModelUsage } from '@/orpc/contracts/usage'
// ============================================================================
// 懒加载 OpenBridge 组件(避免 SSR 问题)
// ============================================================================
const ObcTopBar = lazy(() =>
import(
'@oicl/openbridge-webcomponents-react/components/top-bar/top-bar'
).then((mod) => ({ default: mod.ObcTopBar })),
)
const ObcAlertTopbarElement = lazy(() =>
import(
'@oicl/openbridge-webcomponents-react/components/alert-topbar-element/alert-topbar-element'
).then((mod) => ({ default: mod.ObcAlertTopbarElement })),
)
const ObcNotificationMessageItem = lazy(() =>
import(
'@oicl/openbridge-webcomponents-react/components/notification-message-item/notification-message-item'
).then((mod) => ({ default: mod.ObcNotificationMessageItem })),
)
const ObcAlertIcon = lazy(() =>
import(
'@oicl/openbridge-webcomponents-react/components/alert-icon/alert-icon'
).then((mod) => ({ default: mod.ObcAlertIcon })),
)
const ObcNavigationMenu = lazy(() =>
import(
'@oicl/openbridge-webcomponents-react/components/navigation-menu/navigation-menu'
).then((mod) => ({ default: mod.ObcNavigationMenu })),
)
const ObcNavigationItem = lazy(() =>
import(
'@oicl/openbridge-webcomponents-react/components/navigation-item/navigation-item'
).then((mod) => ({ default: mod.ObcNavigationItem })),
)
// ============================================================================
// 类型定义
// ============================================================================
@@ -171,15 +132,6 @@ const getHighestAlertType = (alerts: AlertInfo[]): AlertType => {
return AlertType.Caution
}
// ============================================================================
// 子组件
// ============================================================================
/** TopBar 加载占位符 (SSR 时显示) */
const TopBarFallback = () => (
<div className="h-14 bg-[var(--container-background-color)] border-b border-[var(--divider-color)]" />
)
// ============================================================================
// 主组件
// ============================================================================
@@ -211,149 +163,41 @@ export const TokenUsageDashboard = ({ data }: TokenUsageDashboardProps) => {
setMenuOpen((prev) => !prev)
}, [])
/** 关闭侧边菜单 */
const closeMenu = useCallback(() => {
setMenuOpen(false)
}, [])
/** 切换主题并关闭菜单 */
const handleThemeChange = useCallback(
(newTheme: ObcTheme) => {
setTheme(newTheme)
setMenuOpen(false)
},
[setTheme],
)
// ========== 渲染 ==========
return (
<div className="min-h-screen flex flex-col bg-[var(--container-background-color)] text-[var(--on-container-color)]">
<div className="min-h-screen flex flex-col bg-[var(--container-background-color,#f7f7f7)] text-[var(--element-active-color,#3d3d3d)]">
{/* 顶部导航栏 */}
<header className="sticky top-0 z-50">
<Suspense fallback={<TopBarFallback />}>
<ObcTopBar
appTitle="Token Usage Viewer"
pageName=""
onMenuButtonClicked={
handleMenuButtonClick as unknown as EventListener
}
menuButtonActivated={menuOpen}
>
{/* 右侧告警面板 */}
<div slot="alerts">
<ObcAlertTopbarElement
nAlerts={alerts.length}
alertType={alertType}
alertMuted={alertMuted}
showAck={false}
onMuteclick={handleMuteClick as unknown as EventListener}
>
{topAlert && (
<ObcNotificationMessageItem time="">
<span slot="icon">
<ObcAlertIcon
name={
topAlert.type === AlertType.Alarm
? 'alarm-unack'
: 'warning-unack'
}
/>
</span>
<span slot="message">
{extractUsername(topAlert.account)}: {' '}
{Math.round(topAlert.remainingFraction * 100)}%
</span>
</ObcNotificationMessageItem>
)}
</ObcAlertTopbarElement>
</div>
</ObcTopBar>
</Suspense>
</header>
<TopBar
appTitle="Token Usage Viewer"
menuButtonActivated={menuOpen}
onMenuButtonClick={handleMenuButtonClick}
rightSlot={
<>
<AlertBadge
count={alerts.length}
alertType={alertType}
muted={alertMuted}
onMuteClick={handleMuteClick}
/>
{topAlert && (
<AlertNotification
account={extractUsername(topAlert.account)}
remainingPercent={Math.round(topAlert.remainingFraction * 100)}
alertType={topAlert.type}
/>
)}
</>
}
/>
{/* 侧边导航菜单 */}
{menuOpen && (
<aside className="fixed top-14 left-0 z-40 h-[calc(100vh-3.5rem)] w-64 bg-[var(--container-background-color)] border-r border-[var(--divider-color)] shadow-lg">
<Suspense fallback={null}>
<ObcNavigationMenu>
<div slot="main">
{/* 白天模式选项 */}
<ObcNavigationItem
label="白天模式"
checked={theme === 'day'}
onClick={() => handleThemeChange('day')}
>
<span
slot="icon"
// biome-ignore lint: custom element
dangerouslySetInnerHTML={{
__html: '<obi-palette-day></obi-palette-day>',
}}
/>
</ObcNavigationItem>
{/* 明亮模式选项 */}
<ObcNavigationItem
label="明亮模式"
checked={theme === 'bright'}
onClick={() => handleThemeChange('bright')}
>
<span
slot="icon"
// biome-ignore lint: custom element
dangerouslySetInnerHTML={{
__html:
'<obi-palette-day-bright></obi-palette-day-bright>',
}}
/>
</ObcNavigationItem>
{/* 黄昏模式选项 */}
<ObcNavigationItem
label="黄昏模式"
checked={theme === 'dusk'}
onClick={() => handleThemeChange('dusk')}
>
<span
slot="icon"
// biome-ignore lint: custom element
dangerouslySetInnerHTML={{
__html: '<obi-palette-dusk></obi-palette-dusk>',
}}
/>
</ObcNavigationItem>
{/* 夜间模式选项 */}
<ObcNavigationItem
label="夜间模式"
checked={theme === 'night'}
onClick={() => handleThemeChange('night')}
>
<span
slot="icon"
// biome-ignore lint: custom element
dangerouslySetInnerHTML={{
__html: '<obi-palette-night></obi-palette-night>',
}}
/>
</ObcNavigationItem>
</div>
</ObcNavigationMenu>
</Suspense>
</aside>
)}
{/* 点击遮罩关闭菜单 */}
{menuOpen && (
<button
type="button"
aria-label="关闭菜单"
className="fixed inset-0 z-30 bg-black/20 cursor-default"
onClick={closeMenu}
onKeyDown={(e) => e.key === 'Escape' && closeMenu()}
/>
)}
{/* 主题切换侧边栏 */}
<ThemeSidebar
open={menuOpen}
onOpenChange={setMenuOpen}
currentTheme={theme}
onThemeChange={setTheme}
/>
{/* 主内容区 - 配额圆环展示 */}
<main className="flex-1 flex flex-col items-center justify-center p-8">

View File

@@ -0,0 +1,140 @@
/**
* AlertBadge 组件
*
* 告警徽章组件,显示告警数量和状态。
* 支持三种告警类型Alarm紧急、Warning警告、Caution注意
*/
import { AlertTriangle, Bell, BellOff, XCircle } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
/** 告警类型枚举 */
export enum AlertType {
Caution = 'caution',
Warning = 'warning',
Alarm = 'alarm',
}
export interface AlertBadgeProps {
/** 告警数量 */
count: number
/** 告警类型 */
alertType: AlertType
/** 是否静音 */
muted?: boolean
/** 静音按钮点击事件 */
onMuteClick?: () => void
/** 额外的 className */
className?: string
}
/** 告警类型对应的颜色配置 - 使用 OpenBridge CSS 变量 */
const ALERT_COLORS: Record<
AlertType,
{ bgVar: string; textClass: string; icon: typeof AlertTriangle }
> = {
[AlertType.Alarm]: {
bgVar: 'var(--alert-alarm-color, rgb(227, 0, 25))',
textClass: 'text-white',
icon: XCircle,
},
[AlertType.Warning]: {
bgVar: 'var(--alert-warning-color, rgb(254, 148, 19))',
textClass: 'text-white',
icon: AlertTriangle,
},
[AlertType.Caution]: {
bgVar: 'var(--alert-caution-color, rgb(255, 219, 55))',
textClass: 'text-gray-900',
icon: AlertTriangle,
},
}
export const AlertBadge = ({
count,
alertType,
muted = false,
onMuteClick,
className,
}: AlertBadgeProps) => {
const { bgVar, textClass, icon: Icon } = ALERT_COLORS[alertType]
if (count === 0) {
return null
}
return (
<div className={cn('flex items-center gap-1', className)}>
{/* 告警徽章 */}
<div
className={cn(
'flex items-center gap-1.5 px-2.5 py-1 rounded-full',
textClass,
)}
style={{ backgroundColor: bgVar }}
>
<Icon className="size-4" />
<span className="text-sm font-medium">{count}</span>
</div>
{/* 静音按钮 */}
{onMuteClick && (
<Button
variant="ghost"
size="icon-sm"
onClick={onMuteClick}
className={cn(
'hover:bg-[var(--element-hover-color,rgba(0,0,0,0.12))]',
muted && 'opacity-50',
)}
aria-label={muted ? '取消静音' : '静音告警'}
>
{muted ? (
<BellOff className="size-4 text-[var(--element-inactive-color,#707070)]" />
) : (
<Bell className="size-4 text-[var(--element-active-color,#3d3d3d)]" />
)}
</Button>
)}
</div>
)
}
export interface AlertNotificationProps {
/** 账户名 */
account: string
/** 剩余百分比 */
remainingPercent: number
/** 告警类型 */
alertType: AlertType
}
/** 获取告警类型对应的 CSS 变量颜色 */
const getAlertColor = (alertType: AlertType): string => {
switch (alertType) {
case AlertType.Alarm:
return 'var(--alert-alarm-color, rgb(227, 0, 25))'
case AlertType.Warning:
return 'var(--alert-warning-color, rgb(254, 148, 19))'
case AlertType.Caution:
return 'var(--alert-caution-color, rgb(255, 219, 55))'
}
}
export const AlertNotification = ({
account,
remainingPercent,
alertType,
}: AlertNotificationProps) => {
const { icon: Icon } = ALERT_COLORS[alertType]
const alertColor = getAlertColor(alertType)
return (
<div className="flex items-center gap-2 px-3 py-2 text-sm">
<Icon className="size-4 shrink-0" style={{ color: alertColor }} />
<span className="text-[var(--element-active-color,#3d3d3d)]">
{account}: {remainingPercent}%
</span>
</div>
)
}

View File

@@ -0,0 +1,130 @@
/**
* ThemeSidebar 组件
*
* 主题切换侧边栏,使用 shadcn Sheet 组件实现。
* 支持 OpenBridge 四种主题day/bright/dusk/night
*/
import { Check, Moon, Sun, SunDim, Sunrise } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
import type { ObcTheme } from '@/hooks/useTheme'
import { cn } from '@/lib/utils'
export interface ThemeSidebarProps {
/** 是否打开 */
open: boolean
/** 关闭事件 */
onOpenChange: (open: boolean) => void
/** 当前主题 */
currentTheme: ObcTheme
/** 主题切换事件 */
onThemeChange: (theme: ObcTheme) => void
}
/** 主题配置 */
const THEME_OPTIONS: Array<{
value: ObcTheme
label: string
icon: typeof Sun
description: string
}> = [
{
value: 'day',
label: '白天模式',
icon: Sun,
description: '明亮的白色背景',
},
{
value: 'bright',
label: '明亮模式',
icon: SunDim,
description: '高对比度明亮主题',
},
{
value: 'dusk',
label: '黄昏模式',
icon: Sunrise,
description: '柔和的暖色调',
},
{
value: 'night',
label: '夜间模式',
icon: Moon,
description: '深色背景护眼',
},
]
export const ThemeSidebar = ({
open,
onOpenChange,
currentTheme,
onThemeChange,
}: ThemeSidebarProps) => {
const handleThemeSelect = (theme: ObcTheme) => {
onThemeChange(theme)
onOpenChange(false)
}
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
side="left"
className="w-72 bg-[var(--container-background-color,#fcfcfc)] border-[var(--divider-color,#e0e0e0)]"
showCloseButton={false}
>
<SheetHeader className="border-b border-[var(--divider-color,#e0e0e0)] pb-4">
<SheetTitle className="text-[var(--on-container-color,#1a1a1a)]">
</SheetTitle>
</SheetHeader>
<nav className="flex flex-col gap-1 py-4">
{THEME_OPTIONS.map(({ value, label, icon: Icon, description }) => {
const isActive = currentTheme === value
return (
<Button
key={value}
variant="ghost"
onClick={() => handleThemeSelect(value)}
className={cn(
'w-full justify-start gap-3 h-14 px-3',
'hover:bg-[var(--element-hover-color,rgba(0,0,0,0.12))]',
isActive &&
'bg-[var(--element-focused-color,rgba(0,0,0,0.08))]',
)}
>
<div
className={cn(
'size-10 rounded-lg flex items-center justify-center',
'bg-[var(--container-section-color,#f0f0f0)]',
)}
>
<Icon className="size-5 text-[var(--element-active-color,#1a1a1a)]" />
</div>
<div className="flex-1 text-left">
<div className="text-sm font-medium text-[var(--element-active-color,#1a1a1a)]">
{label}
</div>
<div className="text-xs text-[var(--element-inactive-color,#666)]">
{description}
</div>
</div>
{isActive && (
<Check className="size-5 text-[var(--alert-running-color,#34C759)]" />
)}
</Button>
)
})}
</nav>
</SheetContent>
</Sheet>
)
}

View File

@@ -0,0 +1,84 @@
/**
* TopBar 组件
*
* 基于 OpenBridge 设计系统的顶部导航栏,使用 shadcn/ui 重新实现。
* 特性:汉堡菜单按钮、应用标题、右侧告警/操作区域
*/
import { Menu } from 'lucide-react'
import type { ReactNode } from 'react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
export interface TopBarProps {
/** 应用标题 */
appTitle: string
/** 页面名称(可选,显示在标题右侧) */
pageName?: string
/** 菜单按钮是否激活 */
menuButtonActivated?: boolean
/** 菜单按钮点击事件 */
onMenuButtonClick?: () => void
/** 右侧插槽内容(告警、操作按钮等) */
rightSlot?: ReactNode
/** 额外的 className */
className?: string
}
export const TopBar = ({
appTitle,
pageName,
menuButtonActivated = false,
onMenuButtonClick,
rightSlot,
className,
}: TopBarProps) => {
return (
<header
className={cn(
'h-12 flex items-center justify-between px-2',
'bg-[var(--container-background-color,#fcfcfc)]',
'border-b border-[var(--divider-color,#e0e0e0)]',
'shadow-[0px_2px_4px_0px_rgba(0,0,0,0.1)]',
className,
)}
>
{/* 左侧:汉堡菜单按钮 + 标题 */}
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={onMenuButtonClick}
className={cn(
'size-8 rounded-md',
'hover:bg-[var(--element-focused-color,rgba(0,0,0,0.08))]',
menuButtonActivated &&
'bg-[var(--element-active-color,rgba(0,0,0,0.12))]',
)}
aria-label="打开菜单"
aria-expanded={menuButtonActivated}
>
<Menu className="size-5 text-[var(--on-container-color,#1a1a1a)]" />
</Button>
<div className="flex items-center gap-1.5">
<span className="text-base font-medium text-[var(--on-container-color,#1a1a1a)]">
{appTitle}
</span>
{pageName && (
<>
<span className="text-[var(--on-container-low-color,#666)]">
/
</span>
<span className="text-sm text-[var(--on-container-low-color,#666)]">
{pageName}
</span>
</>
)}
</div>
</div>
{/* 右侧:告警/操作区域 */}
{rightSlot && <div className="flex items-center gap-1">{rightSlot}</div>}
</header>
)
}

View File

@@ -0,0 +1,64 @@
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import type * as React from 'react'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
'icon-sm': 'size-8',
'icon-lg': 'size-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
function Button({
className,
variant = 'default',
size = 'default',
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button'
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

141
src/components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,141 @@
import * as SheetPrimitive from '@radix-ui/react-dialog'
import { XIcon } from 'lucide-react'
import type * as React from 'react'
import { cn } from '@/lib/utils'
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
'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/50',
className,
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = 'right',
showCloseButton = true,
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: 'top' | 'right' | 'bottom' | 'left'
showCloseButton?: boolean
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
side === 'right' &&
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
side === 'left' &&
'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
side === 'top' &&
'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
side === 'bottom' &&
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
className,
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="sheet-header"
className={cn('flex flex-col gap-1.5 p-4', className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="sheet-footer"
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn('text-foreground font-semibold', className)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@@ -9,8 +9,8 @@ import { dirname, join } from 'node:path'
import { createEnv } from '@t3-oss/env-core'
import { z } from 'zod'
/** Token 使用量 API 的默认地址 */
const DEFAULT_TOKEN_USAGE_URL = 'http://10.0.1.1:8318/usage'
/** Token 使用量 API 的默认地址 (按模型分组) */
const DEFAULT_TOKEN_USAGE_URL = 'http://10.0.1.1:8318/api/usage/model'
/** 服务器端口默认值 */
const DEFAULT_SERVER_PORT = '3000'

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -8,17 +8,24 @@ import { env } from '@/env'
import { dbProvider } from '@/orpc/middlewares'
import { os } from '@/orpc/server'
/** 远程 API 响应中的模型数据结构 */
interface RemoteModelData {
model: string
displayName?: string
/** 远程 API 响应中的账户数据结构 */
interface RemoteAccountData {
accountName: string
remainingFraction: number
resetTime?: string
status?: string
}
/** 远程 API 响应中的模型数据结构 (按模型分组) */
interface RemoteModelData {
modelId: string
displayName?: string
accounts: RemoteAccountData[]
}
/** 远程 API 响应结构 */
interface RemoteResponse {
result: Record<string, RemoteModelData[]>
data: RemoteModelData[]
}
export const getUsage = os.usage.getUsage
@@ -31,7 +38,7 @@ export const getUsage = os.usage.getUsage
}
const data = (await response.json()) as RemoteResponse
// 2. 解析并筛选每个账户的 claude-opus-4-5-thinking 模型
// 2. 找到 claude-opus-4-5-thinking 模型,然后从其 accounts 中提取数据
const opusModels: Array<{
account: string
model: string
@@ -40,21 +47,22 @@ export const getUsage = os.usage.getUsage
resetTime?: string
}> = []
for (const [accountFile, models] of Object.entries(data.result)) {
const account = accountFile.replace('.json', '')
// 新 API 按模型分组,找到目标模型
const opusModelData = data.data.find(
(m) => m.modelId === 'claude-opus-4-5-thinking',
)
// 只找 claude-opus-4-5-thinking 模型
const opusModel = models.find(
(m) => m.model === 'claude-opus-4-5-thinking',
)
if (opusModelData) {
// 遍历该模型下的所有账户
for (const accountData of opusModelData.accounts) {
const account = accountData.accountName.replace('.json', '')
if (opusModel) {
opusModels.push({
account,
model: opusModel.model,
displayName: opusModel.displayName,
remainingFraction: opusModel.remainingFraction,
resetTime: opusModel.resetTime,
model: opusModelData.modelId,
displayName: opusModelData.displayName,
remainingFraction: accountData.remainingFraction,
resetTime: accountData.resetTime,
})
}
}

View File

@@ -1 +1,292 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
/* ============================================================================
* OpenBridge 航海设计系统 - 四种光照条件主题
*
* 符合 IMO 航海规范,确保在各种光照条件下的可读性:
* - Day: 日间正常光照
* - Bright: 强光/阳光直射
* - Dusk: 黄昏/低光照
* - Night: 夜间模式(保护夜视能力)
* ============================================================================ */
/* Day 主题 - 日间正常光照 */
:root[data-obc-theme='day'] {
/* 容器/背景颜色 */
--container-global-color: rgb(255, 255, 255);
--container-background-color: rgb(247, 247, 247);
--container-section-color: rgb(240, 240, 240);
--container-backdrop-color: rgb(224, 224, 224);
/* 文字/元素颜色 - 深色文字确保可读性 */
--element-active-color: rgb(61, 61, 61);
--element-neutral-color: rgb(83, 83, 83);
--element-inactive-color: rgb(112, 112, 112);
--element-disabled-color: rgb(202, 202, 202);
/* 兼容旧变量名 */
--on-container-color: rgb(61, 61, 61);
--on-container-low-color: rgb(112, 112, 112);
/* 分割线/边框颜色 */
--border-outline-color: rgb(221, 221, 221);
--border-divider-color: rgb(221, 221, 221);
--border-silhouette-color: rgb(247, 247, 247);
--divider-color: rgb(221, 221, 221);
/* 交互状态 */
--element-focused-color: rgba(0, 0, 0, 0.08);
--element-hover-color: rgba(0, 0, 0, 0.12);
/* 告警颜色 */
--alert-alarm-color: rgb(227, 0, 25);
--alert-warning-color: rgb(254, 148, 19);
--alert-caution-color: rgb(255, 219, 55);
--alert-running-color: rgb(0, 131, 0);
/* 按钮/强调色 */
--primary-color: rgb(51, 84, 131);
--border-focus-color: rgb(51, 84, 131);
}
/* Bright 主题 - 强光/阳光直射(高对比度) */
:root[data-obc-theme='bright'] {
/* 容器/背景颜色 - 纯白最大亮度 */
--container-global-color: rgb(255, 255, 255);
--container-background-color: rgb(255, 255, 255);
--container-section-color: rgb(255, 255, 255);
--container-backdrop-color: rgb(255, 255, 255);
/* 文字/元素颜色 - 最深黑色确保阳光下可读 */
--element-active-color: rgb(26, 26, 26);
--element-neutral-color: rgb(61, 61, 61);
--element-inactive-color: rgb(83, 83, 83);
--element-disabled-color: rgb(202, 202, 202);
/* 兼容旧变量名 */
--on-container-color: rgb(26, 26, 26);
--on-container-low-color: rgb(83, 83, 83);
/* 分割线/边框颜色 - 加粗边框增强对比 */
--border-outline-color: rgb(142, 142, 142);
--border-divider-color: rgb(142, 142, 142);
--border-silhouette-color: rgb(255, 255, 255);
--divider-color: rgb(142, 142, 142);
/* 交互状态 */
--element-focused-color: rgba(0, 0, 0, 0.12);
--element-hover-color: rgba(0, 0, 0, 0.18);
/* 告警颜色 - 饱和度更高 */
--alert-alarm-color: rgb(235, 0, 20);
--alert-warning-color: rgb(254, 148, 19);
--alert-caution-color: rgb(228, 186, 2);
--alert-running-color: rgb(6, 131, 6);
/* 按钮/强调色 - 更深的蓝色 */
--primary-color: rgb(34, 60, 97);
--border-focus-color: rgb(34, 60, 97);
}
/* Dusk 主题 - 黄昏/低光照 */
:root[data-obc-theme='dusk'] {
/* 容器/背景颜色 - 深灰色减少眩光 */
--container-global-color: rgb(36, 36, 36);
--container-background-color: rgb(26, 26, 26);
--container-section-color: rgb(18, 18, 18);
--container-backdrop-color: rgb(3, 3, 3);
/* 文字/元素颜色 - 浅灰色文字 */
--element-active-color: rgb(198, 198, 198);
--element-neutral-color: rgb(165, 165, 165);
--element-inactive-color: rgb(130, 130, 130);
--element-disabled-color: rgb(58, 58, 58);
/* 兼容旧变量名 */
--on-container-color: rgb(198, 198, 198);
--on-container-low-color: rgb(130, 130, 130);
/* 分割线/边框颜色 */
--border-outline-color: rgb(0, 0, 0);
--border-divider-color: rgba(255, 255, 255, 0.09);
--border-silhouette-color: rgb(0, 0, 0);
--divider-color: rgba(255, 255, 255, 0.09);
/* 交互状态 */
--element-focused-color: rgba(255, 255, 255, 0.08);
--element-hover-color: rgba(255, 255, 255, 0.12);
/* 告警颜色 */
--alert-alarm-color: rgb(203, 19, 29);
--alert-warning-color: rgb(254, 155, 41);
--alert-caution-color: rgb(233, 195, 0);
--alert-running-color: rgb(18, 119, 15);
/* 按钮/强调色 - 亮蓝色 */
--primary-color: rgb(133, 167, 216);
--border-focus-color: rgb(133, 167, 216);
}
/* Night 主题 - 夜间模式(保护夜视能力)
* 使用红色/黄色光谱,避免蓝光影响夜视适应 */
:root[data-obc-theme='night'] {
/* 容器/背景颜色 - 纯黑背景 */
--container-global-color: rgb(10, 10, 0);
--container-background-color: rgb(0, 0, 0);
--container-section-color: rgb(0, 0, 0);
--container-backdrop-color: rgb(5, 5, 0);
/* 文字/元素颜色 - 暗黄色文字(保护夜视) */
--element-active-color: rgb(71, 71, 0);
--element-neutral-color: rgba(71, 71, 0, 0.65);
--element-inactive-color: rgba(71, 71, 0, 0.45);
--element-disabled-color: rgba(71, 71, 0, 0.25);
/* 兼容旧变量名 */
--on-container-color: rgb(71, 71, 0);
--on-container-low-color: rgba(71, 71, 0, 0.65);
/* 分割线/边框颜色 */
--border-outline-color: rgba(71, 71, 0, 0.2);
--border-divider-color: rgba(71, 71, 0, 0.2);
--border-silhouette-color: rgb(0, 0, 0);
--divider-color: rgba(71, 71, 0, 0.2);
/* 交互状态 */
--element-focused-color: rgba(71, 71, 0, 0.15);
--element-hover-color: rgba(71, 71, 0, 0.25);
/* 告警颜色 - 低亮度红/黄/绿 */
--alert-alarm-color: rgb(77, 0, 5);
--alert-warning-color: rgb(92, 44, 0);
--alert-caution-color: rgb(92, 69, 0);
--alert-running-color: rgb(23, 56, 0);
/* 按钮/强调色 - 暗绿色 */
--primary-color: rgb(0, 51, 21);
--border-focus-color: rgb(71, 71, 0);
}
/* ============================================================================ */
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}